package de.accso.flexinale.besucherportal.api_in.web;

import de.accso.flexinale.FlexinaleDistributedApplicationTestIntegrated;
import de.accso.flexinale.backoffice.api_out.event.FilmPublisher;
import de.accso.flexinale.backoffice.api_out.event.KinoPublisher;
import de.accso.flexinale.backoffice.api_out.event.VorfuehrungPublisher;
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_contract.event.model.GutscheinEinloesenAuftragTO;
import de.accso.flexinale.besucherportal.api_out.event.GutscheinEinloesenBeauftragtPublisher;
import de.accso.flexinale.besucherportal.api_in.event.TicketSubscriber;
import de.accso.flexinale.common.shared_kernel.DoNotCheckInArchitectureTests;
import de.accso.flexinale.common.shared_kernel.Identifiable;
import de.accso.flexinale.common.shared_kernel.Versionable;
import de.accso.flexinale.shared_code_for_test.TestDataBuilder;
import de.accso.flexinale.ticketing.api_contract.event.model.TicketTO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
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 org.springframework.test.context.junit.jupiter.SpringExtension;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

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

@SpringBootTest(classes = {FlexinaleDistributedApplicationTestIntegrated.class})
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
@ActiveProfiles("test-integrated")
@ExtendWith(SpringExtension.class)
@DoNotCheckInArchitectureTests
class GutscheinEinloesenBeauftragtSubscriberAndSortingTest {
    @Autowired
    private TicketSubscriber ticketSubscriber;

    @Autowired
    private GutscheinEinloesenBeauftragtPublisher gutscheinEinloesenBeauftragtPublisher;

    @Autowired
    private KinoPublisher kinoPublisher;

    @Autowired
    private VorfuehrungPublisher vorfuehrungPublisher;

    @Autowired
    FilmPublisher filmPublisher;

    private Identifiable.Id besucherId;
    private Vorfuehrung vorfuehrung1;


    @BeforeEach
    void prepare() {
        // arrange for all tests
        Film film = createFilm();
        Collection<Film> newFilms = new ArrayList<>();
        newFilms.add(film);
        filmPublisher.publishNewFilms(newFilms);

        Kino kino = createKinoWithKinosaele(120);
        KinoSaal kinoSaal = kino.kinoSaele.iterator().next();
        Collection<Kino> newKinos = new ArrayList<>();
        newKinos.add(kino);
        kinoPublisher.publishNewKinos(newKinos);

        LocalDateTime beginnVorfuehrung1 = LocalDateTime.of(2022, 11, 29, 16, 0);

        vorfuehrung1 = TestDataBuilder.createVorfuehrung(beginnVorfuehrung1, film, kinoSaal);
        Vorfuehrung vorfuehrung2 = TestDataBuilder.createVorfuehrung(beginnVorfuehrung1.plusHours(24), film, kinoSaal);
        Vorfuehrung vorfuehrung3 = TestDataBuilder.createVorfuehrung(beginnVorfuehrung1.plusHours(3), film, kinoSaal);

        Collection<Vorfuehrung> newVorfuehrungen = List.of(vorfuehrung1, vorfuehrung2, vorfuehrung3);

        vorfuehrungPublisher.publishNewVorfuehrungen(newVorfuehrungen);

        besucherId = Identifiable.Id.of();

        gutscheinEinloesenBeauftragtPublisher
                .publishGutscheinEinloesenBeauftragtEvent(vorfuehrung1.id(), vorfuehrung1.film.id(), besucherId, 2);
        gutscheinEinloesenBeauftragtPublisher
                .publishGutscheinEinloesenBeauftragtEvent(vorfuehrung2.id(), vorfuehrung2.film.id(), besucherId, 3);
        gutscheinEinloesenBeauftragtPublisher
                .publishGutscheinEinloesenBeauftragtEvent(vorfuehrung3.id(), vorfuehrung3.film.id(), besucherId, 4);
    }

   @Test
   void testTicketKauf() {
        // arrange
       Identifiable.Id anotherBesucherId = Identifiable.Id.of();

       List<TicketTO> tickets = ticketSubscriber.getCache().ticketsByBesucher(besucherId);
        assertThat(tickets.size()).isEqualTo(9);

        // arbitrary besucher who has not yet bought tickets
        List<TicketTO> shouldNotContainTickets = ticketSubscriber.getCache().ticketsByBesucher(anotherBesucherId);
        assertThat(shouldNotContainTickets).isEmpty();
   }

