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

import de.accso.flexinale.FlexinaleDistributedApplicationTestIntegrated;
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.common.shared_kernel.Versionable;
import de.accso.flexinale.common.shared_kernel.DoNotCheckInArchitectureTests;
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.ActiveProfiles;
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 = {FlexinaleDistributedApplicationTestIntegrated.class})
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
@ActiveProfiles("test-integrated")
@ExtendWith(SpringExtension.class)
@DoNotCheckInArchitectureTests
public class FKKsVEntityVersioningAndOptimisticLockTest {

    @Autowired
    private KinoDao kinoDao;

    @Autowired
    private FilmDao filmDao;

    @Autowired
    private VorfuehrungDao vorfuehrungDao;

    @Test
    void saveFilmInitiallySetsInitialVersionFromUnknownToZero() {
        // arrange
        Film newCreatedFilm = createFilm();

        // act
        filmDao.save(newCreatedFilm);

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

        Optional<Film> searchedFilm = filmDao.findById(newCreatedFilm.id());
        assertThat(searchedFilm.isPresent()).isTrue();
        assertThat(searchedFilm.get().version).isEqualTo(Versionable.initialVersion());

        // cleanup
        filmDao.deleteById(newCreatedFilm.id());

        // assert
        assertThat(filmDao.findById(newCreatedFilm.id())).isEmpty();
    }

    @Test
    void saveFilmThenChangeAndSaveAgainIncreasesItsVersion() {
        // arrange
        int initialVeryLongMovie = 24 * 60;
        Integer changeToOneHourMovie = 60;

        Film newCreatedFilm = createFilm();
        newCreatedFilm.dauerInMinuten = new Film.DauerInMinuten(initialVeryLongMovie);

        // act - save first time
        filmDao.save(newCreatedFilm);
        assertThat(newCreatedFilm.version).isEqualTo(Versionable.unknownVersion());

        // act - load from db and change
        Optional<Film> searched1stTimeFilm = filmDao.findById(newCreatedFilm.id());
        assertThat(searched1stTimeFilm.isPresent()).isTrue();

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

        // act - change and save again
        filmToUpdateAndSaveAgain.dauerInMinuten = new Film.DauerInMinuten(changeToOneHourMovie); // change value on the loaded entity
        filmDao.save(filmToUpdateAndSaveAgain);

        // act - load again from db
        Optional<Film> searched2ndTimeFilm = filmDao.findById(newCreatedFilm.id());
        assertThat(searched2ndTimeFilm.isPresent()).isTrue();

        Film filmLoadedAfterChange = searched2ndTimeFilm.get();
        assertThat(filmLoadedAfterChange.version).isEqualTo(Versionable.Version.of(1)); // version increased after update
        assertThat(filmLoadedAfterChange.id).isEqualTo(newCreatedFilm.id);
        assertThat(filmLoadedAfterChange.titel).isEqualTo(newCreatedFilm.titel);
        assertThat(filmLoadedAfterChange.imdbUrl).isEqualTo(newCreatedFilm.imdbUrl);
        assertThat(filmLoadedAfterChange.dauerInMinuten.raw()).isEqualTo(changeToOneHourMovie);

        // cleanup
        filmDao.deleteById(newCreatedFilm.id());

        // assert
        assertThat(filmDao.findById(newCreatedFilm.id())).isEmpty();
    }

    @Test
    void saveFilmThenChangeAndSaveAgainUsingOutdatedVersionIsAnError() {
        // arrange
        int initialVeryLongMovie = 24 * 60;
        int changeToOneHourMovie = 60;

        Film newCreatedFilm = createFilm();
        newCreatedFilm.dauerInMinuten = new Film.DauerInMinuten(initialVeryLongMovie);

        // act - save first time
        filmDao.save(newCreatedFilm);
        assertThat(newCreatedFilm.version).isEqualTo(Versionable.unknownVersion());

        // act - load from db and change
        Optional<Film> searched1stTimeFilm = filmDao.findById(newCreatedFilm.id());
        assertThat(searched1stTimeFilm.isPresent()).isTrue();

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

        // act - change and save again
        newCreatedFilm.dauerInMinuten = new Film.DauerInMinuten(changeToOneHourMovie); // change value but not the version

        // act
        Throwable thrown = catchThrowable(() -> filmDao.save(newCreatedFilm));

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

        // cleanup
        filmDao.deleteById(newCreatedFilm.id());

        // assert
        assertThat(filmDao.findById(newCreatedFilm.id())).isEmpty();
    }

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

