package io.wealthwizards.appsynctesting.utils;

import com.github.f4b6a3.ulid.Ulid;
import com.github.f4b6a3.ulid.UlidCreator;
import io.wealthwizards.appsynctesting.AppSyncState;
import io.wealthwizards.appsynctesting.utils.dynamodb.DynamoDB;
import io.wealthwizards.appsynctesting.utils.transform.Transform;
import org.junit.jupiter.api.*;
import org.mockito.Mockito;

import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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

@DisplayName("$util (stateless methods)")
public class UtilTest {
    private Util util;

    @BeforeEach
    void setup() {
        util = new Util(
                new AppSyncState(),
                new Time(),
                new DynamoDB(),
                new io.wealthwizards.appsynctesting.utils.List(),
                new io.wealthwizards.appsynctesting.utils.Map(),
                new Str(),
                new Math(),
                new Transform(),
                new Rds(),
                new Http(),
                new Xml()
        );
    }

    @Test
    @DisplayName("inherits the stateful superclass methods")
    void shouldInheritStatefulMethods() {
        assertEquals(StatefulUtil.class, util.getClass().getSuperclass());
    }

    @Nested
    @DisplayName(".qr and .quiet")
    class Quiet {
        @Test
        @DisplayName("should accept nothing and return nothing")
        public void shouldAcceptNothingReturnNothing() throws NoSuchMethodException {
            var qrReturnType = util.getClass().getMethod("qr").getReturnType();
            var quietReturnType = util.getClass().getMethod("quiet").getReturnType();

            assertEquals(void.class, quietReturnType);
            assertEquals(void.class, qrReturnType);
        }

        @Test
        @DisplayName("should accept object and return nothing")
        public void shouldReturnNothing() throws NoSuchMethodException {
            var qrReturnType = util.getClass().getMethod("qr", Object.class).getReturnType();
            var quietReturnType = util.getClass().getMethod("quiet", Object.class).getReturnType();

            assertEquals(void.class, quietReturnType);
            assertEquals(void.class, qrReturnType);
        }
    }

    @Test
    @DisplayName(".escapeJavaScript should escape selected special chars")
    void shouldEscapeSpecialChars() {
        var specials = "'\"\t\0\b\f\n\r\\";
        var expected = "\\'\\\"\\t\\u0000\\b\\f\\n\\r\\\\";

        var actual = util.escapeJavaScript(specials);

        assertEquals(expected, actual);
    }

    @Test
    @DisplayName(".urlEncode should encode in application/x-www-form-urlencoded format")
    void shouldEncodeUrl() {
        var unencoded = "Artie, growing a mustache? Looks good.";
        var expected = "Artie%2C+growing+a+mustache%3F+Looks+good.";

        var actual = util.urlEncode(unencoded);

        assertEquals(expected, actual);
    }

    @Test
    @DisplayName(".urlDecode should decode from application/x-www-form-urlencoded format")
    void shouldDecodeUrl() {
        var encoded = "Artie%2C+growing+a+mustache%3F+Looks+good.";
        var expected = "Artie, growing a mustache? Looks good.";

        var actual = util.urlDecode(encoded);

        assertEquals(expected, actual);
    }

    @Nested
    @DisplayName(".base64Encode")
    class Base64Encode {
        @Test
        @DisplayName("should encode byte array to base64 string")
        void shouldEncodeBytesToBase64() {
            var unencoded = "According to all known laws of aviation, there is no way a bee should be able to fly.";
            var expected = "QWNjb3JkaW5nIHRvIGFsbCBrbm93biBsYXdzIG9mIGF2aWF0aW9uLCB0aGVyZSBpcyBubyB3YXkgYSBiZWUgc2hvdWxkIGJlIGFibGUgdG8gZmx5Lg==";

            var actual = util.base64Encode(unencoded.getBytes(StandardCharsets.UTF_8));

            assertEquals(expected, actual);
        }

        @Test
        @DisplayName("should encode string to base64 string")
        void shouldEncodeStringToBase64() {
            var unencoded = "According to all known laws of aviation, there is no way a bee should be able to fly.";
            var expected = "QWNjb3JkaW5nIHRvIGFsbCBrbm93biBsYXdzIG9mIGF2aWF0aW9uLCB0aGVyZSBpcyBubyB3YXkgYSBiZWUgc2hvdWxkIGJlIGFibGUgdG8gZmx5Lg==";

            var actual = util.base64Encode(unencoded);

            assertEquals(expected, actual);
        }
    }