    @Test
    void testTicketKaufButNotEnoughKontingent() {
        // arrange
        gutscheinEinloesenBeauftragtPublisher
                .publishGutscheinEinloesenBeauftragtEvent(vorfuehrung1.id(), vorfuehrung1.film.id(), besucherId, 100);

        // act
        List<TicketTO> shouldOnlyContainTicketsFromPrepare = ticketSubscriber.getCache().ticketsByBesucher(besucherId);

        // assert
        assertThat(shouldOnlyContainTicketsFromPrepare.size()).isEqualTo(9);
    }

    @Test
    void testTicketKaufWithDuplicateEvent() {
        // arrange
        Identifiable.Id anotherBesucherId = Identifiable.Id.of();
        GutscheinEinloesenAuftragTO gutschein = new GutscheinEinloesenAuftragTO(Identifiable.Id.of(), Versionable.initialVersion(),
                vorfuehrung1.film.id(), vorfuehrung1.id(), anotherBesucherId,
                new GutscheinEinloesenAuftragTO.AnzahlTickets(2));

        GutscheinEinloesenBeauftragtEvent gutscheinEinloesenBeauftragtEvent =
                new GutscheinEinloesenBeauftragtEvent(gutschein);

        // act
        gutscheinEinloesenBeauftragtPublisher.post(GutscheinEinloesenBeauftragtEvent.class, gutscheinEinloesenBeauftragtEvent);

        List<TicketTO> tickets = ticketSubscriber.getCache().ticketsByBesucher(anotherBesucherId);

        // assert
        assertThat(tickets.size()).isEqualTo(2);

        // act
        // pulish same event again
        gutscheinEinloesenBeauftragtPublisher.post(GutscheinEinloesenBeauftragtEvent.class, gutscheinEinloesenBeauftragtEvent);

        List<TicketTO> ticketsAfterSecondEvent = ticketSubscriber.getCache().ticketsByBesucher(anotherBesucherId);

        // assert
        assertThat(ticketsAfterSecondEvent.size()).isEqualTo(2);
    }

    @Test
    void testTicketKaufWithSameButNotDuplicateEvent() {
        // arrange
        Identifiable.Id anotherBesucherId = Identifiable.Id.of();

        // act
        gutscheinEinloesenBeauftragtPublisher
                .publishGutscheinEinloesenBeauftragtEvent(vorfuehrung1.id(), vorfuehrung1.film.id(), anotherBesucherId, 2);
        List<TicketTO> tickets = ticketSubscriber.getCache().ticketsByBesucher(anotherBesucherId);

        // assert
        assertThat(tickets.size()).isEqualTo(2);

        // act
        gutscheinEinloesenBeauftragtPublisher
                .publishGutscheinEinloesenBeauftragtEvent(vorfuehrung1.id(), vorfuehrung1.film.id(), anotherBesucherId, 2);
        List<TicketTO> ticketsAfterSecondEvent = ticketSubscriber.getCache().ticketsByBesucher(anotherBesucherId);

        // assert
        assertThat(ticketsAfterSecondEvent.size()).isEqualTo(4);
    }

    @Test
    void testSortAndMapGueltigeTicketsPerDay() {
        // act
        List<TicketTO> tickets = ticketSubscriber.getCache().ticketsByBesucher(besucherId);

        // assert
        assertThat(tickets.size()).isEqualTo(9);

        List<List<VorfuehrungMitAnzahlTicketsTO>> vorfuehrungenTickets
                = TicketSorter.sortAndMapTicketsPerDay(tickets, ticketSubscriber.getCache());

        // The outer list contains one list for each day. There are tickets for 2 different days
        assertThat(vorfuehrungenTickets.size()).isEqualTo(2);

        // assert order by Zeit
        VorfuehrungMitAnzahlTicketsTO last = null;
        for (VorfuehrungMitAnzahlTicketsTO vMATTO: vorfuehrungenTickets.stream()
                .flatMap(inner -> inner.stream()).toList())
        {
            if (last != null) {
                LocalDateTime lastZeit = getRawOrNull(last.zeit());
                LocalDateTime vMATTOZeit = getRawOrNull(vMATTO.zeit());
                assertThat(lastZeit).isBeforeOrEqualTo(vMATTOZeit);
            }
            last = vMATTO;
        }

        // assert sorting
        // Day 1: 2 Vorfuehrungen a 2 resp 3 tickets
        List<VorfuehrungMitAnzahlTicketsTO> vorfuehrungenOfDay1 = vorfuehrungenTickets.getFirst();

        assertThat(vorfuehrungenOfDay1.size()).isEqualTo(2);
        LocalDateTime zeit0 = getRawOrNull(vorfuehrungenOfDay1.get(0).zeit());
        LocalDateTime zeit1 = getRawOrNull(vorfuehrungenOfDay1.get(1).zeit());
        assertThat(zeit0).isBeforeOrEqualTo(zeit1);

        assertThat(vorfuehrungenOfDay1.get(0).anzahlGueltigeTickets().raw()).isEqualTo(2);
        assertThat(vorfuehrungenOfDay1.get(1).anzahlGueltigeTickets().raw()).isEqualTo(4);

        // Day 2: 1 Vorfuehrung
        List<VorfuehrungMitAnzahlTicketsTO> vorfuehrungenOfDay2 = vorfuehrungenTickets.get(1);
        assertThat(vorfuehrungenOfDay2.size()).isEqualTo(1);
        assertThat(vorfuehrungenOfDay2.getFirst().anzahlGueltigeTickets().raw()).isEqualTo(3);
    }

