package com.github.jinahya.bit.io;



import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;

import java.io.IOException;

import static com.github.jinahya.bit.io.BitIoTests.randomSizeForByte;
import static com.github.jinahya.bit.io.BitIoTests.randomSizeForChar;
import static com.github.jinahya.bit.io.BitIoTests.randomSizeForInt;
import static com.github.jinahya.bit.io.BitIoTests.randomSizeForLong;
import static com.github.jinahya.bit.io.BitIoTests.randomSizeForShort;
import static java.util.concurrent.ThreadLocalRandom.current;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.lenient;

/**
 * A class for unit-testing {@link AbstractBitInput} with a spy.
 *
 * @author Jin Kwon <onacit_at_gmail.com>
 * @see AbstractBitOutputSpyTest
 */
@ExtendWith({MockitoExtension.class})
@Slf4j
final class AbstractBitInputSpyTest {

    // -----------------------------------------------------------------------------------------------------------------
    @BeforeEach
    void stubRead() throws IOException {
        lenient().when(bitInput.read()).thenReturn(current().nextInt(0, 256));
    }

    // -----------------------------------------------------------------------------------------------------------------
    @AfterEach
    void alignAfterEach() throws IOException {
        bitInput.align(current().nextInt(1, 8));
    }

    // ------------------------------------------------------------------------------------------------------------ byte

    /**
     * Tests {@link AbstractBitInput#readByte(boolean, int)} method.
     *
     * @throws IOException if an I/O error occurs.
     */
    @RepeatedTest(8)
    void testReadByte() throws IOException {
        final boolean unsigned = current().nextBoolean();
        final int size = randomSizeForByte(unsigned);
        final byte value = bitInput.readByte(unsigned, size);
        if (unsigned) {
            assertEquals(0, value >> size);
        } else {
            if (value >= 0) {
                assertEquals(0, value >> (size - 1));
            } else {
                assertEquals(-1, value >> (size - 1));
            }
        }
    }

    // ----------------------------------------------------------------------------------------------------------- short
    @RepeatedTest(8)
    void testReadShort() throws IOException {
        final boolean unsigned = current().nextBoolean();
        final int size = randomSizeForShort(unsigned);
        final short value = bitInput.readShort(unsigned, size);
        if (unsigned) {
            assertEquals(0, value >> size);
        } else {
            if (value >= 0) {
                assertEquals(0, value >> (size - 1));
            } else {
                assertEquals(-1, value >> (size - 1));
            }
        }
    }

    // ------------------------------------------------------------------------------------------------------------- int
    @RepeatedTest(8)
    void testReadInt() throws IOException {
        final boolean unsigned = current().nextBoolean();
        final int size = randomSizeForInt(unsigned);
        final int value = bitInput.readInt(unsigned, size);
        if (unsigned) {
            assertEquals(0, value >> size);
        } else {
            if (value >= 0) {
                assertEquals(0, value >> (size - 1));
            } else {
                assertEquals(-1, value >> (size - 1));
            }
        }
    }

    // ------------------------------------------------------------------------------------------------------------ long
    @RepeatedTest(8)
    void testReadSignedLong() throws IOException {
        final boolean unsigned = current().nextBoolean();
        final int size = randomSizeForLong(unsigned);
        final long value = bitInput.readLong(unsigned, size);
        if (unsigned) {
            assertEquals(0L, value >> size);
        } else {
            if (value >= 0L) {
                assertEquals(0L, value >> (size - 1));
            } else {
                assertEquals(-1L, value >> (size - 1));
            }
        }
    }

    // ------------------------------------------------------------------------------------------------------------ char
    @RepeatedTest(8)
    void testReadChar() throws IOException {
        final int size = randomSizeForChar();
        final char value = bitInput.readChar(size);
        assertEquals(0, value >> size);
    }

    // ------------------------------------------------------------------------------------------------------------ skip
    @Test
    void assertSkipThrowsIllegalArgumentExceptionWhenBitsIsZero() {
        assertThrows(IllegalArgumentException.class, () -> bitInput.skip(0));
    }

    @Test
    void assertSkipThrowsIllegalArgumentExceptionWhenBitsIsNegative() {
        assertThrows(IllegalArgumentException.class, () -> bitInput.skip(current().nextInt() | Integer.MIN_VALUE));
    }

    @Test
    void testSkip() throws IOException {
        bitInput.skip(current().nextInt(1, 129));
    }

    // ----------------------------------------------------------------------------------------------------------- align
    @Test
    void testAlign() throws IOException {
        final long discarded = bitInput.align(1);
        assertTrue(discarded >= 0L);
    }

    // -----------------------------------------------------------------------------------------------------------------
    @Spy
    private AbstractBitInput bitInput;
}
