package io.wealthwizards.appsynctesting.utils;

import io.wealthwizards.appsynctesting.AppSyncException;
import io.wealthwizards.appsynctesting.AppSyncState;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;

@DisplayName("$util (error methods)")
public class StatefulUtilTest {
    private io.wealthwizards.appsynctesting.utils.StatefulUtil statefulUtil;

    @BeforeEach
    void setup() {
        statefulUtil = new io.wealthwizards.appsynctesting.utils.StatefulUtil(
                new AppSyncState("New Auth Type", null)
        );
    }

    @Nested
    @DisplayName(".appendError")
    class AppendError {
        @Test
        @DisplayName("should accept message")
        public void shouldAcceptMessage() throws NoSuchFieldException, IllegalAccessException {
            statefulUtil.appendError("error message");

            var expected = List.of(
                    new AppSyncException("error message", null, null, null)
            );

            var field = statefulUtil.getClass().getDeclaredField("errors");
            field.setAccessible(true);

            var actual = field.get(statefulUtil);

            assertEquals(expected, actual);
        }

        @Test
        @DisplayName("should accept message, and error type")
        public void shouldAcceptErrorType() throws NoSuchFieldException, IllegalAccessException {
            statefulUtil.appendError("error message", "Unauthorized");

            var expected = List.of(
                    new AppSyncException("error message", "Unauthorized", null, null)
            );

            var field = statefulUtil.getClass().getDeclaredField("errors");
            field.setAccessible(true);

            var actual = field.get(statefulUtil);

            assertEquals(expected, actual);
        }

        @Test
        @DisplayName("should accept message, error type, and error")
        public void shouldAcceptError() throws NoSuchFieldException, IllegalAccessException {
            statefulUtil.appendError("error message", "Unauthorized", Map.of("key", "value"));

            var expected = List.of(
                    new AppSyncException(
                            "error message",
                            "Unauthorized",
                            Map.of("key", "value"),
                            null
                    )
            );

            var field = statefulUtil.getClass().getDeclaredField("errors");
            field.setAccessible(true);

            var actual = field.get(statefulUtil);

            assertEquals(expected, actual);
        }

        @Test
        @DisplayName("should accept message, error type, error and error info")
        public void shouldAcceptErrorInfo() throws NoSuchFieldException, IllegalAccessException {
            statefulUtil.appendError(
                    "error message",
                    "Unauthorized",
                    Map.of("key", "value"),
                    Map.of("anotherKey", "anotherValue")
            );

            var expected = List.of(
                    new AppSyncException(
                            "error message",
                            "Unauthorized",
                            Map.of("key", "value"),
                            Map.of("anotherKey", "anotherValue")
                    )
            );

            var field = statefulUtil.getClass().getDeclaredField("errors");
            field.setAccessible(true);

            var actual = field.get(statefulUtil);

            assertEquals(expected, actual);
        }
    }

    @Test
    @DisplayName(".unauthorized should throw")
    public void shouldThrowUnauthorizedException() {
        var exception = assertThrows(
                AppSyncException.class,
                () -> statefulUtil.unauthorized()
        );

        var expected = new AppSyncException(
                "Unauthorized",
                "Unauthorized",
                null,
                null
        );

        assertEquals(expected, exception);
    }

    @Nested
    @DisplayName(".error")
    class Error {
        @Test
        @DisplayName("message should be encoded in thrown exception AppSync output")
        public void shouldThrowMessageException() {
            var exception = assertThrows(
                    AppSyncException.class,
                    () -> statefulUtil.error("Uh oh, this was unexpected")
            );

            var expected = new AppSyncException(
                    "Uh oh, this was unexpected",
                    null,
                    null,
                    null
            );

            assertEquals(expected, exception);
        }

        @Test
        @DisplayName("message, and type should be encoded in thrown exception AppSync output")
        public void shouldThrowMessageAndTypeException() {
            var exception = assertThrows(
                    AppSyncException.class,
                    () -> statefulUtil.error("Uh oh, this was unexpected", "RealType")
            );

            var expected = new AppSyncException(
                    "Uh oh, this was unexpected",
                    "RealType",
                    null,
                    null
            );

            assertEquals(expected, exception);
        }

        @Test
        @DisplayName("message, type, and error should be encoded in thrown exception AppSync output")
        public void shouldThrowMessageTypeAndErrorException() {
            var exception = assertThrows(
                    AppSyncException.class,
                    () -> statefulUtil.error(
                            "Uh oh, this was unexpected",
                            "RealType",
                            Map.of(
                                    "key", "value"
                            )
                    )
            );

            var expected = new AppSyncException(
                    "Uh oh, this was unexpected",
                    "RealType",
                    java.util.Map.of("key", "value"),
                    null
            );

            assertEquals(expected, exception);
        }
    }

    @Nested
    @DisplayName(".validate")
    class Validate {
        @Test
        @DisplayName("should throw exception for message")
        public void shouldThrowExceptionWithMessage() {
            assertDoesNotThrow(() -> statefulUtil.validate(
                    true,
                    "valid so this should never throw"
            ));

            var exception = assertThrows(
                    AppSyncException.class,
                    () -> statefulUtil.validate(false, "uh oh")
            );

            var expected = new AppSyncException(
                    "uh oh",
                    null,
                    null,
                    null
            );

            assertEquals(expected, exception);
        }

        @Test
        @DisplayName("should throw exception for message, and type")
        public void shouldThrowExceptionWithMessageAndType() {
            assertDoesNotThrow(() -> statefulUtil.validate(
                    true,
                    "valid so this should never throw",
                    "RealType"
            ));

            var exception = assertThrows(
                    AppSyncException.class,
                    () -> statefulUtil.validate(false, "uh oh", "RealType")
            );

            var expected = new AppSyncException(
                    "uh oh",
                    "RealType",
                    null,
                    null
            );

            assertEquals(expected, exception);
        }

        @Test
        @DisplayName("should throw exception for message, and type")
        public void shouldThrowExceptionWithMessageTypeAndError() {
            assertDoesNotThrow(() -> statefulUtil.validate(
                    true,
                    "valid so this should never throw",
                    "RealType",
                    Map.of("key", "value")
            ));

            var exception = assertThrows(
                    AppSyncException.class,
                    () -> statefulUtil.validate(
                            false,
                            "uh oh",
                            "RealType",
                            java.util.Map.of("big", "error")
                    )
            );

            var expected = new AppSyncException(
                    "uh oh",
                    "RealType",
                    java.util.Map.of("big", "error"),
                    null
            );

            assertEquals(expected, exception);
        }
    }

    @Test
    @DisplayName(".authType should read state")
    public void shouldReadAuthTypeState() {
        assertEquals("New Auth Type", statefulUtil.authType());
    }
}
