package io.wealthwizards.appsynctesting.utils;

import io.wealthwizards.appsynctesting.utils.dynamodb.DynamoDB;
import org.junit.jupiter.api.*;

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

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

@DisplayName("$util.dynamodb")
public class DynamoDBTest {
    @Nested
    @DisplayName(".toDynamoDB")
    class ToDynamoDB {
        @Test
        @DisplayName("should convert String to DynamoDB AttributeValue")
        public void shouldConvertInputObjectToDynamoDB_S() {
            var target = new DynamoDB();

            var actual = target.toDynamoDB("foo");
            var expected = Map.of("S", "foo");

            assertEquals(expected, actual);
        }

        @Test
        @DisplayName("should convert Number to DynamoDB AttributeValue")
        public void shouldConvertInputObjectToDynamoDB_N() {
            var target = new DynamoDB();

            var actual = target.toDynamoDB(12345);
            var expected = Map.of("N", "12345");

            assertEquals(expected, actual);
        }    

        @Test
        @DisplayName("should convert Boolean to DynamoDB AttributeValue")
        public void shouldConvertInputObjectToDynamoDB_Bool() {
            var target = new DynamoDB();

            var actual = target.toDynamoDB(true);
            var expected = Map.of("BOOL", true);

            assertEquals(expected, actual);
        }

        @Test
        @DisplayName("should convert List to DynamoDB AttributeValue")
        public void shouldConvertInputObjectToDynamoDB_L() {
            var target = new DynamoDB();

            var actual = target.toDynamoDB(List.of("foo", 123, Map.of("bar", "baz")));
            var expected = Map.of(
                    "L", List.of(
                            Map.of("S", "foo"),
                            Map.of("N", "123"),
                            Map.of("M", Map.of(
                                    "bar", Map.of("S", "baz")
                            ))
                    )
            );

            assertEquals(expected, actual);
        }

        @Test
        @DisplayName("should convert Map to DynamoDB AttributeValue")
        public void shouldConvertInputObjectToDynamoDB_M() {
            var target = new DynamoDB();

            var actual = target.toDynamoDB(
                    Map.of(
                            "foo", "bar",
                            "baz", 1234,
                            "beep", List.of("boop")
                    )
            );
            var expected = Map.of(
                    "M", Map.of(
                            "foo", Map.of("S", "bar"),
                            "baz", Map.of("N", "1234"),
                            "beep", Map.of(
                                    "L", List.of(Map.of("S", "boop"))
                            )
                    )
            );

            assertEquals(expected, actual);
        }

        @Test
        @DisplayName("should convert NumberSet to DynamoDB list AttributeValue")
        public void shouldConvertInputObjectToDynamoDB_NS() {
            var target = new DynamoDB();

            var numbers = new LinkedHashSet<Number>();
            numbers.add(1);
            numbers.add(23);
            numbers.add(4.56);

            var actual = target.toDynamoDB(numbers);
            var expected = Map.of(
                    "L", List.of(
                            Map.of("N", "1"),
                            Map.of("N", "23"),
                            Map.of("N", "4.56")
                    )
            );

            assertEquals(expected, actual);
        }

        @Test
        @DisplayName("should convert StringSet to DynamoDB list AttributeValue")
        public void shouldConvertInputObjectToDynamoDB_SS() {
            var target = new DynamoDB();

            var strings = new LinkedHashSet<String>();
            strings.add("foo");
            strings.add("bar");
            strings.add("baz");

            var actual = target.toDynamoDB(strings);
            var expected = Map.of(
                    "L", List.of(
                            Map.of("S", "foo"),
                            Map.of("S", "bar"),
                            Map.of("S", "baz")
                    )
            );

            assertEquals(expected, actual);
        }
    }

    @Nested
    @DisplayName(".toDynamoDBJson")
    class ToDynamoDBJson {
        @Test
        @DisplayName("should convert String to DynamoDB JSON encoded string")
        public void shouldConvertInputObjectToDynamoDBJson_S() {
            var target = new DynamoDB();

            String actual = target.toDynamoDBJson("foo");
            String expected = "{\"S\":\"foo\"}";

            assertEquals(expected, actual);
        }

