from collections.abc import Generator
from typing import (
    TYPE_CHECKING,
)

import pytest
from sqlalchemy import (
    create_engine,
    text,
)
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session

from galaxy import model as m
from galaxy.datatypes.registry import Registry as DatatypesRegistry
from galaxy.model.triggers.update_audit_table import install as install_timestamp_triggers
from .. import MockObjectStore

if TYPE_CHECKING:
    from sqlalchemy.engine import Engine


@pytest.fixture(scope="module")
def db_url() -> str:
    """
    By default, use an in-memory database.
    To overwrite, add this fixture with a new db url to a test module.
    """
    return "sqlite:///:memory:"


@pytest.fixture(scope="module")
def engine(db_url: str) -> "Engine":
    return create_engine(db_url)


@pytest.fixture
def session(engine: "Engine") -> Session:
    session = Session(engine)
    # For sqlite, we need to explicitly enable foreign key constraints.
    if engine.name == "sqlite":
        session.execute(text("PRAGMA foreign_keys = ON;"))
    return session


@pytest.fixture(autouse=True, scope="module")
def init_database(engine: "Engine") -> None:
    """Create database objects."""
    m.mapper_registry.metadata.create_all(engine)
    install_timestamp_triggers(engine)


@pytest.fixture(autouse=True, scope="module")
def init_object_store() -> None:
    m.Dataset.object_store = MockObjectStore()  # type: ignore[assignment]


@pytest.fixture(autouse=True, scope="module")
def init_datatypes() -> None:
    datatypes_registry = DatatypesRegistry()
    datatypes_registry.load_datatypes()
    m.set_datatypes_registry(datatypes_registry)


@pytest.fixture(autouse=True)
def clear_database(engine: "Engine", session) -> "Generator":
    """Delete all rows from all tables. Called after each test."""
    yield

    # If a test left an open transaction, rollback to prevent database locking.
    if session.in_transaction():
        session.rollback()

    with engine.connect() as conn:
        if engine.name == "sqlite":
            conn.execute(text("PRAGMA foreign_keys = OFF;"))
            for table in m.mapper_registry.metadata.tables:
                conn.execute(text(f"DELETE FROM {table}"))
        else:
            # For postgres, we can disable foreign key constraints with this statement:
            #   conn.execute(text(f"ALTER TABLE {table} DISABLE TRIGGER ALL"))
            # However, unless running as superuser, this will raise an error when trying
            #   to disable a system trigger. Disabling USER triggers instead of ALL
            #   won't work because the USER option excludes foreign key constraints.
            # The following is an alternative: we do multiple passes until all tables have been cleared:
            to_delete = list(m.mapper_registry.metadata.tables)
            failed = []
            while to_delete:
                for table in to_delete:
                    try:
                        conn.execute(text(f"DELETE FROM {table}"))
                    except IntegrityError:
                        failed.append(table)
                        conn.rollback()
                to_delete, failed = failed, []

        conn.commit()
