#  Copyright (c) 2022. The Pycroft Authors. See the AUTHORS file.
#  This file is part of the Pycroft project and licensed under the terms of
#  the Apache License, Version 2.0. See the LICENSE file for details
from typing import cast

import pytest
from coverage.annotate import os
from sqlalchemy import event
from sqlalchemy.ext.declarative import DeferredReflection
from sqlalchemy.future import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.pool import SingletonThreadPool

from pycroft.model import drop_db_model, create_db_model
from pycroft.model.session import set_scoped_session, Session


@pytest.fixture(scope='session')
def engine():
    try:
        uri = os.environ['PYCROFT_DB_URI']
    except KeyError:
        raise RuntimeError("Environment variable PYCROFT_DB_URI must be "
                           "set to an SQLalchemy connection string.")
    return create_engine(uri, poolclass=SingletonThreadPool, future=True)


@pytest.fixture(scope='session')
def clean_engine(engine):
    connection = engine.connect()
    drop_db_model(connection)
    create_db_model(connection)
    connection.commit()
    return engine


@pytest.fixture(scope='session')
def connection(clean_engine):
    engine = clean_engine
    connection = engine.connect()
    # this turns our connection into „transactional state“
    # henceforth, every session binding to this connection will participate in this transaction.
    # see https://docs.sqlalchemy.org/en/14/orm/session_transaction.html#joining-a-session-into-an-external-transaction-such-as-for-test-suites
    transaction = connection.begin()  # outer, non-ORM transaction
    DeferredReflection.prepare(engine)

    yield connection

    transaction.rollback()
    connection.close()


@pytest.fixture(scope='module')
def module_session(connection):
    """Provides a session to a created database.

    Rolled back after use
    """
    nested = connection.begin_nested()
    s = scoped_session(sessionmaker(bind=connection, future=True))
    set_scoped_session(s)
    session = cast(Session, s())

    # see the comment above
    @event.listens_for(session, "after_transaction_end")
    def end_savepoint(session, transaction):
        nonlocal nested
        if not nested.is_active:
            nested = connection.begin_nested()

    yield session

    # close_all_sessions()
    session.rollback()
    Session.remove()
    # if the transaction is still associated, this means it has e.g. pending trigger events.
    transaction_associated = nested.connection._transaction == nested
    if transaction_associated:
        nested.rollback()


@pytest.fixture()
def session(connection, module_session):
    # starts a transaction & rolls it back after each test
    module_session.begin_nested()
    yield module_session
    module_session.rollback()

@@ -1771,24 +1771,369 @@
 on.close()%0A%0A
+@pytest.fixture(scope='module')%0Adef module_transaction(connection):%0A    nested = connection.begin_nested()%0A    yield nested%0A    # if the transaction is still associated, this means it has e.g. pending trigger events.%0A    transaction_associated = nested.connection._transaction == nested%0A    if transaction_associated:%0A        nested.rollback()%0A%0A
 %0A@pytest.fix
@@ -2178,24 +2178,44 @@
 n(connection
+, module_transaction
 ):%0A    %22%22%22Pr
@@ -2306,33 +2306,26 @@
 d = 
-connection.begin_nested()
+module_transaction
 %0A   
@@ -2777,206 +2777,463 @@
 e()%0A
-    # if the transaction is still associated, this means it has e.g. pending trigger events.%0A    transaction_associated = nested.connection._transaction == nested%0A    if transaction_associated:
+%0A%0Adef _maybe_rollback_transaction(session):%0A    # flushes pass the exception to their parents%0A    if (t := session._transaction) and t._rollback_exception:%0A        t.rollback()%0A%0A%0A@pytest.fixture(scope='class')%0Adef class_session(module_session, module_transaction):%0A    # starts a transaction & rolls it back after each class' test%0A    session = module_session%0A    _maybe_rollback_transaction(session)%0A    nested = session.begin_nested()
 %0A    
+yield session%0A
     
@@ -3283,33 +3283,20 @@
 ession(c
-onnection, module
+lass
 _session
@@ -3357,31 +3357,102 @@
 ch test%0A    
-module_
+session = class_session%0A    _maybe_rollback_transaction(session)%0A    nested = 
 session.begi
@@ -3472,23 +3472,16 @@
   yield 
-module_
 session%0A
@@ -3484,30 +3484,22 @@
 ion%0A    
-module_session
+nested
 .rollbac

Add `class_session` and fix rollback behavior on exceptions