package de.accso.flexinale;

import de.accso.flexinale.backoffice.api_contract.event.VorfuehrungCreatedEvent;
import de.accso.flexinale.backoffice.api_contract.event.VorfuehrungUpdatedEvent;
import de.accso.flexinale.backoffice.api_out.event.VorfuehrungPublisher;
import de.accso.flexinale.backoffice.domain.dao.FilmDao;
import de.accso.flexinale.backoffice.domain.dao.KinoDao;
import de.accso.flexinale.backoffice.domain.dao.VorfuehrungDao;
import de.accso.flexinale.backoffice.domain.model.Film;
import de.accso.flexinale.backoffice.domain.model.Kino;
import de.accso.flexinale.backoffice.domain.model.KinoSaal;
import de.accso.flexinale.backoffice.domain.model.Vorfuehrung;
import de.accso.flexinale.besucherportal.api_contract.event.GutscheinEinloesenBeauftragtEvent;
import de.accso.flexinale.besucherportal.api_out.event.GutscheinEinloesenBeauftragtPublisher;
import de.accso.flexinale.besucherportal.api_in.event.TicketSubscriber;
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 de.accso.flexinale.ticketing.api_contract.event.TicketGekauftEvent;
import de.accso.flexinale.ticketing.api_contract.event.model.TicketTO;
import de.accso.flexinale.ticketing.application.services.TicketService;
import de.accso.flexinale.ticketing.domain.dao.KontingentDao;
import de.accso.flexinale.ticketing.domain.model.Ticket;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;

import java.time.LocalDateTime;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;

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

@SpringBootTest(classes = {FlexinaleDistributedApplicationTestIntegrated.class})
@ActiveProfiles("test-integrated")
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
@DoNotCheckInArchitectureTests
public class End2EndEventsTest {
    // Besucherportal
    @Autowired
    GutscheinEinloesenBeauftragtPublisher gutscheinEinloesenBeauftragtPublisher;

    @Autowired
    TicketSubscriber ticketSubscriber;

    // Backoffice
    @Autowired
    VorfuehrungDao vorfuehrungDao;

    @Autowired
    FilmDao filmDao;

    @Autowired
    KinoDao kinoDao;

    @Autowired
    VorfuehrungPublisher vorfuehrungPublisher;

    // Ticketing
    @Autowired
    TicketService ticketService;

    @Autowired
    KontingentDao kontingentDao;

    // Bus
    @Autowired
    EventBusFactory eventBusFactory;

