package fr.umlv.javainside;

import fr.umlv.javainside.DictionaryDecoder.Data;
import fr.umlv.javainside.DictionaryDecoder.Dictionary;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Objects;
import java.util.Set;

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

public class DictionaryDecoderTest {

  /*@Test @Tag("Q0")
  public void parseLine() {
    var dictionary = Dictionary.parseLine("dog: true, chicken: false, cat: 12.0, lion: 54, goldfish: hello");
    assertEquals(
        List.of(new Data("dog", true), new Data("chicken", false), new Data("cat", 12.0), new Data("lion", 54), new Data("goldfish", "hello")),
        dictionary.dataList());
  }*/

  @SuppressWarnings("unused")
  static final class Person {
    private String name;

    public Person() {
      this(null);
    }

    public Person(String name) {
      this.name = name;
    }

    public String getName() {
      return name;
    }

    public void setName(String name) {
      this.name = name;
    }

    @Override
    public boolean equals(Object o) {
      return o instanceof Person person && Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
      return Objects.hashCode(name);
    }

    @Override
    public String toString() {
      return "Person(" + name + ')';
    }
  }

  @Nested
  class Q1 {

    @Test @Tag("Q1")
    public void findKeysOneKey() {
      var keys = DictionaryDecoder.findKeys(Dictionary.parseLine("name: Bob"));
      assertEquals(Set.of("name"), keys);
    }

    @Test @Tag("Q1")
    public void findKeysTwoKeys() {
      var keys = DictionaryDecoder.findKeys(Dictionary.parseLine("name: Ana, age: 45"));
      assertEquals(Set.of("name", "age"), keys);
    }

    @Test @Tag("Q1")
    public void findKeysThreeKeys() {
      var keys = DictionaryDecoder.findKeys(Dictionary.parseLine("name: Ana, age: 45, manager: true"));
      assertEquals(Set.of("name", "age", "manager"), keys);
    }

    @Test @Tag("Q1")
    public void findKeysZeroKeys() {
      var keys = DictionaryDecoder.findKeys(Dictionary.parseLine(""));
      assertEquals(Set.of(), keys);
    }

    @Test @Tag("Q1")
    public void canDecodeOnePerson() {
      DictionaryDecoder decoder = new DictionaryDecoder();
      decoder.register(Person.class);
      Dictionary dictionary = Dictionary.parseLine("name: Bob");
      var keys = DictionaryDecoder.findKeys(dictionary);
      assertTrue(decoder.canDecode(keys));
    }

    @Test @Tag("Q1")
    public void canNotDecode() {
      DictionaryDecoder decoder = new DictionaryDecoder();
      decoder.register(Person.class);
      Dictionary dictionary = Dictionary.parseLine("name: Bob, age: 33");
      var keys = DictionaryDecoder.findKeys(dictionary);
      assertFalse(decoder.canDecode(keys));
    }

    @Test @Tag("Q1")
    public void canNotDecode2() {
      DictionaryDecoder decoder = new DictionaryDecoder();
      decoder.register(Person.class);
      Dictionary dictionary = Dictionary.parseLine("");
      var keys = DictionaryDecoder.findKeys(dictionary);
      assertFalse(decoder.canDecode(keys));
    }
  }

  @SuppressWarnings("unused")
  static final class Shelter {
    private int cat;
    private int dog;
    private int lion;

    public Shelter() {
      this(0, 0, 0);
    }

    public Shelter(int cat, int dog, int lion) {
      this.cat = cat;
      this.dog = dog;
      this.lion = lion;
    }

    public int getCat() {
      return cat;
    }
    public void setCat(int cat) {
      this.cat = cat;
    }
    public int getDog() {
      return dog;
    }
    public void setDog(int dog) {
      this.dog = dog;
    }
    public int getLion() {
      return lion;
    }
    public void setLion(int lion) {
      this.lion = lion;
    }

    @Override
    public boolean equals(Object obj) {
      return obj instanceof Shelter shelter &&
          cat == shelter.cat &&
          dog == shelter.dog &&
          lion == shelter.lion;
    }
    @Override
    public int hashCode() {
      return Objects.hash(cat, dog, lion);
    }
    @Override
    public String toString() {
      return "Shelter { cat: " + cat + ", dog: " + dog + ", lion: " + lion + '}';
    }
  }

  @Nested
  class Q2 {
    public static class Empty {}

    @SuppressWarnings("unused")
    public static class Author {
      private String name;

      public String getName() {
        return name;
      }
      public void setName(String name) {
        this.name = name;
      }
    }

    @SuppressWarnings("unused")
    public static class NoSetter {
      private String entity;

      public String getEntity() {
        return entity;
      }
    }

    @SuppressWarnings("unused")
    public static class NoGetter {
      public void setEntity(String entity) {
      }
    }

    @Test
    @Tag("Q2")
    public void registeredClassCanNotBeEmpty() {
      var decoder = new DictionaryDecoder();
      assertThrows(IllegalArgumentException.class, () -> decoder.register(Empty.class));
    }

