package de.accso.flexinale.infrastructure.persistence;

import de.accso.flexinale.FlexinaleModulith1OnionApplication;
import de.accso.flexinale.domain.dao.FilmDao;
import de.accso.flexinale.domain.dao.KinoDao;
import de.accso.flexinale.domain.dao.TicketDao;
import de.accso.flexinale.domain.dao.VorfuehrungDao;
import de.accso.flexinale.domain.model.*;
import de.accso.flexinale.infrastructure.security.BenutzerDao;
import de.accso.flexinale.infrastructure.security.BenutzerEntity;
import de.accso.flexinale.infrastructure.security.mapper.BenutzerEntity2BesucherMapper;
import de.accso.flexinale.shared_kernel.DoNotCheckInArchitectureTests;
import de.accso.flexinale.shared_kernel.Identifiable;
import de.accso.flexinale.shared_kernel.Versionable;
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 = {FlexinaleModulith1OnionApplication.class})
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
@ExtendWith(SpringExtension.class)
@DoNotCheckInArchitectureTests
public class EntityVersioningAndOptimisticLockTest {

    @Autowired
    private KinoDao kinoDao;

    @Autowired
    private FilmDao filmDao;

    @Autowired
    private VorfuehrungDao vorfuehrungDao;

    @Autowired
    private TicketDao ticketDao;

    @Autowired
    private BenutzerDao benutzerDao;

    @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.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).isEqualTo(new Kino.Name(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.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,
                        vorfuehrungToUpdateAndSaveAgain.getKontingent());
        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).isEqualTo(new Vorfuehrung.Zeit(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,
                newCreatedVorfuehrung.getKontingent());
        // 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.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();
    }

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

    @Test
    void saveTicketInitiallySetsInitialVersionFromUnknownToZero() {
        // 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 vorfuehrung = createVorfuehrung(now, film, kinoSaal);
        vorfuehrungDao.save(vorfuehrung);

        BenutzerEntity benutzer = createBenutzerEntity();
        benutzerDao.save(benutzer);
        Besucher besucher = BenutzerEntity2BesucherMapper.map(benutzer);

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

        // 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());
        benutzerDao.deleteById(Identifiable.Id.of(benutzer.id));
        vorfuehrungDao.deleteById(vorfuehrung.id());
        filmDao.deleteById(film.id());
        kinoDao.deleteById(kino.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();
        filmDao.save(film);

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

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

        BenutzerEntity benutzer = createBenutzerEntity();
        benutzerDao.save(benutzer);
        Besucher besucher = BenutzerEntity2BesucherMapper.map(benutzer);

        Ticket newCreatedTicket = createTicket(film, vorfuehrung, besucher);
        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.film.equalsByContent(newCreatedTicket.film)).isTrue();
        assertThat(ticketLoadedAfterChange.vorfuehrung.equalsByContent(newCreatedTicket.vorfuehrung)).isTrue();
        assertThat(ticketLoadedAfterChange.besucher.equalsByContent(newCreatedTicket.besucher)).isTrue();
        assertThat(ticketLoadedAfterChange.verkaufsKanal).isEqualTo(changeToVerkaufskanal);

        // cleanup
        ticketDao.deleteById(newCreatedTicket.id());
        benutzerDao.deleteById(Identifiable.Id.of(benutzer.id));
        vorfuehrungDao.deleteById(vorfuehrung.id());
        filmDao.deleteById(film.id());
        kinoDao.deleteById(kino.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();
        filmDao.save(film);

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

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

        BenutzerEntity benutzer = createBenutzerEntity();
        benutzerDao.save(benutzer);
        Besucher besucher = BenutzerEntity2BesucherMapper.map(benutzer);

        Ticket newCreatedTicket = createTicket(film, vorfuehrung, besucher);
        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.infrastructure.persistence.TicketEntity";
        assertThat(thrown)
                .isInstanceOf(OptimisticLockingFailureException.class)
                .hasMessageContaining(expectedMessagePart);

        // cleanup
        ticketDao.deleteById(newCreatedTicket.id());
        benutzerDao.deleteById(Identifiable.Id.of(benutzer.id));
        vorfuehrungDao.deleteById(vorfuehrung.id());
        filmDao.deleteById(film.id());
        kinoDao.deleteById(kino.id());

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