        @Test
        @DisplayName("should convert Number to DynamoDB JSON encoded string")
        public void shouldConvertInputObjectToDynamoDBJson_N() {
            var target = new DynamoDB();

            String actual = target.toDynamoDBJson(12345);
            String expected = "{\"N\":\"12345\"}";

            assertEquals(expected, actual);
        }    

        @Test
        @DisplayName("should convert Boolean to DynamoDB JSON encoded string")
        public void shouldConvertInputObjectToDynamoDBJson_Bool() {
            var target = new DynamoDB();

            String actual = target.toDynamoDBJson(true);
            String expected = "{\"BOOL\":true}";

            assertEquals(expected, actual);
        }

        @Test
        @DisplayName("should convert List to DynamoDB JSON encoded string")
        public void shouldConvertInputObjectToDynamoDBJson_L() {
            var target = new DynamoDB();

            String actual = target.toDynamoDBJson(List.of("foo", 123, Map.of("bar", "baz")));
            String expected = "{\"L\":[{\"S\":\"foo\"},{\"N\":\"123\"},{\"M\":{\"bar\":{\"S\":\"baz\"}}}]}";

            assertEquals(expected, actual);
        }

        @Test
        @DisplayName("should convert Map to DynamoDB JSON encoded string")
        public void shouldConvertInputObjectToDynamoDBJson_M() {
            var target = new DynamoDB();

            String actual = target.toDynamoDBJson(Map.of("foo", "bar", "baz", 1234, "beep", List.of("boop")));
            String expected = "{\"M\":{\"beep\":{\"L\":[{\"S\":\"boop\"}]},\"baz\":{\"N\":\"1234\"},\"foo\":{\"S\":\"bar\"}}}";

            assertEquals(expected, actual);
        }

        @Test
        @DisplayName("should convert NumberSet to DynamoDB JSON encoded list string")
        public void shouldConvertInputObjectToDynamoDBJson_NS() {
            var target = new DynamoDB();

            var numbers = new LinkedHashSet<Number>();
            numbers.add(1);
            numbers.add(23);
            numbers.add(4.56);

            String actual = target.toDynamoDBJson(numbers);
            String expected = "{\"L\":[{\"N\":\"1\"},{\"N\":\"23\"},{\"N\":\"4.56\"}]}";

            assertEquals(expected, actual);
        }

        @Test
        @DisplayName("should convert StringSet to DynamoDB JSON encoded list string")
        public void shouldConvertInputObjectToDynamoDBJson_SS() {
            var target = new DynamoDB();

            var strings = new LinkedHashSet<String>();
            strings.add("foo");
            strings.add("bar");
            strings.add("baz");

            String actual = target.toDynamoDBJson(strings);
            String expected = "{\"L\":[{\"S\":\"foo\"},{\"S\":\"bar\"},{\"S\":\"baz\"}]}";

            assertEquals(expected, actual);
        }

        @Test
        @DisplayName("should convert null to DynamoDB JSON encoded string")
        public void shouldConvertNullToDynamoDBJson() {
            var target = new DynamoDB();

            String actual = target.toDynamoDBJson(null);
            String expected = "{\"NULL\":null}";

            assertEquals(expected, actual);
        }
    }

    @Nested
    @DisplayName(".toMap")
    class ToMap {
        @Test
        @DisplayName("should convert Map to DynamoDB AttributeValue")
        public void shouldConvertInputObjectToDynamoDB_M() {
            var target = new DynamoDB();

            var actual = target.toMap(
                    Map.of(
                            "foo", "bar",
                            "baz", 1234,
                            "beep", List.of("boop")
                    )
            );
            var expected = Map.of(
                    "M", Map.of(
                            "foo", Map.of("S", "bar"),
                            "baz", Map.of("N", "1234"),
                            "beep", Map.of(
                                    "L", List.of(
                                            Map.of("S", "boop")
                                    )
                            )
                    )
            );

            assertEquals(expected, actual);
        }
    }

