package de.accso.flexinale.backoffice.api_contract.event;

import com.fasterxml.jackson.core.JsonProcessingException;
import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO;
import de.accso.flexinale.backoffice.api_contract.event.model.KinoSaalTO;
import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO;
import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO;
import de.accso.flexinale.common.api.event.Event;
import de.accso.flexinale.common.infrastructure.eventbus.EventSerializationHelper;
import static de.accso.flexinale.backoffice.api_contract.event.ReflectionHelper.setField;
import de.accso.flexinale.common.shared_kernel.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Stream;

import static de.accso.flexinale.common.shared_kernel.Identifiable.Id.uuidString;
import static org.assertj.core.api.Assertions.assertThat;

class EventSerializationTest {
    private static Stream<Arguments> allFilmEventTypes() {
        return Set.of(FilmCreatedEvent.class, FilmUpdatedEvent.class, FilmDeletedEvent.class).stream().map(Arguments::of);
    }

    private static Stream<Arguments> allKinoEventTypes() {
        return Set.of(KinoCreatedEvent.class, KinoUpdatedEvent.class, KinoDeletedEvent.class).stream().map(Arguments::of);
    }

    private static Stream<Arguments> allVorfuehrungEventTypes() {
        return Set.of(VorfuehrungCreatedEvent.class, VorfuehrungUpdatedEvent.class, VorfuehrungDeletedEvent.class).stream().map(Arguments::of);
    }

    @ParameterizedTest
    @MethodSource("allFilmEventTypes")
    <E extends Event> void testSerializeFilmEventClass2JsonString(final Class<E> filmEventType)
            throws JsonProcessingException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
        // arrange
        Constructor<E> constructor = filmEventType.getDeclaredConstructor();
        constructor.setAccessible(true);
        Event event = constructor.newInstance();

        FilmTO filmTO = createFilmTO();
        setField(filmEventType, event, "film", filmTO, true);

        // act - serialize to JSON
        String jsonString = EventSerializationHelper.serializeEvent2JsonString(event);

        // assert
        assertThat(jsonString).contains(getExpectedJsonStringForVersion(event.version()));
        assertThat(jsonString).contains(getExpectedJsonStringForRawTypeString("titel", filmTO.titel()));
        assertThat(jsonString).contains(getExpectedJsonStringForRawTypeString("imdbUrl", filmTO.imdbUrl()));
        assertThat(jsonString).contains(getExpectedJsonStringForRawTypeInteger("dauerInMinuten", filmTO.dauerInMinuten()));

        // act - deserialize back from JSON
        E deserializedEvent = EventSerializationHelper.deserializeJsonString2Event(jsonString, filmEventType);

        // assert
        assertThat(deserializedEvent).isInstanceOf(filmEventType);
        assertThat(deserializedEvent).isInstanceOf(FilmCRUDEvent.class);
        FilmCRUDEvent castedDeserializedEvent = (FilmCRUDEvent) deserializedEvent;
        assertThat(castedDeserializedEvent.version()).isEqualTo(event.version());
        assertThat(castedDeserializedEvent.film).isEqualTo(filmTO);
    }

    @ParameterizedTest
    @MethodSource("allKinoEventTypes")
    <E extends Event> void testSerializeKinoEventClass2JsonString(final Class<E> kinoEventType)
            throws JsonProcessingException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
        // arrange
        Constructor<E> constructor = kinoEventType.getDeclaredConstructor();
        constructor.setAccessible(true);
        Event event = constructor.newInstance();

        KinoTO kinoTO = createKinoTO();
        setField(kinoEventType, event, "kino", kinoTO, true);

        // act
        String jsonString = EventSerializationHelper.serializeEvent2JsonString(event);

        // assert
        assertThat(jsonString).contains(getExpectedJsonStringForVersion(event.version()));
        assertThat(jsonString).contains(getExpectedJsonStringForRawTypeString("name", kinoTO.name()));
        assertThat(jsonString).contains(getExpectedJsonStringForRawTypeString("adresse", kinoTO.adresse()));
        assertThat(jsonString).contains(getExpectedJsonStringForRawTypeString("emailAdresse", kinoTO.emailAdresse()));

        // act - deserialize back from JSON
        E deserializedEvent = EventSerializationHelper.deserializeJsonString2Event(jsonString, kinoEventType);

        // assert
        assertThat(deserializedEvent).isInstanceOf(kinoEventType);
        assertThat(deserializedEvent).isInstanceOf(KinoCRUDEvent.class);
        KinoCRUDEvent castedDeserializedEvent = (KinoCRUDEvent) deserializedEvent;
        assertThat(castedDeserializedEvent.version()).isEqualTo(event.version());