    @Test
    void saveKinoInitiallySetsInitialVersionFromUnknownToZero() {
        // arrange
        Kino newCreatedKino = createKinoWithKinosaele();

        // act
        kinoDao.save(newCreatedKino);

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

        Optional<Kino> searchedKino = kinoDao.findById(newCreatedKino.id());
        assertThat(searchedKino.isPresent()).isTrue();
        assertThat(searchedKino.get().version).isEqualTo(Versionable.initialVersion());

        // cleanup
        kinoDao.deleteById(newCreatedKino.id());

        // assert
        assertThat(kinoDao.findById(newCreatedKino.id())).isEmpty();
    }

    @Test
    void saveKinoThenChangeAndSaveAgainIncreasesItsVersion() {
        // arrange
        String initialName = "initial";
        String changeToNewName = "renamed";

        Kino newCreatedKino = createKinoWithKinosaele();
        newCreatedKino.name = new Kino.Name(initialName);

        // act - save first time
        kinoDao.save(newCreatedKino);
        assertThat(newCreatedKino.version).isEqualTo(Versionable.unknownVersion());

        // act - load from db and change
        Optional<Kino> searched1stTimeKino = kinoDao.findById(newCreatedKino.id());
        assertThat(searched1stTimeKino.isPresent()).isTrue();

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

        // act - change and save again
        kinoToUpdateAndSaveAgain.name = new Kino.Name(changeToNewName); // change value on the loaded entity
        kinoDao.save(kinoToUpdateAndSaveAgain);

        // act - load again from db
        Optional<Kino> searched2ndTimeKino = kinoDao.findById(newCreatedKino.id());
        assertThat(searched2ndTimeKino.isPresent()).isTrue();

        Kino kinoLoadedAfterChange = searched2ndTimeKino.get();

        assertThat(kinoLoadedAfterChange.version).isEqualTo(Versionable.Version.of(1)); // version increased after update
        assertThat(kinoLoadedAfterChange.id).isEqualTo(newCreatedKino.id);
        assertThat(kinoLoadedAfterChange.adresse).isEqualTo(newCreatedKino.adresse);
        assertThat(kinoLoadedAfterChange.emailAdresse).isEqualTo(newCreatedKino.emailAdresse);
        assertThat(kinoLoadedAfterChange.kinoSaele.size()).isEqualTo(newCreatedKino.kinoSaele.size());
        assertThat(kinoLoadedAfterChange.name.raw()).isEqualTo(changeToNewName);

        // cleanup
        kinoDao.deleteById(newCreatedKino.id());

        // assert
        assertThat(kinoDao.findById(newCreatedKino.id())).isEmpty();
    }

    @Test
    void saveKinoThenChangeAndSaveAgainUsingOutdatedVersionIsAnError() {
        // arrange
        String initialName = "initial";
        String changeToNewName = "renamed";

        Kino newCreatedKino = createKinoWithKinosaele();
        newCreatedKino.name = new Kino.Name(initialName);

        // act - save first time
        kinoDao.save(newCreatedKino);
        assertThat(newCreatedKino.version).isEqualTo(Versionable.unknownVersion());

        // act - load from db and change
        Optional<Kino> searched1stTimeKino = kinoDao.findById(newCreatedKino.id());
        assertThat(searched1stTimeKino.isPresent()).isTrue();

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

        // act - change and save again
        newCreatedKino.name = new Kino.Name(changeToNewName); // change value but not the version

        // act
        Throwable thrown = catchThrowable(() -> kinoDao.save(newCreatedKino));

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

        // cleanup
        kinoDao.deleteById(newCreatedKino.id());

        // assert
        assertThat(kinoDao.findById(newCreatedKino.id())).isEmpty();
    }

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

    @Test
    void saveVorfuehrungInitiallySetsInitialVersionFromUnknownToZero() {
        // arrange
        Film film = createFilm();
        filmDao.save(film);

        Kino kino = createKinoWithKinosaele();
        kino = kinoDao.save(kino);

        LocalDateTime now = LocalDateTime.now();
        KinoSaal kinoSaal = kino.kinoSaele.stream().findFirst().get();
        Vorfuehrung newCreatedVorfuehrung = createVorfuehrung(now, film, kinoSaal);

        // act
        vorfuehrungDao.save(newCreatedVorfuehrung);

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

        Optional<Vorfuehrung> searchedVorfuehrung = vorfuehrungDao.findById(newCreatedVorfuehrung.id());
        assertThat(searchedVorfuehrung.isPresent()).isTrue();
        assertThat(searchedVorfuehrung.get().version).isEqualTo(Versionable.initialVersion());

        // cleanup
        vorfuehrungDao.deleteById(newCreatedVorfuehrung.id());
        filmDao.deleteById(film.id());
        kinoDao.deleteById(kino.id());

        // assert
        assertThat(vorfuehrungDao.findById(newCreatedVorfuehrung.id())).isEmpty();
    }

