package de.accso.flexinale.backoffice.api_in.rest;

import de.accso.flexinale.FlexinaleDistributedApplicationTestIntegrated;
import de.accso.flexinale.backoffice.api_contract.event.*;
import de.accso.flexinale.backoffice.domain.model.KinoSaal;
import de.accso.flexinale.besucherportal.api_in.event.FilmSubscriber;
import de.accso.flexinale.besucherportal.api_in.event.KinoSubscriber;
import de.accso.flexinale.besucherportal.api_in.event.KontingentSubscriber;
import de.accso.flexinale.besucherportal.api_in.event.VorfuehrungSubscriber;
import de.accso.flexinale.common.api.eventbus.EventBusFactory;
import de.accso.flexinale.common.infrastructure.eventbus.InMemorySyncEventBusSpy;
import de.accso.flexinale.common.shared_kernel.DoNotCheckInArchitectureTests;
import de.accso.flexinale.common.shared_kernel.Identifiable;
import de.accso.flexinale.ticketing.api_contract.event.OnlineKontingentChangedEvent;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(classes = {FlexinaleDistributedApplicationTestIntegrated.class})
@ActiveProfiles("test-integrated")
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
@DoNotCheckInArchitectureTests
public class End2EndWithIntegratedInMemoryEventBusTest {
    private static final Class<End2EndWithIntegratedInMemoryEventBusTest> CLAZZ =
            End2EndWithIntegratedInMemoryEventBusTest.class;

    private static final Logger LOGGER = LoggerFactory.getLogger(CLAZZ);

    private static final String xlsDataFileForNewData =
            "/de/accso/flexinale/backoffice/api_in/rest/NewDataForRestCallIntegrationTest.xlsx";
    private static final String xlsDataFileForUpdatedData =
            "/de/accso/flexinale/backoffice/api_in/rest/UpdatedDataForRestCallIntegrationTest.xlsx";
    private static InputStream xlsDataFileForNewDataIS() {
        return Objects.requireNonNull(CLAZZ.getResourceAsStream(xlsDataFileForNewData));
    }
    private static InputStream xlsDataFileForUpdatedDataIS() {
        return Objects.requireNonNull(CLAZZ.getResourceAsStream(xlsDataFileForUpdatedData));
    }

    @Autowired
    FilmRestController filmRestController;

    @Autowired
    KinoKinoSaalRestController kinoKinoSaalRestController;

    @Autowired
    VorfuehrungRestController vorfuehrungRestController;

    @Autowired
    FilmSubscriber filmSubscriber;

    @Autowired
    KinoSubscriber kinoSubscriber;

    @Autowired
    VorfuehrungSubscriber vorfuehrungSubscriber;

    @Autowired
    KontingentSubscriber kontingentSubscriber;

    @Autowired
    EventBusFactory eventBusFactory;

    @Test
    void testUploadNewAndUpdatedDataAndCheckForPublishedEventsOnTheEventBus() throws IOException {
        // All data in the database is automatically deleted because of the property
        //     spring.jpa.hibernate.ddl-auto=create
        // in the test application.properties

        // Loading: order is important because of foreign key relations

        // ---------------------------------------------------------------------------------------------------------

        // Add new data

        // Filme
        try (BufferedInputStream stream = new BufferedInputStream(xlsDataFileForNewDataIS())) {
            testUploadNewDataViaRestAndCheckPersistenceAndPublishedEvents4Filme(stream);
        }

        // Kino and KinoSaele
        Collection<KinoSaal> newKinoSaele;
        try (BufferedInputStream stream = new BufferedInputStream(xlsDataFileForNewDataIS())) {
            newKinoSaele = kinoKinoSaalRestController.loadKinoSaele(stream);
        }
        try (BufferedInputStream stream = new BufferedInputStream(xlsDataFileForNewDataIS())) {
            testUploadNewDataViaRestAndCheckPersistenceAndPublishedEvents4Kinos(stream, newKinoSaele);
        }

        // Vorfuehrungen
        try (BufferedInputStream stream = new BufferedInputStream(xlsDataFileForNewDataIS())) {
            testUploadNewDataViaRestAndCheckPersistenceAndPublishedEvents4VorfuehrungenAndKontingente(stream);
        }

        // ---------------------------------------------------------------------------------------------------------

        // Update existing data - Filme and Kinos and implicitely also VorfuehrungUpdated

        // Filme
        try (BufferedInputStream stream = new BufferedInputStream(xlsDataFileForUpdatedDataIS())) {
            testUploadUpdatedDataViaRestAndCheckPersistenceAndPublishedEvents4Filme(stream);
        }

        // Kino and KinoSaele
        Collection<KinoSaal> updatedKinoSaele;
        try (BufferedInputStream stream = new BufferedInputStream(xlsDataFileForUpdatedDataIS())) {
            updatedKinoSaele = kinoKinoSaalRestController.loadKinoSaele(stream);
        }
        try (BufferedInputStream stream = new BufferedInputStream(xlsDataFileForUpdatedDataIS())) {
            testUploadUpdatedDataViaRestAndCheckPersistenceAndPublishedEvents4Kinos(stream, updatedKinoSaele);
        }

        // Vorfuehrungen
        //    do not test updates explicitely but VorfuehrungUpdated are created implicitely
        //    when ever a Vorfuehrung's Film or KinoSaal has changed
    }