    @Nested
    @DisplayName(".toMapJson")
    class ToMapJson {
        @Test
        @DisplayName("should convert Map to DynamoDB JSON encoded string")
        public void shouldConvertInputObjectToDynamoDBJson_M() {
            var target = new DynamoDB();

            String actual = target.toMapJson(Map.of("foo", "bar", "baz", 1234, "beep", List.of("boop")));
            String expected = "{\"M\":{\"beep\":{\"L\":[{\"S\":\"boop\"}]},\"baz\":{\"N\":\"1234\"},\"foo\":{\"S\":\"bar\"}}}";

            assertEquals(expected, actual);
        }
    }

    @Nested
    @DisplayName(".toMapValues")
    class ToMapValues {
        @Test
        @DisplayName("should create a copy of the Map where each value has been converted to its appropriate DynamoDB format")
        public void shouldConvertInputObjectToDynamoDB_M() {
            var target = new DynamoDB();

            var actual = target.toMapValues(Map.of("foo", "bar", "baz", 1234, "beep", List.of("boop")));
            var expected = Map.of(
                    "foo", Map.of("S", "bar"),
                    "baz", Map.of("N", "1234"),
                    "beep", Map.of(
                            "L", List.of(
                                    Map.of("S", "boop")
                            )
                    )
            );

            assertEquals(expected, actual);
        }
    }

    @Nested
    @DisplayName(".toMapValuesJson")
    class ToMapValuesJson {
        @Test
        @DisplayName("should create a copy of the Map where each value has been converted to its appropriate DynamoDB JSON encoded string")
        public void shouldConvertInputObjectToDynamoDBJson_M() {
            var target = new DynamoDB();

            String actual = target.toMapValuesJson(Map.of("foo", "bar", "baz", 1234, "beep", List.of("boop")));
            String expected = "{\"beep\":{\"L\":[{\"S\":\"boop\"}]},\"baz\":{\"N\":\"1234\"},\"foo\":{\"S\":\"bar\"}}";

            assertEquals(expected, actual);
        }
    }

    @Test
    @DisplayName(".toString")
    void shouldConvertToStringAttributeValue() {
        var dynamoDB = new DynamoDB();

        var actual = dynamoDB.toString("Hello");
        var expected = Map.of("S", "Hello");

        assertEquals(expected, actual);
    }

    @Test
    @DisplayName(".toStringJson")
    void shouldConvertToStringAttributeValueJson() {
        var dynamoDB = new DynamoDB();

        var actual = dynamoDB.toStringJson("Hello");
        var expected = "{\"S\":\"Hello\"}";

        assertEquals(expected, actual);
    }

    @Test
    @DisplayName(".toStringSet")
    void shouldConvertToStringSetAttributeValue() {
        var dynamoDB = new DynamoDB();

        var actual = dynamoDB.toStringSet(List.of("hello", "world"));
        var expected = Map.of("SS", List.of("hello", "world"));

        assertEquals(expected, actual);
    }

    @Test
    @DisplayName(".toStringSetJson")
    void shouldConvertToStringSetAttributeValueJson() {
        var dynamoDB = new DynamoDB();

        var actual = dynamoDB.toStringSetJson(List.of("hello", "world"));
        var expected = "{\"SS\":[\"hello\",\"world\"]}";

        assertEquals(expected, actual);
    }

    @Test
    @DisplayName(".toNumber")
    void shouldConvertToNumberAttributeValue() {
        var dynamoDB = new DynamoDB();

        var actual = dynamoDB.toNumber(12f);
        var expected = Map.of("N", "12.0");

        assertEquals(expected, actual);
    }

    @Test
    @DisplayName(".toNumberJson")
    void shouldConvertToNumberAttributeValueJson() {
        var dynamoDB = new DynamoDB();

        var actual = dynamoDB.toNumberJson(12f);
        var expected = "{\"N\":\"12.0\"}";

        assertEquals(expected, actual);
    }

    @Test
    @DisplayName(".toNumberSet")
    void shouldConvertToNumberSetAttributeValue() {
        var dynamoDB = new DynamoDB();

        var actual = dynamoDB.toNumberSet(List.of(12f, 11, 10.1d));
        var expected = Map.of("NS", List.of("12.0", "11", "10.1"));

        assertEquals(expected, actual);
    }

