/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.net.module.util.wear;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.ignoreStubs;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import android.os.ParcelFileDescriptor;

import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import com.android.net.module.util.async.AsyncFile;
import com.android.net.module.util.async.BufferedFile;
import com.android.net.module.util.async.EventManager;
import com.android.net.module.util.async.FileHandle;
import com.android.net.module.util.async.ReadableByteBuffer;
import com.android.testutils.async.ReadableDataAnswer;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

@RunWith(AndroidJUnit4.class)
@SmallTest
public class StreamingPacketFileTest {
    private static final int MAX_PACKET_SIZE = 100;

    @Mock EventManager mockEventManager;
    @Mock PacketFile.Listener mockFileListener;
    @Mock AsyncFile mockAsyncFile;
    @Mock ParcelFileDescriptor mockParcelFileDescriptor;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
    }

    @After
    public void tearDown() throws Exception {
        verifyNoMoreInteractions(ignoreStubs(mockFileListener, mockAsyncFile, mockEventManager));
    }

    @Test
    public void continueReadingAndClose() throws Exception {
        final int maxBufferedInboundPackets = 3;
        final int maxBufferedOutboundPackets = 5;

        final StreamingPacketFile file =
            createFile(maxBufferedInboundPackets, maxBufferedOutboundPackets);
        final BufferedFile bufferedFile = file.getUnderlyingFileForTest();

        assertEquals(maxBufferedInboundPackets * (MAX_PACKET_SIZE + 2),
            bufferedFile.getInboundBufferFreeSizeForTest());
        assertEquals(maxBufferedOutboundPackets * (MAX_PACKET_SIZE + 2),
            bufferedFile.getOutboundBufferFreeSize());
        assertEquals(bufferedFile.getOutboundBufferFreeSize() - 2,
            file.getOutboundFreeSize());

        file.continueReading();
        verify(mockAsyncFile).enableReadEvents(true);

        file.close();
        verify(mockAsyncFile).close();
    }

    @Test
    public void enqueueOutboundPacket() throws Exception {
        final int maxBufferedInboundPackets = 10;
        final int maxBufferedOutboundPackets = 20;

        final StreamingPacketFile file =
            createFile(maxBufferedInboundPackets, maxBufferedOutboundPackets);
        final BufferedFile bufferedFile = file.getUnderlyingFileForTest();

        final byte[] packet1 = new byte[11];
        final byte[] packet2 = new byte[12];
        packet1[0] = (byte) 1;
        packet2[0] = (byte) 2;

        assertEquals(0, bufferedFile.getOutboundBufferSize());

        when(mockAsyncFile.write(any(), anyInt(), anyInt())).thenReturn(0);
        assertTrue(file.enqueueOutboundPacket(packet1, 0, packet1.length));
        verify(mockAsyncFile).enableWriteEvents(true);

        assertEquals(packet1.length + 2, bufferedFile.getOutboundBufferSize());

        checkAndResetMocks();

        final int totalLen = packet1.length + packet2.length + 4;

        final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
        final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
        final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
        when(mockAsyncFile.write(
            arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture())).thenReturn(totalLen);

        assertTrue(file.enqueueOutboundPacket(packet2, 0, packet2.length));

        assertEquals(0, bufferedFile.getInboundBuffer().size());
        assertEquals(0, bufferedFile.getOutboundBufferSize());

        assertEquals(0, posCaptor.getValue().intValue());
        assertEquals(totalLen, lenCaptor.getValue().intValue());

        final byte[] capturedData = arrayCaptor.getValue();
        assertEquals(packet1.length, NetPacketHelpers.decodeNetworkUnsignedInt16(capturedData, 0));
        assertEquals(packet2.length,
            NetPacketHelpers.decodeNetworkUnsignedInt16(capturedData, packet1.length + 2));
        assertEquals(packet1[0], capturedData[2]);
        assertEquals(packet2[0], capturedData[packet1.length + 4]);
    }

    @Test
    public void onInboundPacket() throws Exception {
        final int maxBufferedInboundPackets = 10;
        final int maxBufferedOutboundPackets = 20;

        final StreamingPacketFile file =
            createFile(maxBufferedInboundPackets, maxBufferedOutboundPackets);
        final BufferedFile bufferedFile = file.getUnderlyingFileForTest();
        final ReadableByteBuffer inboundBuffer = bufferedFile.getInboundBuffer();

        final int len1 = 11;
        final int len2 = 12;
        final byte[] data = new byte[len1 + len2 + 4];
        NetPacketHelpers.encodeNetworkUnsignedInt16(len1, data, 0);
        NetPacketHelpers.encodeNetworkUnsignedInt16(len2, data, 11 + 2);
        data[2] = (byte) 1;
        data[len1 + 4] = (byte) 2;

        final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data);

        final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
        final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
        final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);

        when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
        when(mockFileListener.onPreambleData(any(), eq(0), eq(data.length))).thenReturn(0);
        bufferedFile.onReadReady(mockAsyncFile);
        verify(mockAsyncFile).enableReadEvents(true);
        verify(mockFileListener).onInboundBuffered(data.length, data.length);
        verify(mockFileListener).onInboundPacket(
            arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture());
        verify(mockEventManager).execute(any());

        byte[] capturedData = arrayCaptor.getValue();
        assertEquals(2, posCaptor.getValue().intValue());
        assertEquals(len1, lenCaptor.getValue().intValue());
        assertEquals((byte) 1, capturedData[2]);

        checkAndResetMocks();

        when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
        file.onBufferedFileInboundData(0);
        verify(mockFileListener).onInboundPacket(
            arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture());
        verify(mockEventManager).execute(any());

        capturedData = arrayCaptor.getValue();
        assertEquals(2, posCaptor.getValue().intValue());
        assertEquals(len2, lenCaptor.getValue().intValue());
        assertEquals((byte) 2, capturedData[2]);

        assertEquals(0, bufferedFile.getOutboundBufferSize());
        assertEquals(0, inboundBuffer.size());
    }

    @Test
    public void onReadReady_preambleData() throws Exception {
        final int maxBufferedInboundPackets = 10;
        final int maxBufferedOutboundPackets = 20;

        final StreamingPacketFile file =
            createFile(maxBufferedInboundPackets, maxBufferedOutboundPackets);
        final BufferedFile bufferedFile = file.getUnderlyingFileForTest();
        final ReadableByteBuffer inboundBuffer = bufferedFile.getInboundBuffer();

        final int preambleLen = 23;
        final int len1 = 11;
        final byte[] data = new byte[preambleLen + 2 + len1];
        NetPacketHelpers.encodeNetworkUnsignedInt16(len1, data, preambleLen);
        data[preambleLen + 2] = (byte) 1;

        final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data);

        when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
        when(mockFileListener.onPreambleData(any(), eq(0), eq(data.length))).thenReturn(5);
        when(mockFileListener.onPreambleData(
            any(), eq(0), eq(data.length - 5))).thenReturn(preambleLen - 5);
        when(mockFileListener.onPreambleData(
            any(), eq(0), eq(data.length - preambleLen))).thenReturn(0);

        bufferedFile.onReadReady(mockAsyncFile);

        final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
        final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
        final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);

        verify(mockFileListener).onInboundBuffered(data.length, data.length);
        verify(mockFileListener).onInboundPacket(
            arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture());
        verify(mockEventManager).execute(any());
        verify(mockAsyncFile).enableReadEvents(true);

        final byte[] capturedData = arrayCaptor.getValue();
        assertEquals(2, posCaptor.getValue().intValue());
        assertEquals(len1, lenCaptor.getValue().intValue());
        assertEquals((byte) 1, capturedData[2]);

        assertEquals(0, bufferedFile.getOutboundBufferSize());
        assertEquals(0, inboundBuffer.size());
    }

    @Test
    public void shutdownReading() throws Exception {
        final int maxBufferedInboundPackets = 10;
        final int maxBufferedOutboundPackets = 20;

        final StreamingPacketFile file =
            createFile(maxBufferedInboundPackets, maxBufferedOutboundPackets);
        final BufferedFile bufferedFile = file.getUnderlyingFileForTest();

        final byte[] data = new byte[100];
        final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data);
        when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);

        doAnswer(new Answer() {
            @Override public Object answer(InvocationOnMock invocation) {
                file.shutdownReading();
                return Integer.valueOf(-1);
            }}).when(mockFileListener).onPreambleData(any(), anyInt(), anyInt());

        bufferedFile.onReadReady(mockAsyncFile);

        verify(mockFileListener).onInboundBuffered(data.length, data.length);
        verify(mockAsyncFile).enableReadEvents(false);

        assertEquals(0, bufferedFile.getInboundBuffer().size());
    }

    private void checkAndResetMocks() {
        verifyNoMoreInteractions(ignoreStubs(mockFileListener, mockAsyncFile, mockEventManager,
            mockParcelFileDescriptor));
        reset(mockFileListener, mockAsyncFile, mockEventManager);
    }

    private StreamingPacketFile createFile(
            int maxBufferedInboundPackets, int maxBufferedOutboundPackets) throws Exception {
        when(mockEventManager.registerFile(any(), any())).thenReturn(mockAsyncFile);
        return new StreamingPacketFile(
            mockEventManager,
            FileHandle.fromFileDescriptor(mockParcelFileDescriptor),
            mockFileListener,
            MAX_PACKET_SIZE,
            maxBufferedInboundPackets,
            maxBufferedOutboundPackets);
    }
}