    @Test
    @Tag("Q2")
    public void registeredClassWithSameKeys() {
      var decoder = new DictionaryDecoder();
      decoder.register(Person.class);
      assertThrows(IllegalStateException.class, () -> decoder.register(Author.class));
    }

    @Test
    @Tag("Q2")
    public void registeredClassTwice() {
      var decoder = new DictionaryDecoder();
      decoder.register(Person.class);
      assertThrows(IllegalStateException.class, () -> decoder.register(Person.class));
    }

    @Test
    @Tag("Q2")
    public void registeredClassWithNoGetter() {
      var decoder = new DictionaryDecoder();
      assertThrows(IllegalArgumentException.class, () -> decoder.register(NoSetter.class));
    }

    @Test
    @Tag("Q2")
    public void registeredClassWithNoSetter() {
      var decoder = new DictionaryDecoder();
      assertThrows(IllegalArgumentException.class, () -> decoder.register(NoSetter.class));
    }

    @Test
    @Tag("Q2")
    public void registeredClassNull() {
      var decoder = new DictionaryDecoder();
      assertThrows(NullPointerException.class, () -> decoder.register(null));
    }
  }



  @Nested
  class Q3 {
    @SuppressWarnings("unused")
    static final class Point {
      private int x;
      private int y;

      public Point() {
        this(0, 0);
      }
      public Point(int x, int y) {
        this.x = x;
        this.y = y;
      }

      public int getX() {
        return x;
      }
      public void setX(int x) {
        this.x = x;
      }
      public int getY() {
        return y;
      }
      public void setY(int y) {
        this.y = y;
      }

      @Override
      public boolean equals(Object o) {
        return o instanceof Point point && x == point.x && y == point.y;
      }
      @Override
      public int hashCode() {
        return Integer.hashCode(x) ^ Integer.hashCode(y);
      }
      @Override
      public String toString() {
        return "Point(" + x + ", " + y + ')';
      }
    }

    sealed interface Party {
      @SuppressWarnings("unused")
      final class User implements Party {
        private int id;

        public User() {
          this(0);
        }
        public User(int id) {
          this.id = id;
        }

        public int getId() {
          return id;
        }
        public void setId(int id) {
          this.id = id;
        }

        @Override
        public boolean equals(Object o) {
          return o instanceof User user && id == user.id;
        }
        @Override
        public int hashCode() {
          return id;
        }
      }

      @SuppressWarnings("unused")
      final class Company implements Party {
        private int id;
        private int employeeCount;

        public Company() {
          this(0, 0);
        }
        public Company(int id, int employeeCount) {
          this.id = id;
          this.employeeCount = employeeCount;
        }

        public int getId() {
          return id;
        }
        public void setId(int id) {
          this.id = id;
        }
        public int getEmployeeCount() {
          return employeeCount;
        }
        public void setEmployeeCount(int employeeCount) {
          this.employeeCount = employeeCount;
        }

        @Override
        public boolean equals(Object o) {
          return o instanceof Company company && id == company.id && employeeCount == company.employeeCount;
        }
        @Override
        public int hashCode() {
          return id ^ employeeCount;
        }
      }
    }

    @Test @Tag("Q3")
    public void decodeWithClassOnePerson() {
      DictionaryDecoder decoder = new DictionaryDecoder();
      decoder.register(Person.class);
      Dictionary dictionary = Dictionary.parseLine("name: Bob");
      Person person = (Person) decoder.decode(dictionary);
      assertEquals(new Person("Bob"), person);
    }

    @Test @Tag("Q3")
    public void decodeWithClassManyPersons() {
      var decoder = new DictionaryDecoder();
      decoder.register(Person.class);
      var persons = """
          name: Bob
          name: Ana
          name: Peach
          """.lines().map(Dictionary::parseLine)
          .map(dictionary -> (Person) decoder.decode(dictionary))
          .toList();
      assertEquals(List.of(new Person("Bob"), new Person("Ana"), new Person("Peach")), persons);
    }

    @Test @Tag("Q3")
    public void decodeWithClassPersonNull() {
      var decoder = new DictionaryDecoder();
      decoder.register(Person.class);
      assertThrows(NullPointerException.class, () -> decoder.decode(null));
    }

    @Test @Tag("Q3")
    public void decodeWithoutAClassShelter() {
      var decoder = new DictionaryDecoder();
      decoder.register(Shelter.class);
      var shelters = """
            cat: 12, dog: 8, lion: 54
            dog: 8, lion: 54, cat: 12
            lion: 54, cat: 12, dog: 8
            """.lines().map(Dictionary::parseLine).toList();
      for (var dictionary : shelters) {
        assertEquals(new Shelter(12, 8, 54), decoder.decode(dictionary));
      }
    }

    @Test @Tag("Q3")
    public void decodeWithoutAClassPoint() {
      var decoder = new DictionaryDecoder();
      decoder.register(Point.class);
      var points = """
            x: 12, y: 67
            y: 23, x: 5
            """.lines().map(Dictionary::parseLine)
          .map(decoder::decode)
          .toList();
      assertEquals(List.of(new Point(12, 67), new Point(5, 23)), points);
    }