    @Test
    @DisplayName(".toNumberSetJson")
    void shouldConvertToNumberSetAttributeValueJson() {
        var dynamoDB = new DynamoDB();

        var actual = dynamoDB.toNumberSetJson(List.of(12f, 11, 10.1d));
        var expected = "{\"NS\":[\"12.0\",\"11\",\"10.1\"]}";

        assertEquals(expected, actual);
    }

    @Test
    @DisplayName(".toBinary")
    void shouldConvertToBinaryAttributeValue() {
        var dynamoDB = new DynamoDB();

        var data = "blahblahblah";
        var actual = dynamoDB.toBinary(data);
        var expected = Map.of("B", data);

        assertEquals(expected, actual);
    }

    @Test
    @DisplayName(".toBinaryJson")
    void shouldConvertToBinaryAttributeValueJson() {
        var dynamoDB = new DynamoDB();

        var actual = dynamoDB.toBinaryJson("blahblahblah");
        var expected = "{\"B\":\"blahblahblah\"}";

        assertEquals(expected, actual);
    }

    @Test
    @DisplayName(".toBinarySet")
    void shouldConvertToBinarySetAttributeValue() {
        var dynamoDB = new DynamoDB();

        var list = List.of("blahblahblah", "another one");
        var actual = dynamoDB.toBinarySet(list);
        var expected = Map.of("BS", list);

        assertEquals(expected, actual);
    }

    @Test
    @DisplayName(".toBinarySetJson")
    void shouldConvertToBinarySetAttributeValueJson() {
        var dynamoDB = new DynamoDB();

        var actual = dynamoDB.toBinarySetJson(List.of("blahblahblah", "another one"));
        var expected = "{\"BS\":[\"blahblahblah\",\"another one\"]}";

        assertEquals(expected, actual);
    }

    @Test
    @DisplayName(".toBoolean")
    void shouldConvertToBooleanAttributeValue() {
        var dynamoDB = new DynamoDB();

        var actual = dynamoDB.toBoolean(true);
        var expected = Map.of("BOOL", true);

        assertEquals(expected, actual);
    }

    @Test
    @DisplayName(".toBooleanJson")
    void shouldConvertToBooleanAttributeValueJson() {
        var dynamoDB = new DynamoDB();

        var actual = dynamoDB.toBooleanJson(false);
        var expected = "{\"BOOL\":false}";

        assertEquals(expected, actual);
    }

    @Test
    @DisplayName(".toNull")
    void shouldConvertToNullAttributeValue() {
        var dynamoDB = new DynamoDB();

        var actual = dynamoDB.toNull();
        var expected = new HashMap<>();
        expected.put("NULL", null);

        assertEquals(expected, actual);
    }

    @Test
    @DisplayName(".toNullJson")
    void shouldConvertToNullAttributeValueJson() {
        var dynamoDB = new DynamoDB();

        var actual = dynamoDB.toNullJson();
        var expected = "{\"NULL\":null}";

        assertEquals(expected, actual);
    }

    @Test
    @DisplayName(".toList")
    void shouldConvertToListAttributeValue() {
        var dynamoDB = new DynamoDB();

        var list = List.of("abc", 11, Map.of("abc", "def"));

        var actual = dynamoDB.toList(list);
        var expected = Map.of(
                "L", List.of(
                        Map.of("S", "abc"),
                        Map.of("N", "11"),
                        Map.of("M", Map.of(
                                "abc", Map.of(
                                        "S", "def"
                                )
                        ))
                )
        );

        assertEquals(expected, actual);
    }

    @Test
    @DisplayName(".toListJson")
    void shouldConvertToListAttributeValueJson() {
        var dynamoDB = new DynamoDB();

        var list = List.of("abc", 11, Map.of("abc", "def"));

        var actual = dynamoDB.toListJson(list);
        var expected = "{\"L\":[{\"S\":\"abc\"},{\"N\":\"11\"},{\"M\":{\"abc\":{\"S\":\"def\"}}}]}";

        assertEquals(expected, actual);
    }