    @Test
    @DisplayName(".base64Decode should decode base64 string")
    void shouldDecodeBase64() {
        var encoded = "SXRzIHdpbmdzIGFyZSB0b28gc21hbGwgdG8gZ2V0IGl0cyBmYXQgbGl0dGxlIGJvZHkgb2ZmIHRoZSBncm91bmQu";
        var expected = "Its wings are too small to get its fat little body off the ground.";

        var actual = util.base64Decode(encoded);

        assertEquals(expected, actual);
    }

    @Nested
    @DisplayName(".parseJson")
    class ParseJson {
        @Test
        @DisplayName("should handle top level booleans")
        void shouldHandleBooleans() {
            var encoded = "true";

            var actual = util.parseJson(encoded);

            assertEquals(true, actual);
        }

        @Test
        @DisplayName("should handle top level strings")
        void shouldHandleStrings() {
            var encoded = "\"Hello, World\"";

            var actual = util.parseJson(encoded);

            assertEquals("Hello, World", actual);
        }

        @Test
        @DisplayName("should handle maps")
        void shouldHandleMaps() {
            var encoded = "{ \"hi\": \"string\", \"nested\": [12, true, { \"bye\": \"see ya\" }] }";
            var expected = Map.of(
                    "hi", "string",
                    "nested", List.of(12d, true, Map.of("bye", "see ya"))
            );

            var actual = util.parseJson(encoded);

            assertEquals(expected, actual);
        }

        @Test
        @DisplayName("should handle arrays")
        void shouldHandleArrays() {
            var encoded = "[1, 2, \"three\"]";
            var expected = List.of(1d, 2d, "three");

            var actual = util.parseJson(encoded);

            assertTrue(expected.equals(actual));
        }
    }

    @Nested
    @DisplayName(".toJson")
    class ToJson {
        @Test
        @DisplayName("should handle top level booleans")
        void shouldHandleBooleans() {
            var actual = util.toJson(true);

            assertEquals("true", actual);
        }

        @Test
        @DisplayName("should handle top level strings")
        void shouldHandleStrings() {
            var actual = util.toJson("Hello, World");

            assertEquals("\"Hello, World\"", actual);
        }

        @Test
        @DisplayName("should handle maps")
        void shouldHandleMaps() {
            var object = Map.of("hi", "string");

            var expected = "{\"hi\":\"string\"}";
            var actual = util.toJson(object);

            assertEquals(expected, actual);
        }

        @Test
        @DisplayName("should handle arrays")
        void shouldHandleArrays() {
            var list = List.of(1, 2, "three");

            var expected = "[1,2,\"three\"]";
            var actual = util.toJson(list);

            assertEquals(expected, actual);
        }
    }

    @Test
    @DisplayName(".autoId should generate UUID")
    void shouldGenerateUuid() {
        var actual = util.autoId();

        assertTrue(actual.matches("[0-9a-fA-F]{8}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{12}"));
    }

    @Test
    @DisplayName(".autoUlid should generate ULID")
    void shouldGenerateUlid() {
        try (var mocked = Mockito.mockStatic(UlidCreator.class)) {
            var expected = "01EX8Y21KB8HPZNBP3PTW7HVEY";

            mocked.when(UlidCreator::getUlid).thenReturn(Ulid.from(expected));

            var actual = util.autoUlid();

            assertEquals(expected, actual);
        }
    }

    @Test
    @DisplayName(".isString should tell the truth")
    public void shouldImplementIsString() {
        assertTrue(util.isString("is a string"));

        assertFalse(util.isString(true));
        assertFalse(util.isString(null));
        assertFalse(util.isString(23));
    }

    @Test
    @DisplayName(".isNull should tell the truth")
    public void shouldImplementIsNull() {
        assertTrue(util.isNull(null));

        assertFalse(util.isNull(true));
        assertFalse(util.isNull("abc"));
        assertFalse(util.isNull(23));
    }

    @Test
    @DisplayName(".isNullOrEmpty should tell the truth")
    public void shouldImplementIsNullOrEmpty() {
        assertTrue(util.isNullOrEmpty(null));
        assertTrue(util.isNullOrEmpty(""));

        assertFalse(util.isNullOrEmpty(" "));
    }