    @Test @Tag("Q3")
    public void decodeAHierarchy() {
      DictionaryDecoder decoder = new DictionaryDecoder();
      decoder.register(Party.User.class);
      decoder.register(Party.Company.class);
      List<Party> parties = """
          id: 12
          id: 14, employeeCount: 123
          id: 13
          """.lines().map(Dictionary::parseLine)
          .map(dictionary -> (Party) decoder.decode(dictionary))
          .toList();
      assertEquals(List.of(new Party.User(12), new Party.Company(14, 123), new Party.User(13)), parties);
    }

    @Test @Tag("Q3")
    public void decodeWithoutAClassNull() {
      var decoder = new DictionaryDecoder();
      decoder.register(Person.class);
      assertThrows(NullPointerException.class, () -> decoder.decode(null));
    }
  }



  @Nested
  class Q4 {

    @SuppressWarnings("unused")
    static final class Bus {
      private String name;

      public Bus() { this(null); }
      public Bus(String name) {
        this.name = name;
      }

      @fr.umlv.javainside.DictionaryDecoder.Property("driver")
      public String getName() {
        return name;
      }
      public void setName(String name) {
        this.name = name;
      }

      @Override
      public boolean equals(Object o) {
        return o instanceof Bus bus && Objects.equals(name, bus.name);
      }

      @Override
      public int hashCode() {
        return Objects.hashCode(name);
      }
    }

    @SuppressWarnings("unused")
    static final class SameAnnotationTwice {
      private String property1;
      private String property2;

      public SameAnnotationTwice() {}

      @fr.umlv.javainside.DictionaryDecoder.Property("same-property")
      public String getProperty1() {
        return property1;
      }
      public void setProperty1(String property1) {
        this.property1 = property1;
      }

      @fr.umlv.javainside.DictionaryDecoder.Property("same-property")
      public String getProperty2() {
        return property2;
      }
      public void setProperty2(String property2) {
        this.property2 = property2;
      }
    }

    @Test @Tag("Q4")
    public void decodeClassWithAnnotation() {
      var decoder = new DictionaryDecoder();
      decoder.register(Bus.class);
      var dictionary = Dictionary.parseLine("driver: Bob");
      var bus = decoder.decode(dictionary);
      assertEquals(new Bus("Bob"), bus);
    }

    @Test @Tag("Q4")
    public void decodeClassWithAnnotationSameTwice() {
      var decoder = new DictionaryDecoder();
      assertThrows(IllegalStateException.class, () -> decoder.register(SameAnnotationTwice.class));
    }

  }


  @Nested
  class Q5 {
    public record Person(String name) {}

    public record PetShop(int cat, int dog, int lion) {}

    public record Bus(@fr.umlv.javainside.DictionaryDecoder.Property("driver") String name) {}

    public record SameAnnotationTwice(
        @fr.umlv.javainside.DictionaryDecoder.Property("same-property") String property1,
        @fr.umlv.javainside.DictionaryDecoder.Property("same-property") String property2) {}

    @Test @Tag("Q5")
    public void decodeWithRecordPerson() {
      var decoder = new DictionaryDecoder();
      decoder.register(Person.class);
      var persons = """
          name: Bob
          name: Ana
          name: Peach
          """.lines().map(Dictionary::parseLine)
          .map(dataList -> (Person) decoder.decode(dataList))
          .toList();
      assertEquals(List.of(new Person("Bob"), new Person("Ana"), new Person("Peach")), persons);
    }

    @Test @Tag("Q5")
    public void decodeRecordPetShop() {
      var decoder = new DictionaryDecoder();
      decoder.register(PetShop.class);
      var petShops = """
            cat: 12, dog: 8, lion: 54
            dog: 8, lion: 54, cat: 12
            lion: 54, cat: 12, dog: 8
            """.lines().map(Dictionary::parseLine).toList();
      for (var dataList : petShops) {
        assertEquals(new PetShop(12, 8, 54), decoder.decode(dataList));
      }
    }

    @Test @Tag("Q5")
    public void decodeRecordWithAnnotation() {
      var decoder = new DictionaryDecoder();
      decoder.register(Bus.class);
      var dictionary = Dictionary.parseLine("driver: Bob");
      var bus = decoder.decode(dictionary);
      assertEquals(new Bus("Bob"), bus);
    }

    @Test @Tag("Q5")
    public void decodeRecordWithAnnotationSameTwice() {
      var decoder = new DictionaryDecoder();
      assertThrows(IllegalStateException.class, () -> decoder.register(SameAnnotationTwice.class));
    }

    @Test @Tag("Q5")
    public void registeredClassAndRecordWithSameKeys() {
      var decoder = new DictionaryDecoder();
      decoder.register(Shelter.class);
      assertThrows(IllegalStateException.class, () -> decoder.register(PetShop.class));
    }
  }
}