    @Nested
    @DisplayName(".toS3ObjectJson")
    class ToS3ObjectJson {
        @Test
        @DisplayName("with key, bucket, and region")
        void shouldConvertToS3ObjectAttributeValueJson() {
            var dynamoDB = new DynamoDB();

            var actual = dynamoDB.toS3ObjectJson("foo.jpg", "bar-bucket", "baz-region");
            var expected = "{\"S\":\"{\\\"s3\\\":{\\\"bucket\\\":\\\"bar-bucket\\\",\\\"region\\\":\\\"baz-region\\\",\\\"key\\\":\\\"foo.jpg\\\"}}\"}";

            assertEquals(expected, actual);
        }

        @Test
        @DisplayName("with key, bucket, region, and version")
        void shouldConvertToS3ObjectWithVersionAttributeValueJson() {
            var dynamoDB = new DynamoDB();

            var actual = dynamoDB.toS3ObjectJson("foo.jpg", "bar-bucket", "baz-region", "v1");
            var expected = "{\"S\":\"{\\\"s3\\\":{\\\"bucket\\\":\\\"bar-bucket\\\",\\\"region\\\":\\\"baz-region\\\",\\\"version\\\":\\\"v1\\\",\\\"key\\\":\\\"foo.jpg\\\"}}\"}";

            assertEquals(expected, actual);
        }
    }

    @Nested
    @DisplayName(".toS3Object")
    class ToS3Object {
        @Test
        @DisplayName("with key, bucket, and region")
        void shouldConvertToS3ObjectAttributeValue() {
            var dynamoDB = new DynamoDB();

            var actual = dynamoDB.toS3Object("foo.jpg", "bar-bucket", "baz-region");
            var expected = Map.of(
                    "S", "{\"s3\":{\"bucket\":\"bar-bucket\",\"region\":\"baz-region\",\"key\":\"foo.jpg\"}}"
            );

            assertEquals(expected, actual);
        }

        @Test
        @DisplayName("with key, bucket, region, and version")
        void shouldConvertToS3ObjectWithVersionAttributeValue() {
            var dynamoDB = new DynamoDB();

            var actual = dynamoDB.toS3Object("foo.jpg", "bar-bucket", "baz-region", "v1");
            var expected = Map.of(
                    "S", "{\"s3\":{\"bucket\":\"bar-bucket\",\"region\":\"baz-region\",\"version\":\"v1\",\"key\":\"foo.jpg\"}}"
            );

            assertEquals(expected, actual);
        }
    }

    @Nested
    @DisplayName(".fromS3ObjectJson")
    class FromS3ObjectJson {
        @Test
        @DisplayName("with key, bucket, and region")
        void shouldConvertFromS3ObjectAttributeValueJson() {
            var dynamoDB = new DynamoDB();

            var json = "{\"S\":\"{\\\"s3\\\":{\\\"bucket\\\":\\\"bar-bucket\\\",\\\"region\\\":\\\"baz-region\\\",\\\"key\\\":\\\"foo.jpg\\\"}}\"}";

            var actual = dynamoDB.fromS3ObjectJson(json);
            var expected = Map.of(
                    "bucket", "bar-bucket",
                    "region", "baz-region",
                    "key", "foo.jpg"
            );

            assertEquals(expected, actual);
        }

        @Test
        @DisplayName("with key, bucket, region, and version")
        void shouldConvertFromS3ObjectWithVersionAttributeValueJson() {
            var dynamoDB = new DynamoDB();

            var json = "{\"S\":\"{\\\"s3\\\":{\\\"bucket\\\":\\\"bar-bucket\\\",\\\"region\\\":\\\"baz-region\\\",\\\"version\\\":\\\"v1\\\",\\\"key\\\":\\\"foo.jpg\\\"}}\"}";

            var actual = dynamoDB.fromS3ObjectJson(json);
            var expected = Map.of(
                    "bucket", "bar-bucket",
                    "region", "baz-region",
                    "key", "foo.jpg",
                    "version", "v1"
            );

            assertEquals(expected, actual);
        }
    }
}