    void testUploadNewDataViaRestAndCheckPersistenceAndPublishedEvents4Filme(
            final BufferedInputStream stream) throws IOException {
        Set<String> filmIds = Set.of("F00", "F01", "F02", "F03");

        // arrange
        InMemorySyncEventBusSpy<FilmCreatedEvent> spy = (InMemorySyncEventBusSpy<FilmCreatedEvent>)
                eventBusFactory.createOrGetEventBusFor(FilmCreatedEvent.class);
        spy.clear();

        // act
        ResponseEntity<RestResponseMessage> result =
                filmRestController.loadAndPersistAndPublishNewFilme(stream, xlsDataFileForNewData);

        // assert
        assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);

        assertThat(spy.size()).isEqualTo(4);
        assertThat(spy.allEvents().stream()
                .map(event -> event.film.id().id())
                .collect(Collectors.toSet()))
                .isEqualTo(filmIds);

        for (String id: filmIds) {
            Identifiable.Id filmId = Identifiable.Id.of(id);
            assertThat(filmSubscriber.getCache().film(filmId)).isNotNull();
        }
    }

    void testUploadNewDataViaRestAndCheckPersistenceAndPublishedEvents4Kinos(
            final BufferedInputStream stream,
            final Collection<KinoSaal> kinoSaele) throws IOException {
        Set<String> kinoIds = Set.of("K00", "K01", "K02", "K03");

        // arrange
        InMemorySyncEventBusSpy<KinoCreatedEvent> spy = (InMemorySyncEventBusSpy<KinoCreatedEvent>)
                eventBusFactory.createOrGetEventBusFor(KinoCreatedEvent.class);
        spy.clear();

        // act
        ResponseEntity<RestResponseMessage> result =
                kinoKinoSaalRestController.loadAndPersistAndPublishNewKinosKinoSaele(stream, xlsDataFileForNewData, kinoSaele);

        // assert
        assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);

        assertThat(spy.size()).isEqualTo(4);
        assertThat(spy.allEvents().stream()
                .map(event -> event.kino.id().id())
                .collect(Collectors.toSet()))
                .isEqualTo(kinoIds);

        for (String id: kinoIds) {
            Identifiable.Id kinoId = Identifiable.Id.of(id);

            assertThat(kinoSubscriber.getCache().kino(kinoId)).isNotNull();
            assertThat(kinoSubscriber.getCache().kino(kinoId).kinoSaele()).isNotNull();
            assertThat(kinoSubscriber.getCache().kino(kinoId).kinoSaele()).isNotEmpty();
            assertThat(kinoSubscriber.getCache().kino(kinoId).kinoSaele().size()).isGreaterThanOrEqualTo(1);
            kinoSubscriber.getCache().kino(kinoId).kinoSaele().forEach(kinoSaalTO -> {
                assertThat(kinoSaalTO.kino()).isNotNull();
                assertThat(kinoSaalTO.kino().id()).isEqualTo(kinoId);
            });
        }
    }

    void testUploadNewDataViaRestAndCheckPersistenceAndPublishedEvents4VorfuehrungenAndKontingente(
            final BufferedInputStream stream) throws IOException {
        Set<String> vorfuehrungIds = Set.of("V00", "V01", "V02", "V03", "V04",
                "V05", "V06", "V07", "V08");

        // arrange
        InMemorySyncEventBusSpy<VorfuehrungCreatedEvent> spy4VorfuehrungCreatedEvent =
                (InMemorySyncEventBusSpy<VorfuehrungCreatedEvent>) eventBusFactory.createOrGetEventBusFor(VorfuehrungCreatedEvent.class);
        spy4VorfuehrungCreatedEvent.clear();
        InMemorySyncEventBusSpy<OnlineKontingentChangedEvent> spy4OnlineKontingentChangedEvent =
                (InMemorySyncEventBusSpy<OnlineKontingentChangedEvent>) eventBusFactory.createOrGetEventBusFor(OnlineKontingentChangedEvent.class);
        spy4OnlineKontingentChangedEvent.clear();

        // act
        ResponseEntity<RestResponseMessage> result =
                vorfuehrungRestController.loadAndPersistAndPublishNewVorfuehrungen(stream, xlsDataFileForNewData);

        // assert
        assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);

        assertThat(spy4VorfuehrungCreatedEvent.size()).isEqualTo(9);
        assertThat(spy4VorfuehrungCreatedEvent.allEvents().stream()
                .map(event -> event.vorfuehrung.id().id())
                .collect(Collectors.toSet()))
                .isEqualTo(vorfuehrungIds);

        for (String vorfuehrungId: vorfuehrungIds) {
            assertThat(vorfuehrungSubscriber.getCache().vorfuehrung(Identifiable.Id.of(vorfuehrungId))).isNotNull();
        }

        // ---------------------------------------------------------------------------------------------------------

        // assert that Kontingente are created due to the uploaded Vorfuehrungen
        assertThat(spy4OnlineKontingentChangedEvent.size()).isEqualTo(9);
        assertThat(spy4OnlineKontingentChangedEvent.allEvents().stream()
                .map(event -> event.onlineKontingent.vorfuehrungId().id()).collect(Collectors.toSet()))
                .isEqualTo(vorfuehrungIds);

        for (String id: vorfuehrungIds) {
            Identifiable.Id vorfuehrungId = Identifiable.Id.of(id);

            assertThat(kontingentSubscriber.getCache().getRestkontingentOnline(vorfuehrungId)).isNotNull();
        }
    }

    // ---------------------------------------------------------------------------------------------------------

    void testUploadUpdatedDataViaRestAndCheckPersistenceAndPublishedEvents4Filme(
            final BufferedInputStream stream) throws IOException {
        Set<String> changedFilmIds = Set.of("F00", "F01");
        Set<String> vorfuehrungUpdatedIds = Set.of("V00", "V01", "V02", "V03");

        // arrange
        InMemorySyncEventBusSpy<FilmUpdatedEvent> spy4FilmUpdatedEvent = (InMemorySyncEventBusSpy<FilmUpdatedEvent>)
                eventBusFactory.createOrGetEventBusFor(FilmUpdatedEvent.class);
        spy4FilmUpdatedEvent.clear();
        InMemorySyncEventBusSpy<VorfuehrungUpdatedEvent> spy4VorfuehrungUpdatedEvent = (InMemorySyncEventBusSpy<VorfuehrungUpdatedEvent>)
                eventBusFactory.createOrGetEventBusFor(VorfuehrungUpdatedEvent.class);
        spy4VorfuehrungUpdatedEvent.clear();

        // act
        ResponseEntity<RestResponseMessage> result =
                filmRestController.loadAndPersistAndPublishNewFilme(stream, xlsDataFileForUpdatedData);

        // assert
        assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);

        assertThat(spy4FilmUpdatedEvent.size()).isEqualTo(2);
        assertThat(spy4FilmUpdatedEvent.allEvents().stream()
                .map(event -> event.film.id().id())
                .collect(Collectors.toSet()))
                .isEqualTo(changedFilmIds);

        for (String id: changedFilmIds) {
            Identifiable.Id filmId = Identifiable.Id.of(id);

            assertThat(filmSubscriber.getCache().film(filmId)).isNotNull();
            String titel = getRawOrNull(filmSubscriber.getCache().film(filmId).titel());
            assertThat(titel.endsWith("_changed")).isTrue();
        }

        // assert VorfuehrungUpdated events
        assertThat(spy4VorfuehrungUpdatedEvent.allEvents().stream()
                .map(event -> event.vorfuehrung.id().id())
                .collect(Collectors.toSet()))
                .isEqualTo(vorfuehrungUpdatedIds);
        assertThat(spy4VorfuehrungUpdatedEvent.allEvents().stream()
                .map(event -> event.vorfuehrung.film().id().id())
                .collect(Collectors.toSet()))
                .isEqualTo(changedFilmIds);
    }

    void testUploadUpdatedDataViaRestAndCheckPersistenceAndPublishedEvents4Kinos(
            final BufferedInputStream stream,
            final Collection<KinoSaal> kinoSaele) throws IOException {
        Set<String> changedKinoIds = Set.of("K00", "K01");
        Set<String> vorfuehrungUpdatedIds = Set.of("V00", "V01", "V02", "V03");

        // arrange
        InMemorySyncEventBusSpy<KinoUpdatedEvent> spy4KinoUpdatedEvent = (InMemorySyncEventBusSpy<KinoUpdatedEvent>)
                eventBusFactory.createOrGetEventBusFor(KinoUpdatedEvent.class);
        spy4KinoUpdatedEvent.clear();
        InMemorySyncEventBusSpy<VorfuehrungUpdatedEvent> spy4VorfuehrungUpdatedEvent = (InMemorySyncEventBusSpy<VorfuehrungUpdatedEvent>)
                eventBusFactory.createOrGetEventBusFor(VorfuehrungUpdatedEvent.class);
        spy4VorfuehrungUpdatedEvent.clear();

        // act
        ResponseEntity<RestResponseMessage> result =
                kinoKinoSaalRestController.loadAndPersistAndPublishNewKinosKinoSaele(stream, xlsDataFileForUpdatedData, kinoSaele);

        // assert
        assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);

        assertThat(spy4KinoUpdatedEvent.size()).isEqualTo(2);
        assertThat(spy4KinoUpdatedEvent.allEvents().stream()
                .map(event -> event.kino.id().id())
                .collect(Collectors.toSet()))
                .isEqualTo(changedKinoIds);

        for (String id: changedKinoIds) {
            Identifiable.Id kinoId = Identifiable.Id.of(id);

            assertThat(kinoSubscriber.getCache().kino(kinoId)).isNotNull();
            String kinoName = getRawOrNull(kinoSubscriber.getCache().kino(kinoId).name());
            assertThat(kinoName.endsWith("_changed")).isTrue();
            assertThat(kinoSubscriber.getCache().kino(kinoId).kinoSaele()).isNotNull();
            assertThat(kinoSubscriber.getCache().kino(kinoId).kinoSaele()).isNotEmpty();
            kinoSubscriber.getCache().kino(kinoId).kinoSaele().forEach(kinoSaalTO -> {
                assertThat(kinoSaalTO.kino()).isNotNull();
                String kinoSaalKinoName = getRawOrNull(kinoSaalTO.kino().name());
                assertThat(kinoSaalKinoName.endsWith("_changed")).isTrue();
                assertThat(kinoSaalTO.kino().id()).isEqualTo(kinoId);
            });
        }

        // assert VorfuehrungUpdated events
        assertThat(spy4VorfuehrungUpdatedEvent.allEvents().stream()
                .map(event -> event.vorfuehrung.id().id())
                .collect(Collectors.toSet()))
                .isEqualTo(vorfuehrungUpdatedIds);
        assertThat(spy4VorfuehrungUpdatedEvent.allEvents().stream()
                .map(event -> event.vorfuehrung.kinoId().id())
                .collect(Collectors.toSet()))
                .isEqualTo(changedKinoIds);
        assertThat(spy4VorfuehrungUpdatedEvent.allEvents().stream()
                .map(event -> event.vorfuehrung.kinoSaal().kino().id().id())
                .collect(Collectors.toSet()))
                .isEqualTo(changedKinoIds);
    }

}