//TODO bidirectional relation K<->KS, assertion commented out, as does not work as the de/serialization is not working correctly for bidirectional relation between Kino and KinoSaal
        // assertThat(castedDeserializedEvent.kino).isEqualTo(kinoTO);
    }

    @ParameterizedTest
    @MethodSource("allVorfuehrungEventTypes")
    <E extends Event> void testSerializeVorfuehrungEventClass2JsonString(final Class<E> vorfuehrungEventType)
            throws JsonProcessingException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
        // arrange
        Constructor<E> constructor = vorfuehrungEventType.getDeclaredConstructor();
        constructor.setAccessible(true);
        Event event = constructor.newInstance();

        VorfuehrungTO vorfuehrungTO = createVorfuehrungTO();
        setField(vorfuehrungEventType, event, "vorfuehrung", vorfuehrungTO, true);

        // act
        String jsonString = EventSerializationHelper.serializeEvent2JsonString(event);

        // assert
        assertThat(jsonString).contains(getExpectedJsonStringForVersion(event.version()));
        assertThat(jsonString).contains(getExpectedJsonStringForRawTypeLocalDateTime("zeit", vorfuehrungTO.zeit()));

        // act - deserialize back from JSON
        E deserializedEvent = EventSerializationHelper.deserializeJsonString2Event(jsonString, vorfuehrungEventType);

        // assert
        assertThat(deserializedEvent).isInstanceOf(vorfuehrungEventType);
        assertThat(deserializedEvent).isInstanceOf(VorfuehrungCRUDEvent.class);
        VorfuehrungCRUDEvent castedDeserializedEvent = (VorfuehrungCRUDEvent) deserializedEvent;
        assertThat(castedDeserializedEvent.version()).isEqualTo(event.version());
        assertThat(castedDeserializedEvent.vorfuehrung).isEqualTo(vorfuehrungTO);
    }

    private static FilmTO createFilmTO() {
        FilmTO.Titel titel = new FilmTO.Titel(uuidString());
        FilmTO.ImdbUrl imdbUrl = new FilmTO.ImdbUrl(uuidString());
        FilmTO.DauerInMinuten dauerInMinuten = new FilmTO.DauerInMinuten(42);
        return new FilmTO(Identifiable.Id.of(), Versionable.unknownVersion(), titel, imdbUrl, dauerInMinuten);
    }

    private static KinoTO createKinoTO() {
        KinoTO.Name name = new KinoTO.Name(uuidString());
        KinoTO.Adresse adresse = new KinoTO.Adresse(uuidString());
        KinoTO.EmailAdresse emailAdresse = new KinoTO.EmailAdresse(uuidString());
        KinoTO kinoTO = new KinoTO(Identifiable.Id.of(), Versionable.unknownVersion(), name, adresse, emailAdresse, new HashSet<>());
        kinoTO.kinoSaele().add(createKinoSaalTO(kinoTO));
        return kinoTO;
    }

    private static KinoSaalTO createKinoSaalTO(KinoTO kinoTO) {
        KinoSaalTO.Name name = new KinoSaalTO.Name(uuidString());
        KinoSaalTO.AnzahlPlaetze adresse = new KinoSaalTO.AnzahlPlaetze(42);
        return new KinoSaalTO(Identifiable.Id.of(), Versionable.unknownVersion(), name, adresse, kinoTO);
    }

    private static VorfuehrungTO createVorfuehrungTO() {
        VorfuehrungTO.Zeit zeit = new VorfuehrungTO.Zeit(LocalDateTime.now().withSecond(0)); // intentionally as ISO format changes then!
        KinoSaalTO kinoSaalTO = createKinoSaalTO(null);
        return new VorfuehrungTO(Identifiable.Id.of(), Versionable.unknownVersion(), zeit,
                createFilmTO(), kinoSaalTO, Identifiable.Id.of());
    }

    private static String getExpectedJsonStringForVersion(Versionable.Version field) {
        return String.format("\"%s\":{\"version\":%d", "version", field.version());
    }
    private static String getExpectedJsonStringForRawTypeString(String fieldName, RawWrapper<String> field) {
        return String.format("\"%s\":{\"raw\":\"%s\"", fieldName, field.raw());
    }
    private static String getExpectedJsonStringForRawTypeInteger(String fieldName, RawWrapper<Integer> field) {
        return String.format("\"%s\":{\"raw\":%d", fieldName, field.raw());
    }
    private static String getExpectedJsonStringForRawTypeLocalDateTime(String fieldName, RawWrapper<LocalDateTime> field) {
        return String.format("\"%s\":{\"raw\":\"%s\"", fieldName, field.raw().format(DateTimeFormatter.ISO_DATE_TIME));
    }
}