    @Test
    void testSortAndMapGueltigeAndUngueltigeTicketsPerDay() {
        // arrange: invalidate a ticket
        List<TicketTO> tickets1 = ticketSubscriber.getCache().ticketsByBesucher(besucherId);
        TicketTO aTicket = tickets1.getFirst();
        TicketTO ungueltigesTicket = new TicketTO(aTicket.id(), aTicket.version(), aTicket.filmId(), aTicket.vorfuehrungId(), aTicket.besucherId(), aTicket.verkaufsKanal(), new TicketTO.Gueltig(false));
        ticketSubscriber.getCache().addOrUpdate(ungueltigesTicket);

        // act
        List<TicketTO> tickets = ticketSubscriber.getCache().ticketsByBesucher(besucherId);

        // assert
        assertThat(tickets.size()).isEqualTo(9);

        List<List<VorfuehrungMitAnzahlTicketsTO>> vorfuehrungenTickets
                = TicketSorter.sortAndMapTicketsPerDay(tickets, ticketSubscriber.getCache());

        // The outer list contains one list for each day. There are tickets for 2 different days
        assertThat(vorfuehrungenTickets.size()).isEqualTo(2);

        // assert order by Zeit
        VorfuehrungMitAnzahlTicketsTO last = null;
        for (VorfuehrungMitAnzahlTicketsTO vMATTO: vorfuehrungenTickets.stream()
                .flatMap(inner -> inner.stream()).toList())
        {
            if (last != null) {
                LocalDateTime lastZeit = getRawOrNull(last.zeit());
                LocalDateTime vMATTOZeit = getRawOrNull(vMATTO.zeit());
                assertThat(lastZeit).isBeforeOrEqualTo(vMATTOZeit);
            }
            last = vMATTO;
        }

        // assert sorting
        // Day 1: 2 Vorfuehrungen a 2 resp 3 tickets
        List<VorfuehrungMitAnzahlTicketsTO> vorfuehrungenOfDay1 = vorfuehrungenTickets.getFirst();

        assertThat(vorfuehrungenOfDay1.size()).isEqualTo(2);
        LocalDateTime zeit0 = getRawOrNull(vorfuehrungenOfDay1.get(0).zeit());
        LocalDateTime zeit1 = getRawOrNull(vorfuehrungenOfDay1.get(1).zeit());
        assertThat(zeit0).isBeforeOrEqualTo(zeit1);

        // Day 2: 1 Vorfuehrung
        List<VorfuehrungMitAnzahlTicketsTO> vorfuehrungenOfDay2 = vorfuehrungenTickets.get(1);
        assertThat(vorfuehrungenOfDay2.size()).isEqualTo(1);

        // anzahl gueltige Tickets
        assertThat(vorfuehrungenOfDay1.get(0).anzahlGueltigeTickets().raw()
                + vorfuehrungenOfDay1.get(1).anzahlGueltigeTickets().raw()
                + vorfuehrungenOfDay2.getFirst().anzahlGueltigeTickets().raw()).isEqualTo(8);

        // anzahl ungueltige Tickets
        assertThat(vorfuehrungenOfDay1.get(0).anzahlUngueltigeTickets().raw()
                + vorfuehrungenOfDay1.get(1).anzahlUngueltigeTickets().raw()
                + vorfuehrungenOfDay2.getFirst().anzahlUngueltigeTickets().raw()).isEqualTo(1);

    }
}