    @Test
    @DisplayName(".isNullOrBlank should tell the truth")
    public void shouldImplementIsNullOrBlank() {
        assertTrue(util.isNullOrBlank(null));
        assertTrue(util.isNullOrBlank(""));
        assertTrue(util.isNullOrBlank(" "));

        assertFalse(util.isNullOrBlank(" abc "));
    }

    @Test
    @DisplayName(".defaultIfNull should return first as default when null")
    public void shouldImplementDefaultIfNull() {
        assertEquals("default", util.defaultIfNull("default", null));
        assertEquals("real", util.defaultIfNull("default", "real"));
        assertEquals(42, util.defaultIfNull(null, 42));
        assertNull(util.defaultIfNull(null, null));
    }

    @Test
    @DisplayName(".defaultIfNullOrEmpty should return first as default when null/empty")
    public void shouldImplementDefaultIfNullOrEmpty() {
        assertEquals("default", util.defaultIfNullOrEmpty("default", null));
        assertEquals("default", util.defaultIfNullOrEmpty("default", ""));
        assertEquals("real", util.defaultIfNullOrEmpty("default", "real"));
        assertNull(util.defaultIfNullOrEmpty(null, null));
    }

    @Test
    @DisplayName(".defaultIfNullOrBlank should return first as default when null/empty")
    public void shouldImplementDefaultIfNullOrBlank() {
        assertEquals("default", util.defaultIfNullOrBlank("default", null));
        assertEquals("default", util.defaultIfNullOrBlank("default", ""));
        assertEquals("default", util.defaultIfNullOrBlank("default", " "));
        assertEquals("real", util.defaultIfNullOrBlank("default", "real"));
        assertNull(util.defaultIfNullOrBlank(null, null));
    }

    @Test
    @DisplayName(".isNumber should tell the truth")
    public void shouldImplementIsNumber() {
        assertFalse(util.isNumber("123"));
        assertFalse(util.isNumber(null));
        assertFalse(util.isNumber(new Object()));

        assertTrue(util.isNumber(12));
        assertTrue(util.isNumber(12L));
        assertTrue(util.isNumber(12f));
        assertTrue(util.isNumber(12d));
    }

    @Test
    @DisplayName(".isBoolean should tell the truth")
    public void shouldImplementIsBoolean() {
        assertFalse(util.isBoolean("123"));
        assertFalse(util.isBoolean(null));
        assertFalse(util.isBoolean(new Object()));

        assertTrue(util.isBoolean(true));
        assertTrue(util.isBoolean(false));
    }

    @Test
    @DisplayName(".isList should tell the truth")
    public void shouldImplementIsList() {
        assertFalse(util.isList("123"));
        assertFalse(util.isList(null));
        assertFalse(util.isList(new Object()));
        assertFalse(util.isList(Map.of("a", 12)));

        assertTrue(util.isList(List.of("a", "b", 12)));
        assertTrue(
            util.isList(
                    new ArrayList<>(List.of("abc", "def"))
            )
        );
    }

    @Test
    @DisplayName(".isMap should tell the truth")
    public void shouldImplementIsMap() {
        assertFalse(util.isMap("123"));
        assertFalse(util.isMap(null));
        assertFalse(util.isMap(new Object()));
        assertFalse(util.isMap(List.of("a", 12)));

        assertTrue(util.isMap(Map.of("a", 12)));
        assertTrue(
                util.isMap(
                        new HashMap<>(Map.of("abc", "def"))
                )
        );
    }

    @Test
    @DisplayName(".typeOf should support expected types")
    public void shouldImplementTypeOf() {
        assertEquals("Null", util.typeOf(null));
        assertEquals("Number", util.typeOf(12));
        assertEquals("String", util.typeOf("Hi"));
        assertEquals("Map", util.typeOf(Map.of("Abc", 12)));
        assertEquals("List", util.typeOf(List.of("Abc", 12)));
        assertEquals("Boolean", util.typeOf(false));
        assertEquals("Object", util.typeOf(new Object()));
        assertEquals("Object", util.typeOf(new StringWriter()));
    }

    @Test
    @DisplayName(".matches behaves itself")
    public void shouldImplementMatches() {
        assertTrue(util.matches("a*b+", "aaaab"));
        assertFalse(util.matches("a*b+", "aaaa"));
    }
}
