/*
 * Copyright (C) 2022 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.async;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.fail;

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

import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(AndroidJUnit4.class)
@SmallTest
public class CircularByteBufferTest {
    @Test
    public void writeBytes() {
        final int capacity = 23;
        CircularByteBuffer buffer = new CircularByteBuffer(capacity);
        assertEquals(0, buffer.size());
        assertEquals(0, buffer.getDirectReadSize());
        assertEquals(capacity, buffer.freeSize());
        assertEquals(capacity, buffer.getDirectWriteSize());

        final byte[] writeBuffer = new byte[15];
        buffer.writeBytes(writeBuffer, 0, writeBuffer.length);

        assertEquals(writeBuffer.length, buffer.size());
        assertEquals(writeBuffer.length, buffer.getDirectReadSize());
        assertEquals(capacity - writeBuffer.length, buffer.freeSize());
        assertEquals(capacity - writeBuffer.length, buffer.getDirectWriteSize());

        buffer.clear();
        assertEquals(0, buffer.size());
        assertEquals(0, buffer.getDirectReadSize());
        assertEquals(capacity, buffer.freeSize());
        assertEquals(capacity, buffer.getDirectWriteSize());
    }

    @Test
    public void writeBytes_withRollover() {
        doTestReadWriteWithRollover(new BufferAccessor(false, false));
    }

    @Test
    public void writeBytes_withFullBuffer() {
        doTestReadWriteWithFullBuffer(new BufferAccessor(false, false));
    }

    @Test
    public void directWriteBytes_withRollover() {
        doTestReadWriteWithRollover(new BufferAccessor(true, true));
    }

    @Test
    public void directWriteBytes_withFullBuffer() {
        doTestReadWriteWithFullBuffer(new BufferAccessor(true, true));
    }

    private void doTestReadWriteWithFullBuffer(BufferAccessor accessor) {
        CircularByteBuffer buffer = doTestReadWrite(accessor, 20, 5, 4);

        assertEquals(0, buffer.size());
        assertEquals(20, buffer.freeSize());
    }

    private void doTestReadWriteWithRollover(BufferAccessor accessor) {
        // All buffer sizes are prime numbers to ensure that some read or write
        // operations will roll over the end of the internal buffer.
        CircularByteBuffer buffer = doTestReadWrite(accessor, 31, 13, 7);

        assertNotEquals(0, buffer.size());
    }

    private CircularByteBuffer doTestReadWrite(BufferAccessor accessor,
            final int capacity, final int writeLen, final int readLen) {
        CircularByteBuffer buffer = new CircularByteBuffer(capacity);

        final byte[] writeBuffer = new byte[writeLen + 2];
        final byte[] peekBuffer = new byte[readLen + 2];
        final byte[] readBuffer = new byte[readLen + 2];

        final int numIterations = 1011;
        final int maxRemaining = readLen - 1;

        int currentWriteSymbol = 0;
        int expectedReadSymbol = 0;
        int expectedSize = 0;
        int totalWritten = 0;
        int totalRead = 0;

        for (int i = 0; i < numIterations; i++) {
            // Fill in with write buffers as much as possible.
            while (buffer.freeSize() >= writeLen) {
                currentWriteSymbol = fillTestBytes(writeBuffer, 1, writeLen, currentWriteSymbol);
                accessor.writeBytes(buffer, writeBuffer, 1, writeLen);

                expectedSize += writeLen;
                totalWritten += writeLen;
                assertEquals(expectedSize, buffer.size());
                assertEquals(capacity - expectedSize, buffer.freeSize());
            }

            // Keep reading into read buffers while there's still data.
            while (buffer.size() >= readLen) {
                peekBuffer[1] = 0;
                peekBuffer[2] = 0;
                buffer.peekBytes(2, peekBuffer, 3, readLen - 2);
                assertEquals(0, peekBuffer[1]);
                assertEquals(0, peekBuffer[2]);

                peekBuffer[2] = buffer.peek(1);

                accessor.readBytes(buffer, readBuffer, 1, readLen);
                peekBuffer[1] = readBuffer[1];

                expectedReadSymbol = checkTestBytes(
                    readBuffer, 1, readLen, expectedReadSymbol, totalRead);

                assertArrayEquals(peekBuffer, readBuffer);

                expectedSize -= readLen;
                totalRead += readLen;
                assertEquals(expectedSize, buffer.size());
                assertEquals(capacity - expectedSize, buffer.freeSize());
            }

            if (buffer.size() > maxRemaining) {
                fail("Too much data remaining: " + buffer.size());
            }
        }

        final int maxWritten = capacity * numIterations;
        final int minWritten = maxWritten / 2;
        if (totalWritten < minWritten || totalWritten > maxWritten
                || (totalWritten - totalRead) > maxRemaining) {
            fail("Unexpected counts: read=" + totalRead + ", written=" + totalWritten
                    + ", minWritten=" + minWritten + ", maxWritten=" + maxWritten);
        }

        return buffer;
    }

    @Test
    public void readBytes_overflow() {
        CircularByteBuffer buffer = new CircularByteBuffer(23);

        final byte[] dataBuffer = new byte[15];
        buffer.writeBytes(dataBuffer, 0, dataBuffer.length - 2);

        try {
            buffer.readBytes(dataBuffer, 0, dataBuffer.length);
            assertTrue(false);
        } catch (IllegalArgumentException e) {
            // expected
        }

        assertEquals(13, buffer.size());
        assertEquals(10, buffer.freeSize());
    }

    @Test
    public void writeBytes_overflow() {
        CircularByteBuffer buffer = new CircularByteBuffer(23);

        final byte[] dataBuffer = new byte[15];
        buffer.writeBytes(dataBuffer, 0, dataBuffer.length);

        try {
            buffer.writeBytes(dataBuffer, 0, dataBuffer.length);
            assertTrue(false);
        } catch (IllegalArgumentException e) {
            // expected
        }

        assertEquals(15, buffer.size());
        assertEquals(8, buffer.freeSize());
    }

    private static int fillTestBytes(byte[] buffer, int pos, int len, int startValue) {
        for (int i = 0; i < len; i++) {
            buffer[pos + i] = (byte) (startValue & 0xFF);
            startValue = (startValue + 1) % 256;
        }
        return startValue;
    }

    private static int checkTestBytes(
            byte[] buffer, int pos, int len, int startValue, int totalRead) {
        for (int i = 0; i < len; i++) {
            byte expectedValue = (byte) (startValue & 0xFF);
            if (expectedValue != buffer[pos + i]) {
                fail("Unexpected byte=" + (((int) buffer[pos + i]) & 0xFF)
                        + ", expected=" + (((int) expectedValue) & 0xFF)
                        + ", pos=" + (totalRead + i));
            }
            startValue = (startValue + 1) % 256;
        }
        return startValue;
    }

    private static final class BufferAccessor {
        private final boolean mDirectRead;
        private final boolean mDirectWrite;

        BufferAccessor(boolean directRead, boolean directWrite) {
            mDirectRead = directRead;
            mDirectWrite = directWrite;
        }

        void writeBytes(CircularByteBuffer buffer, byte[] src, int pos, int len) {
            if (mDirectWrite) {
                while (len > 0) {
                    if (buffer.getDirectWriteSize() == 0) {
                        fail("Direct write size is zero: free=" + buffer.freeSize()
                                + ", size=" + buffer.size());
                    }
                    int copyLen = Math.min(len, buffer.getDirectWriteSize());
                    System.arraycopy(src, pos, buffer.getDirectWriteBuffer(),
                        buffer.getDirectWritePos(), copyLen);
                    buffer.accountForDirectWrite(copyLen);
                    len -= copyLen;
                    pos += copyLen;
                }
            } else {
                buffer.writeBytes(src, pos, len);
            }
        }

        void readBytes(CircularByteBuffer buffer, byte[] dst, int pos, int len) {
            if (mDirectRead) {
                while (len > 0) {
                    if (buffer.getDirectReadSize() == 0) {
                        fail("Direct read size is zero: free=" + buffer.freeSize()
                                + ", size=" + buffer.size());
                    }
                    int copyLen = Math.min(len, buffer.getDirectReadSize());
                    System.arraycopy(
                        buffer.getDirectReadBuffer(), buffer.getDirectReadPos(), dst, pos, copyLen);
                    buffer.accountForDirectRead(copyLen);
                    len -= copyLen;
                    pos += copyLen;
                }
            } else {
                buffer.readBytes(dst, pos, len);
            }
        }
    }
}
