package de.accso.flexinale.ticketing.infrastructure.persistence;

import de.accso.flexinale.FlexinaleModulith2ComponentsApplication;
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.common.shared_kernel.Versionable;
import de.accso.flexinale.security.infrastructure.persistence.BenutzerEntity;
import de.accso.flexinale.common.shared_kernel.DoNotCheckInArchitectureTests;
import de.accso.flexinale.ticketing.domain.dao.TicketDao;
import de.accso.flexinale.ticketing.domain.model.Ticket;
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.dao.OptimisticLockingFailureException;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.time.LocalDateTime;
import java.util.Optional;

import static de.accso.flexinale.shared_code_for_test.TestDataBuilder.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;

@SpringBootTest(classes = {FlexinaleModulith2ComponentsApplication.class})
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
@ExtendWith(SpringExtension.class)
@DoNotCheckInArchitectureTests
public class TicketEntityVersioningAndOptimisticLockingTest {

    @Autowired
    private TicketDao ticketDao;

    @Test
    void saveTicketInitiallySetsInitialVersionFromUnknownToZero() {
        // arrange
        Film film = createFilm();
        Kino kino = createKinoWithKinosaele(100);
        KinoSaal kinoSaal = kino.kinoSaele.iterator().next();

        Vorfuehrung vorfuehrung = createVorfuehrung(LocalDateTime.now(), film, kinoSaal);
        BenutzerEntity benutzer = createBenutzerEntity();

        Ticket newCreatedTicket = createTicket(film, vorfuehrung, benutzer);

        // act
        ticketDao.save(newCreatedTicket);

        // assert
        assertThat(newCreatedTicket.version).isEqualTo(Versionable.unknownVersion());

        Optional<Ticket> searchedTicket = ticketDao.findById(newCreatedTicket.id());
        assertThat(searchedTicket.isPresent()).isTrue();
        assertThat(searchedTicket.get().version).isEqualTo(Versionable.initialVersion());

        // cleanup
        ticketDao.deleteById(newCreatedTicket.id());
        // assert
        assertThat(ticketDao.findById(newCreatedTicket.id())).isEmpty();
    }

    @Test
    void saveTicketThenChangeAndSaveAgainIncreasesItsVersion() {
        // arrange
        Ticket.VerkaufsKanal initialVerkaufskanal = Ticket.VerkaufsKanal.ONLINE;
        Ticket.VerkaufsKanal changeToVerkaufskanal = Ticket.VerkaufsKanal.ZENTRAL;

        Film film = createFilm();
        Kino kino = createKinoWithKinosaele(100);
        KinoSaal kinoSaal = kino.kinoSaele.iterator().next();

        Vorfuehrung vorfuehrung = createVorfuehrung(LocalDateTime.now(), film, kinoSaal);
        BenutzerEntity benutzer = createBenutzerEntity();

        Ticket newCreatedTicket = createTicket(film, vorfuehrung, benutzer);
        newCreatedTicket.verkaufsKanal = initialVerkaufskanal;

        // act - save first time
        ticketDao.save(newCreatedTicket);
        assertThat(newCreatedTicket.version).isEqualTo(Versionable.unknownVersion());

        // act - load from db and change
        Optional<Ticket> searched1stTimeTicket = ticketDao.findById(newCreatedTicket.id());
        assertThat(searched1stTimeTicket.isPresent()).isTrue();

        // assert that version has been updated
        Ticket ticketToUpdateAndSaveAgain = searched1stTimeTicket.get();
        assertThat(ticketToUpdateAndSaveAgain.version).isEqualTo(Versionable.initialVersion()); // version increased after initial create

        // act - change and save again
        ticketToUpdateAndSaveAgain.verkaufsKanal = changeToVerkaufskanal; // change value on the loaded entity
        ticketDao.save(ticketToUpdateAndSaveAgain);

        // act - load again from db
        Optional<Ticket> searched2ndTimeTicket = ticketDao.findById(newCreatedTicket.id());
        assertThat(searched2ndTimeTicket.isPresent()).isTrue();

        Ticket ticketLoadedAfterChange = searched2ndTimeTicket.get();

        assertThat(ticketLoadedAfterChange.version).isEqualTo(Versionable.Version.of(1)); // version increased after update
        assertThat(ticketLoadedAfterChange.id).isEqualTo(newCreatedTicket.id);
        assertThat(ticketLoadedAfterChange.filmId.equals(newCreatedTicket.filmId)).isTrue();
        assertThat(ticketLoadedAfterChange.vorfuehrungId.equals(newCreatedTicket.vorfuehrungId)).isTrue();
        assertThat(ticketLoadedAfterChange.besucherId.equals(newCreatedTicket.besucherId)).isTrue();
        assertThat(ticketLoadedAfterChange.verkaufsKanal).isEqualTo(changeToVerkaufskanal);

        // cleanup
        ticketDao.deleteById(newCreatedTicket.id());

        // assert
        assertThat(ticketDao.findById(newCreatedTicket.id())).isEmpty();
    }

    @Test
    void saveTicketThenChangeAndSaveAgainUsingOutdatedVersionIsAnError() {
        // arrange
        Ticket.VerkaufsKanal initialVerkaufskanal = Ticket.VerkaufsKanal.ONLINE;
        Ticket.VerkaufsKanal changeToVerkaufskanal = Ticket.VerkaufsKanal.ZENTRAL;

        Film film = createFilm();
        Kino kino = createKinoWithKinosaele(100);
        KinoSaal kinoSaal = kino.kinoSaele.iterator().next();

        Vorfuehrung vorfuehrung = createVorfuehrung(LocalDateTime.now(), film, kinoSaal);
        BenutzerEntity benutzer = createBenutzerEntity();

        Ticket newCreatedTicket = createTicket(film, vorfuehrung, benutzer);
        newCreatedTicket.verkaufsKanal = initialVerkaufskanal;

        // act - save first time
        ticketDao.save(newCreatedTicket);
        assertThat(newCreatedTicket.version).isEqualTo(Versionable.unknownVersion());

        // act - load from db and change
        Optional<Ticket> searched1stTimeTicket = ticketDao.findById(newCreatedTicket.id());
        assertThat(searched1stTimeTicket.isPresent()).isTrue();

        // assert that version has been updated
        assertThat(searched1stTimeTicket.get().version).isEqualTo(Versionable.initialVersion()); // version increased after initial create

        // act - change and save again
        newCreatedTicket.verkaufsKanal = changeToVerkaufskanal; // change value but not the version

        // act
        Throwable thrown = catchThrowable(() -> ticketDao.save(newCreatedTicket));

        // assert
        String expectedMessagePart =
                "Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [de.accso.flexinale.ticketing.infrastructure.persistence.TicketEntity";
        assertThat(thrown)
                .isInstanceOf(OptimisticLockingFailureException.class)
                .hasMessageContaining(expectedMessagePart);

        // cleanup
        ticketDao.deleteById(newCreatedTicket.id());

        // assert
        assertThat(ticketDao.findById(newCreatedTicket.id())).isEmpty();
    }
}
