package CH.ifa.draw.util;

import java.awt.Color;
import java.awt.List;
import java.io.ByteArrayInputStream;
import java.io.IOException;

import org.junit.jupiter.api.Test;

import CH.ifa.draw.standard.NullDrawing;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
 * A test class for StorableInput.
 */
public class StorableInputTest {
    /** Tests if a class can be read as storable if its name is read from the InputStream. */
    @Test
    void testReadStorable() throws IOException {
        //given
        String testString = NullDrawing.class.getName();

        //when
        StorableInput storableInput =
            new StorableInput(new ByteArrayInputStream(testString.getBytes()), false);

        //then
        assertThat(storableInput.readStorable()).isNotNull();
    }

    /** Tests if null is returned if NULL is read from the InputStream. */
    @Test
    void testReadStorableInputStreamIsNull() throws IOException {
        //given
        String testString = "NULL";

        //when
        StorableInput storableInput =
            new StorableInput(new ByteArrayInputStream(testString.getBytes()), false);

        //then
        assertThat(storableInput.readStorable()).isNull();
    }

    /** Tests if an exception is thrown when no Storable can be retrieved. */
    @Test
    void testReadStorableInputThrowsExceptionWhenStreamIsEmpty() {
        //given
        String testString = "REF 0";

        //when
        StorableInput storableInput =
            new StorableInput(new ByteArrayInputStream(testString.getBytes()));

        //then
        assertThatThrownBy(storableInput::readStorable)
            .isInstanceOf(ArrayIndexOutOfBoundsException.class);
    }

    /** Tests if an InputStream with a String can be read. */
    @Test
    void testReadString() throws IOException {
        //given
        String testString = "testString";

        //when
        StorableInput storableInput =
            new StorableInput(new ByteArrayInputStream(testString.getBytes()), true);

        //then
        assertThat(storableInput.readString()).isEqualTo(testString);

    }

    /** Tests if an exception is thrown when anything but a String is read from the InputStream. */
    @Test
    void testReadStringThrowsExceptionWhenTokenNotString() {
        //given
        StorableInput storableInput =
            new StorableInput(new ByteArrayInputStream(new byte[0]), true);

        //when/then
        assertThatThrownBy(storableInput::readString).isInstanceOf(IOException.class);
    }

    /** Tests whether an int is correctly identified as int. */
    @Test
    void testCanReadInt() throws IOException {
        //given
        int testInt = 42;

        //when
        StorableInput storableInput = new StorableInput(String.valueOf(testInt));

        //then
        assertThat(storableInput.canReadInt()).isTrue();
    }

    /** Tests whether an exception is thrown if token is not an int. */
    @Test
    void testCanReadIntThrowsExceptionWhenTokenNotInt() {
        //given
        StorableInput storableInput = new StorableInput("NotAnInt");

        //when/then
        assertThatThrownBy(storableInput::readInt).isInstanceOf(IOException.class);
    }

    /** Tests whether an int is correctly read as int. */
    @Test
    void testReadInt() throws IOException {
        //given
        int testInt = 42;

        //when
        StorableInput storableInput = new StorableInput(String.valueOf(testInt));

        //then
        assertThat(storableInput.readInt()).isEqualTo(testInt);
    }

    /** Tests whether an exception is thrown if token is not an int. */
    @Test
    void testReadIntThrowsExceptionWhenTokenNotInt() {
        //given
        StorableInput storableInput = new StorableInput("NotAnInt");

        //when/then
        assertThatThrownBy(storableInput::readInt).isInstanceOf(IOException.class);
    }

    /** Tests whether three back to back int tokens are correctly identified as color. */
    @Test
    void testReadColor() throws IOException {
        //given
        String color = "122 45 80";

        //when
        StorableInput storableInput = new StorableInput(color);

        //then
        assertThat(storableInput.readColor()).isEqualTo(new Color(122, 45, 80));
    }

    /** Tests whether an exception is thrown if values for red/green/blue are invalid. */
    @Test
    void testReadColorThrowsExceptionWhenArgumentsAreInvalid() {
        //given
        String color = "-1 254 256";

        //when
        StorableInput storableInput = new StorableInput(color);

        //then
        assertThatThrownBy(storableInput::readColor).isInstanceOf(IllegalArgumentException.class);
    }

    /**
     *  Tests whether an exception is thrown if a string is provided instead
     *  of three back to back int tokens to read a color from.
     */
    @Test
    void testReadColorThrowsExceptionWhenTokenNotInt() {
        //given
        String color = "NotColorValues";

        //when
        StorableInput storableInput = new StorableInput(color);

        //when/then
        assertThatThrownBy(storableInput::readColor).isInstanceOf(IOException.class);
    }

    /** Tests whether a double is correctly identified as double. */
    @Test
    void testReadDouble() throws IOException {
        //given
        double testDouble = 42.34;

        //when
        StorableInput storableInput = new StorableInput(String.valueOf(testDouble));

        //then
        assertThat(storableInput.readDouble()).isEqualTo(testDouble);
    }

    /** Tests whether an exception is thrown if the token is not a double. */
    @Test
    void testReadDoubleThrowsExceptionWhenTokenNotDouble() {
        //given
        StorableInput storableInput = new StorableInput("NotADouble");

        //when/then
        assertThatThrownBy(storableInput::readDouble).isInstanceOf(IOException.class);
    }

    /** Tests whether a boolean int is correctly identified as boolean int. */
    @Test
    void testReadBoolean() throws IOException {
        //given
        int testBoolean = 1;

        //when
        StorableInput storableInput = new StorableInput(String.valueOf(testBoolean));

        //then
        assertThat(storableInput.readBoolean()).isTrue();
    }

    /** Tests whether an exception is thrown if token is not int. */
    @Test
    void testReadBooleanThrowsExceptionWhenTokenNotInt() {
        //given
        StorableInput storableInput = new StorableInput("notABoolean");

        //when/then
        assertThatThrownBy(storableInput::readBoolean).isInstanceOf(IOException.class);
    }

    /** Tests whether a new instance can be made with a method provided by StorableInput. */
    @Test
    void testMakeInstance() throws IOException {
        //given
        String testString = "NULL";

        //when
        StorableInput storableInput =
            new StorableInput(new ByteArrayInputStream(testString.getBytes()), true);

        //then
        assertThat(storableInput.makeInstance(List.class.getName())).isNotNull();
    }

    /**
     * Tests whether an exception is thrown when the class provided to the
     * <code>makeInstance()</code> method does not exist.
     */
    @Test
    void testMakeInstanceThrowsExceptionWhenClassDoesNotExist() {
        //given
        String testString = "NULL";

        //when
        StorableInput storableInput =
            new StorableInput(new ByteArrayInputStream(testString.getBytes()), true);

        //then
        assertThatThrownBy(() -> storableInput.makeInstance("NonExistingClass"))
            .isInstanceOf(UnknownTypeException.class);
    }

    /**
     * Tests whether an exception is thrown when the class provided to
     * {@code makeInstance()} cannot be instantiated.
     */
    @Test
    void testMakeInstanceThrowsExceptionWhenClassCannotBeInstantiated() {
        //given
        String testString = "NULL";

        //when
        StorableInput storableInput =
            new StorableInput(new ByteArrayInputStream(testString.getBytes()), true);

        //then
        assertThatThrownBy(() -> storableInput.makeInstance(StorableInOut.class.getName()))
            .isInstanceOf(IOException.class);
    }
}