    @Test
    void saveVorfuehrungThenChangeAndSaveAgainIncreasesItsVersion() {
        // arrange
        LocalDateTime initialYesterday = LocalDateTime.now().withNano(0).minusDays(1);
        LocalDateTime changeToToday = LocalDateTime.now().withNano(0);

        Film film = createFilm();
        filmDao.save(film);

        Kino kino = createKinoWithKinosaele();
        kino = kinoDao.save(kino);

        KinoSaal kinoSaal = kino.kinoSaele.stream().findFirst().get();
        Vorfuehrung newCreatedVorfuehrung = createVorfuehrung(initialYesterday, film, kinoSaal);

        // act - save first time
        vorfuehrungDao.save(newCreatedVorfuehrung);
        assertThat(newCreatedVorfuehrung.version).isEqualTo(Versionable.unknownVersion());

        // act - load from db and change
        Optional<Vorfuehrung> searched1stTimeVorfuehrung = vorfuehrungDao.findById(newCreatedVorfuehrung.id());
        assertThat(searched1stTimeVorfuehrung.isPresent()).isTrue();

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

        // act - change and save again
        Vorfuehrung vorfuehrungChangedToUpdateAndSaveAgain =
                new Vorfuehrung(vorfuehrungToUpdateAndSaveAgain.id, vorfuehrungToUpdateAndSaveAgain.version,
                        new Vorfuehrung.Zeit(changeToToday), // changed value on the loaded entity
                        vorfuehrungToUpdateAndSaveAgain.film, vorfuehrungToUpdateAndSaveAgain.kinoSaal);
        vorfuehrungDao.save(vorfuehrungChangedToUpdateAndSaveAgain);

        // act - load again from db
        Optional<Vorfuehrung> searched2ndTimeVorfuehrung = vorfuehrungDao.findById(newCreatedVorfuehrung.id());
        assertThat(searched2ndTimeVorfuehrung.isPresent()).isTrue();

        Vorfuehrung vorfuehrungLoadedAfterChange = searched2ndTimeVorfuehrung.get();

        assertThat(vorfuehrungLoadedAfterChange.version).isEqualTo(Versionable.Version.of(1)); // version increased after update
        assertThat(vorfuehrungLoadedAfterChange.id).isEqualTo(newCreatedVorfuehrung.id);
        assertThat(vorfuehrungLoadedAfterChange.film.equalsByContent(newCreatedVorfuehrung.film)).isTrue();
        assertThat(vorfuehrungLoadedAfterChange.kinoSaal.equalsByContent(newCreatedVorfuehrung.kinoSaal)).isTrue();
        assertThat(vorfuehrungLoadedAfterChange.zeit.raw()).isEqualTo(changeToToday);

        // cleanup
        vorfuehrungDao.deleteById(newCreatedVorfuehrung.id());
        filmDao.deleteById(film.id());
        kinoDao.deleteById(kino.id());

        // assert
        assertThat(vorfuehrungDao.findById(newCreatedVorfuehrung.id())).isEmpty();
    }

    @Test
    void saveVorfuehrungThenChangeAndSaveAgainUsingOutdatedVersionIsAnError() {
        // arrange
        LocalDateTime initialYesterday = LocalDateTime.now().withNano(0).minusDays(1);
        LocalDateTime changeToToday = LocalDateTime.now().withNano(0);

        Film film = createFilm();
        filmDao.save(film);

        Kino kino = createKinoWithKinosaele();
        kino = kinoDao.save(kino);

        KinoSaal kinoSaal = kino.kinoSaele.stream().findFirst().get();
        Vorfuehrung newCreatedVorfuehrung = createVorfuehrung(initialYesterday, film, kinoSaal);

        // act - save first time
        vorfuehrungDao.save(newCreatedVorfuehrung);
        assertThat(newCreatedVorfuehrung.version).isEqualTo(Versionable.unknownVersion());

        // act - load from db and change
        Optional<Vorfuehrung> searched1stTimeVorfuehrung = vorfuehrungDao.findById(newCreatedVorfuehrung.id());
        assertThat(searched1stTimeVorfuehrung.isPresent()).isTrue();

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

        // act - change and save again
        Vorfuehrung newCreatedAndChangedVorfuehrung =
                new Vorfuehrung(newCreatedVorfuehrung.id, newCreatedVorfuehrung.version,
                    new Vorfuehrung.Zeit(changeToToday), // change value but not the version
                    newCreatedVorfuehrung.film, newCreatedVorfuehrung.kinoSaal);
        // act
        Throwable thrown = catchThrowable(() -> vorfuehrungDao.save(newCreatedAndChangedVorfuehrung));

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

        // cleanup
        vorfuehrungDao.deleteById(newCreatedAndChangedVorfuehrung.id());
        filmDao.deleteById(film.id());
        kinoDao.deleteById(kino.id());

        // assert
        assertThat(vorfuehrungDao.findById(newCreatedAndChangedVorfuehrung.id())).isEmpty();
    }
}