    @Test
    void testEnd2EndWithGutscheinEinloesenAndTicketKaufAndVorfuehrungenUpdates() {
        // arrange
        InMemorySyncEventBusSpy<GutscheinEinloesenBeauftragtEvent> gutscheinEinloesenBeauftragtBus =
                (InMemorySyncEventBusSpy<GutscheinEinloesenBeauftragtEvent>)
                        eventBusFactory.createOrGetEventBusFor(GutscheinEinloesenBeauftragtEvent.class);
        InMemorySyncEventBusSpy<TicketGekauftEvent> ticketGekauftBus =
                (InMemorySyncEventBusSpy<TicketGekauftEvent>)
                        eventBusFactory.createOrGetEventBusFor(TicketGekauftEvent.class);
        InMemorySyncEventBusSpy<VorfuehrungCreatedEvent> vorfuehrungCreatedBus =
                (InMemorySyncEventBusSpy<VorfuehrungCreatedEvent>)
                        eventBusFactory.createOrGetEventBusFor(VorfuehrungCreatedEvent.class);
        InMemorySyncEventBusSpy<VorfuehrungUpdatedEvent> vorfuehrungUpdatedBus =
                (InMemorySyncEventBusSpy<VorfuehrungUpdatedEvent>)
                        eventBusFactory.createOrGetEventBusFor(VorfuehrungUpdatedEvent.class);
        InMemorySyncEventBusSpy<OnlineKontingentChangedEvent> kontingentChangedBus =
                (InMemorySyncEventBusSpy<OnlineKontingentChangedEvent>)
                        eventBusFactory.createOrGetEventBusFor(OnlineKontingentChangedEvent.class);


        // ... create and publish Vorfuehrung
        Film film = createFilm();
        filmDao.save(film);

        Kino kino = createKinoWithKinosaele();
        Iterator<KinoSaal> kinoSaalIterator = kino.kinoSaele.iterator();
        KinoSaal kinoSaal1 = kinoSaalIterator.next();
        KinoSaal kinoSaal2 = kinoSaalIterator.next();
        kinoSaal2.anzahlPlaetze = new KinoSaal.AnzahlPlaetze(60);
        kinoDao.save(kino);

        // ... create Vorfuehrung
        Vorfuehrung vorfuehrung = createVorfuehrung(LocalDateTime.now(), film, kinoSaal1);
        Vorfuehrung vorfuehrungCreated = vorfuehrungDao.save(vorfuehrung);
        vorfuehrungPublisher.publishNewVorfuehrungen(List.of(vorfuehrung));

        // check VorfuehrungCreated and OnlineKontingentChanged events on bus(es) and correlation IDs
        assertThat(vorfuehrungCreatedBus.allEvents()).hasSize(1);
        VorfuehrungCreatedEvent vorfuehrungCreatedEvent = vorfuehrungCreatedBus.allEvents().getFirst();

        assertThat(kontingentChangedBus.allEvents()).hasSize(1);
        OnlineKontingentChangedEvent kontingentChangedAfterVorfuehrungCreated = kontingentChangedBus.allEvents().getFirst();

        assertThat(kontingentChangedAfterVorfuehrungCreated.correlationId()).isEqualTo(vorfuehrungCreatedEvent.correlationId());

        // ... loese Gutscheine ein
        Identifiable.Id besucherId = Identifiable.Id.of();
        int anzahlGutscheine = 5;
        gutscheinEinloesenBeauftragtPublisher.publishGutscheinEinloesenBeauftragtEvent(
                vorfuehrung.id(), film.id(), besucherId, anzahlGutscheine);
        assertThat(gutscheinEinloesenBeauftragtBus.allEvents()).hasSize(1);
        GutscheinEinloesenBeauftragtEvent gutscheinEinloesenBeauftragtEvent = gutscheinEinloesenBeauftragtBus.allEvents().getFirst();

        // ... Kaufe Tickets
        assertThat(ticketService.findByBesucher(besucherId).size()).isEqualTo(anzahlGutscheine);
        Integer kinoSaal1AnzahlPlaetze = getRawOrNull(kinoSaal1.anzahlPlaetze);
        assertThat(kontingentDao.findByVorfuehrung(vorfuehrung.id()).get().getRestKontingentOnline().raw())
                .isEqualTo(kinoSaal1AnzahlPlaetze/3 - anzahlGutscheine);

        List<TicketTO> ticketsByBesucherGueltig = ticketSubscriber.getCache().ticketsByBesucher(besucherId);
        assertThat(ticketsByBesucherGueltig.size()).isEqualTo(anzahlGutscheine);
        assertThat(ticketsByBesucherGueltig
                .stream()
                .filter(t -> {
                    Boolean gueltig = getRawOrNull(t.gueltig());
                    return !gueltig; })
                .collect(Collectors.toList()))
                .isEmpty();

        // check TicketGekauft and OnlineKontingentChanged events on bus(es) and correlation IDs
        assertThat(ticketGekauftBus.allEvents()).hasSize(anzahlGutscheine);
        for (TicketGekauftEvent ticketGekauftEvent: ticketGekauftBus.allEvents()) {
            assertThat(ticketGekauftEvent.correlationId()).isEqualTo(
                       gutscheinEinloesenBeauftragtEvent.correlationId());
        }

        assertThat(kontingentChangedBus.allEvents()).hasSize(2);
        OnlineKontingentChangedEvent kontingentChangedAfterTicketKauf = kontingentChangedBus.allEvents().get(1);
        assertThat(kontingentChangedAfterTicketKauf.correlationId()).isEqualTo(gutscheinEinloesenBeauftragtEvent.correlationId());

        // ... update Vorfuehrung (KinoSaal)
        Vorfuehrung vorfuehrungUpdate = new Vorfuehrung(vorfuehrungCreated.id, vorfuehrungCreated.version,
                vorfuehrungCreated.zeit, vorfuehrungCreated.film, kinoSaal2);

        vorfuehrungDao.save(vorfuehrungUpdate);
        vorfuehrungPublisher.publishUpdatedVorfuehrungen(List.of(vorfuehrungUpdate));

        // check VorfuehrungUpdated and OnlineKontingentChanged events on bus(es) and correlation IDs
        assertThat(vorfuehrungUpdatedBus.allEvents()).hasSize(1);
        VorfuehrungUpdatedEvent vorfuehrungUpdatedEvent = vorfuehrungUpdatedBus.allEvents().getFirst();

        assertThat(kontingentChangedBus.allEvents()).hasSize(3);
        OnlineKontingentChangedEvent kontingentChangedAfterVorfuehrungUpdated = kontingentChangedBus.allEvents().get(2);

        assertThat(kontingentChangedAfterVorfuehrungUpdated.correlationId()).isEqualTo(vorfuehrungUpdatedEvent.correlationId());

        // act, assert
        // 1) Ticketing, Kontingente

        List<Ticket> ticketsForBesucher = ticketService.findByBesucher(besucherId);
        assertThat(ticketsForBesucher.size()).isEqualTo(anzahlGutscheine);
        assertThat(ticketsForBesucher.stream().filter(t -> getRawOrNull(t.gueltig)).collect(Collectors.toList()))
                .isEmpty();

        Integer kinoSaal2AnzahlPlaetze = getRawOrNull(kinoSaal2.anzahlPlaetze);
        assertThat(kontingentDao.findByVorfuehrung(vorfuehrung.id()).get().getRestKontingentOnline().raw())
                .isEqualTo(kinoSaal2AnzahlPlaetze/3);

        // 2) Besucherportal
        List<TicketTO> ticketsByBesucherUngueltig = ticketSubscriber.getCache().ticketsByBesucher(besucherId);
        assertThat(ticketsByBesucherUngueltig.size()).isEqualTo(anzahlGutscheine);
        assertThat(ticketsByBesucherUngueltig
                .stream()
                .filter(t -> getRawOrNull(t.gueltig()))
                .collect(Collectors.toList()))
                .isEmpty();

        // act
        // ... buy new Tickets
        int newAnzahlGutscheine = anzahlGutscheine - 1;
        ticketService.loeseGutscheineOnlineFuerVorfuehrungEin(vorfuehrung.id(), film.id(), besucherId,newAnzahlGutscheine);

        List<TicketTO> moreTickets = ticketSubscriber.getCache().ticketsByBesucher(besucherId);

        // assert
        assertThat(moreTickets.size()).isEqualTo(anzahlGutscheine + newAnzahlGutscheine);
        assertThat(moreTickets
                .stream()
                .filter(t -> getRawOrNull(t.gueltig()))
                .toList().size())
                .isEqualTo(newAnzahlGutscheine);
        assertThat(moreTickets
                .stream()
                .filter(t -> {
                    Boolean gueltig = getRawOrNull(t.gueltig());
                    return ! gueltig; })
                .toList().size())
                .isEqualTo(anzahlGutscheine);
    }
}
