import os
from typing import Union

import alembic
import pytest
from alembic.config import Config
from sqlalchemy import (
    MetaData,
    text,
)

import galaxy.model.migrations.scripts
from galaxy.model import migrations
from galaxy.model.database_utils import database_exists
from galaxy.model.migrations import (
    AlembicManager,
    DatabaseStateVerifier,
    get_last_sqlalchemymigrate_version,
    GXY,
    scripts,
    SQLALCHEMYMIGRATE_LAST_VERSION_GXY,
    SQLALCHEMYMIGRATE_LAST_VERSION_TSI,
    TSI,
    verify_databases,
)
from galaxy.model.migrations.base import (
    DatabaseStateCache,
    listify,
    load_metadata,
    metadata_contains_only_kombu_tables,
    SQLALCHEMYMIGRATE_TABLE,
)
from galaxy.model.migrations.exceptions import (
    IncorrectSAMigrateVersionError,
    NoVersionTableError,
    OutdatedDatabaseError,
    SAMigrateError,
)
from galaxy.model.migrations.scripts import LegacyManageDb
from galaxy.model.unittest_utils.model_testing_utils import (  # noqa: F401  (url_factory is a fixture we have to import explicitly)
    create_and_drop_database,
    disposing_engine,
    drop_existing_database,
    url_factory,
)
from galaxy.util.resources import (
    as_file,
    resource_path,
)

# Revision numbers from test versions directories
GXY_REVISION_0 = "62695fac6cc0"  # oldest/base
GXY_REVISION_1 = "2e8a580bc79a"
GXY_REVISION_2 = "e02cef55763c"  # current/head
TSI_REVISION_0 = "1bceec30363a"  # oldest/base
TSI_REVISION_1 = "8364ef1cab05"
TSI_REVISION_2 = "0e28bf2fb7b5"  # current/head


class TestAlembicManager:
    def test_is_at_revision__one_head_one_revision(self, url_factory):  # noqa: F811
        # Use case: Check if separate tsi database is at a given revision.
        db_url = url_factory()
        with create_and_drop_database(db_url):
            with disposing_engine(db_url) as engine:
                revision = GXY_REVISION_0
                assert not AlembicManagerForTests.is_at_revision(engine, revision)
                am = AlembicManagerForTests(engine)
                am.stamp_revision(revision)
                assert AlembicManagerForTests.is_at_revision(engine, revision)

    def test_is_at_revision__two_heads_one_revision(self, url_factory):  # noqa: F811
        # Use case: Check if combined gxy and tsi database is at a given gxy revision.
        db_url = url_factory()
        with create_and_drop_database(db_url):
            with disposing_engine(db_url) as engine:
                revision = GXY_REVISION_0
                revisions = [GXY_REVISION_0, TSI_REVISION_0]
                assert not AlembicManagerForTests.is_at_revision(engine, revision)
                am = AlembicManagerForTests(engine)
                am.stamp_revision(revisions)
                assert AlembicManagerForTests.is_at_revision(engine, revision)

    def test_is_at_revision__two_heads_two_revisions(self, url_factory):  # noqa: F811
        # Use case: Check if combined gxy and tsi database is at given gxy and tsi revisions.
        db_url = url_factory()
        with create_and_drop_database(db_url):
            with disposing_engine(db_url) as engine:
                revisions = [GXY_REVISION_0, TSI_REVISION_0]
                assert not AlembicManagerForTests.is_at_revision(engine, revisions)
                am = AlembicManagerForTests(engine)
                am.stamp_revision(revisions)
                assert AlembicManagerForTests.is_at_revision(engine, revisions)

    def test_is_up_to_date_single_revision(self, url_factory):  # noqa: F811
        db_url = url_factory()
        with create_and_drop_database(db_url):
            with disposing_engine(db_url) as engine:
                model = GXY
                am = AlembicManagerForTests(engine)
                assert not am.is_up_to_date(model)
                am.stamp_revision(GXY_REVISION_1)
                assert not am.is_up_to_date(model)
                am.stamp_revision(GXY_REVISION_2)
                assert am.is_up_to_date(model)

    def test_not_is_up_to_date_wrong_model(self, url_factory):  # noqa: F811
        db_url = url_factory()
        with create_and_drop_database(db_url):
            with disposing_engine(db_url) as engine:
                am = AlembicManagerForTests(engine)
                assert not am.is_up_to_date(GXY)
                assert not am.is_up_to_date(TSI)
                am.stamp_revision(GXY_REVISION_2)
                assert am.is_up_to_date(GXY)
                assert not am.is_up_to_date(TSI)

    def test_is_up_to_date_multiple_revisions(self, url_factory):  # noqa: F811
        db_url = url_factory()
        with create_and_drop_database(db_url):
            with disposing_engine(db_url) as engine:
                am = AlembicManagerForTests(engine)
                assert not am.is_up_to_date(GXY)  # False: no head revisions in database
                am.stamp_revision([GXY_REVISION_2, TSI_REVISION_2])
                assert am.is_up_to_date(GXY)  # True: both are up-to-date
                assert am.is_up_to_date(TSI)  # True: both are up-to-date

    def test_is_not_up_to_date_multiple_revisions_both(self, url_factory):  # noqa: F811
        db_url = url_factory()
        with create_and_drop_database(db_url):
            with disposing_engine(db_url) as engine:
                am = AlembicManagerForTests(engine)
                am.stamp_revision([GXY_REVISION_1, TSI_REVISION_1])
                assert not am.is_up_to_date(GXY)  # False: both are not up-to-date
                assert not am.is_up_to_date(TSI)  # False: both are not up-to-date

    def test_is_not_up_to_date_multiple_revisions_one(self, url_factory):  # noqa: F811
        db_url = url_factory()
        with create_and_drop_database(db_url):
            with disposing_engine(db_url) as engine:
                am = AlembicManagerForTests(engine)
                am.stamp_revision([GXY_REVISION_2, TSI_REVISION_1])
                assert am.is_up_to_date(GXY)  # True
                assert not am.is_up_to_date(TSI)  # False: only one is up-to-date

    def test_is_under_version_control(self, url_factory):  # noqa: F811
        db_url = url_factory()
        with create_and_drop_database(db_url):
            with disposing_engine(db_url) as engine:
                am = AlembicManagerForTests(engine)
                assert not am.is_under_version_control(GXY)
                assert not am.is_under_version_control(TSI)
                am.stamp_revision(GXY_REVISION_0)
                assert am.is_under_version_control(GXY)
                assert not am.is_under_version_control(TSI)
                am.stamp_revision(TSI_REVISION_0)
                assert am.is_under_version_control(GXY)
                assert am.is_under_version_control(TSI)

    def test_get_revision_raises_error_if_revision_not_found(self, url_factory):  # noqa: F811
        db_url = url_factory()
        with create_and_drop_database(db_url):
            with disposing_engine(db_url) as engine:
                am = AlembicManagerForTests(engine)
                with pytest.raises(alembic.util.exc.CommandError):
                    am._get_revision("invalid")

    def test_get_model_db_head(self, url_factory):  # noqa: F811
        db_url = url_factory()
        with create_and_drop_database(db_url):
            with disposing_engine(db_url) as engine:
                am = AlembicManagerForTests(engine)
                revisions = [GXY_REVISION_1, TSI_REVISION_2]
                am.stamp_revision(revisions)
                db_head = am.get_model_db_head(GXY)
                assert db_head == GXY_REVISION_1  # We stamped the db with this GXY revision

    def test_get_model_script_head(self, url_factory):  # noqa: F811
        db_url = url_factory()
        with create_and_drop_database(db_url):
            with disposing_engine(db_url) as engine:
                am = AlembicManagerForTests(engine)
                script_head = am.get_model_script_head(GXY)
                assert script_head == GXY_REVISION_2  # That's the latest GXY revision in our script directory


class TestDatabaseStateCache:
    def test_is_empty(self, url_factory, metadata_state1_gxy):  # noqa: F811
        db_url, metadata = url_factory(), metadata_state1_gxy
        with create_and_drop_database(db_url):
            with disposing_engine(db_url) as engine:
                assert DatabaseStateCache(engine).is_database_empty()
                with engine.begin() as conn:
                    metadata.create_all(bind=conn)
                assert not DatabaseStateCache(engine).is_database_empty()

    def test_has_alembic_version_table(self, url_factory, metadata_state4_gxy):  # noqa: F811
        db_url, metadata = url_factory(), metadata_state4_gxy
        with create_and_drop_database(db_url):
            with disposing_engine(db_url) as engine:
                assert not DatabaseStateCache(engine).has_alembic_version_table()
                with engine.begin() as conn:
                    metadata.create_all(bind=conn)
                assert DatabaseStateCache(engine).has_alembic_version_table()

    def test_has_sqlalchemymigrate_version_table(self, url_factory, metadata_state2_gxy):  # noqa: F811
        db_url, metadata = url_factory(), metadata_state2_gxy
        with create_and_drop_database(db_url):
            with disposing_engine(db_url) as engine:
                assert not DatabaseStateCache(engine).has_sqlalchemymigrate_version_table()
                with engine.begin() as conn:
                    metadata.create_all(bind=conn)
                assert DatabaseStateCache(engine).has_sqlalchemymigrate_version_table()

    def test_is_last_sqlalchemymigrate_version(self, url_factory, metadata_state2_gxy):  # noqa: F811
        db_url = url_factory()
        with create_and_drop_database(db_url):
            with disposing_engine(db_url) as engine:
                load_metadata(metadata_state2_gxy, engine)
                load_sqlalchemymigrate_version(db_url, SQLALCHEMYMIGRATE_LAST_VERSION_GXY - 1)
                assert not DatabaseStateCache(engine).is_last_sqlalchemymigrate_version(
                    SQLALCHEMYMIGRATE_LAST_VERSION_GXY
                )
                load_sqlalchemymigrate_version(db_url, SQLALCHEMYMIGRATE_LAST_VERSION_GXY)
                assert DatabaseStateCache(engine).is_last_sqlalchemymigrate_version(SQLALCHEMYMIGRATE_LAST_VERSION_GXY)

    def test_only_contains_kombu_tables(self, url_factory, metadata_state0kombu):  # noqa: F811
        db_url, metadata = url_factory(), metadata_state0kombu
        with create_and_drop_database(db_url):
            with disposing_engine(db_url) as engine:
                assert DatabaseStateCache(engine).is_database_empty()
                with engine.begin() as conn:
                    metadata.create_all(bind=conn)
                assert not DatabaseStateCache(engine).is_database_empty()
                assert DatabaseStateCache(engine).contains_only_kombu_tables()


# Database fixture tests


class TestDatabaseFixtures:
    # Verify that database fixtures have the expected state.
    #
    # The fixtures of the form `db_state#_[gxy|tsi]` are urls that point
    # to databases that HAVE BEEN CREATED. Thus, we are not wrapping them here
    # in the `create_and_drop_database` context manager: they are wrapped already.
    class TestState1:
        def test_database_gxy(self, db_state1_gxy, metadata_state1_gxy):
            self.verify_state(db_state1_gxy, metadata_state1_gxy)

        def test_database_tsi(self, db_state1_tsi, metadata_state1_tsi):
            self.verify_state(db_state1_tsi, metadata_state1_tsi)

        def test_database_combined(self, db_state1_combined, metadata_state1_combined):
            self.verify_state(db_state1_combined, metadata_state1_combined)

        def verify_state(self, db_url, metadata):
            assert is_metadata_loaded(db_url, metadata)
            with disposing_engine(db_url) as engine:
                db = DatabaseStateCache(engine)
                assert not db.has_sqlalchemymigrate_version_table()
                assert not db.has_alembic_version_table()

    class TestState2:
        def test_database_gxy(self, db_state2_gxy, metadata_state2_gxy):
            self.verify_state(db_state2_gxy, metadata_state2_gxy)

        def test_database_tsi(self, db_state2_tsi, metadata_state2_tsi):
            self.verify_state(db_state2_tsi, metadata_state2_tsi)

        def test_database_combined(self, db_state2_combined, metadata_state2_combined):
            self.verify_state(db_state2_combined, metadata_state2_combined)

        def verify_state(self, db_url, metadata):
            assert is_metadata_loaded(db_url, metadata)
            with disposing_engine(db_url) as engine:
                db = DatabaseStateCache(engine)
                assert db.has_sqlalchemymigrate_version_table()
                assert not db.is_last_sqlalchemymigrate_version(SQLALCHEMYMIGRATE_LAST_VERSION_GXY)
                assert not db.is_last_sqlalchemymigrate_version(SQLALCHEMYMIGRATE_LAST_VERSION_TSI)
                assert not db.has_alembic_version_table()

    class TestState3:
        def test_database_gxy(self, db_state3_gxy, metadata_state3_gxy):
            self.verify_state(db_state3_gxy, metadata_state3_gxy, SQLALCHEMYMIGRATE_LAST_VERSION_GXY)

        def test_database_tsi(self, db_state3_tsi, metadata_state3_tsi):
            self.verify_state(db_state3_tsi, metadata_state3_tsi, SQLALCHEMYMIGRATE_LAST_VERSION_TSI)

        def test_database_combined(self, db_state3_combined, metadata_state3_combined):
            self.verify_state(db_state3_combined, metadata_state3_combined, SQLALCHEMYMIGRATE_LAST_VERSION_GXY)

        def verify_state(self, db_url, metadata, last_version):
            assert is_metadata_loaded(db_url, metadata)
            with disposing_engine(db_url) as engine:
                db = DatabaseStateCache(engine)
                assert db.has_sqlalchemymigrate_version_table()
                assert db.is_last_sqlalchemymigrate_version(last_version)
                assert not db.has_alembic_version_table()

    class TestState4:
        def test_database_gxy(self, db_state4_gxy, metadata_state4_gxy):
            self.verify_state(db_state4_gxy, metadata_state4_gxy, GXY_REVISION_0, SQLALCHEMYMIGRATE_LAST_VERSION_GXY)

        def test_database_tsi(self, db_state4_tsi, metadata_state4_tsi):
            self.verify_state(db_state4_tsi, metadata_state4_tsi, TSI_REVISION_0, SQLALCHEMYMIGRATE_LAST_VERSION_TSI)

        def test_database_combined(self, db_state4_combined, metadata_state4_combined):
            self.verify_state(
                db_state4_combined,
                metadata_state4_combined,
                [GXY_REVISION_0, TSI_REVISION_0],
                SQLALCHEMYMIGRATE_LAST_VERSION_GXY,
            )

        def verify_state(self, db_url, metadata, revision, last_version):
            assert is_metadata_loaded(db_url, metadata)
            with disposing_engine(db_url) as engine:
                db = DatabaseStateCache(engine)
                assert db.has_sqlalchemymigrate_version_table()
                assert db.is_last_sqlalchemymigrate_version(last_version)
                assert db.has_alembic_version_table()
                assert AlembicManagerForTests.is_at_revision(engine, revision)

    class TestState5:
        def test_database_gxy(self, db_state5_gxy, metadata_state5_gxy):
            self.verify_state(db_state5_gxy, metadata_state5_gxy, GXY_REVISION_1)

        def test_database_tsi(self, db_state5_tsi, metadata_state5_tsi):
            self.verify_state(db_state5_tsi, metadata_state5_tsi, TSI_REVISION_1)

        def test_database_combined(self, db_state5_combined, metadata_state5_combined):
            self.verify_state(db_state5_combined, metadata_state5_combined, [GXY_REVISION_1, TSI_REVISION_1])

        def verify_state(self, db_url, metadata, revision):
            assert is_metadata_loaded(db_url, metadata)
            with disposing_engine(db_url) as engine:
                db = DatabaseStateCache(engine)
                assert not db.has_sqlalchemymigrate_version_table()
                assert db.has_alembic_version_table()
                assert AlembicManagerForTests.is_at_revision(engine, revision)

    class TestState6:
        def test_database_gxy(self, db_state6_gxy, metadata_state6_gxy):
            self.verify_state(db_state6_gxy, metadata_state6_gxy, GXY_REVISION_2)

        def test_database_tsi(self, db_state6_tsi, metadata_state6_tsi):
            self.verify_state(db_state6_tsi, metadata_state6_tsi, TSI_REVISION_2)

        def test_database_combined(self, db_state6_combined, metadata_state6_combined):
            self.verify_state(db_state6_combined, metadata_state6_combined, [GXY_REVISION_2, TSI_REVISION_2])

        def verify_state(self, db_url, metadata, revision):
            assert is_metadata_loaded(db_url, metadata)
            with disposing_engine(db_url) as engine:
                db = DatabaseStateCache(engine)
                assert not db.has_sqlalchemymigrate_version_table()
                assert db.has_alembic_version_table()

    class TestState6GxyState3TsiNoSam:
        def test_database_combined(self, db_state6_gxy_state3_tsi_no_sam, metadata_state6_gxy_state3_tsi_no_sam):
            db_url = db_state6_gxy_state3_tsi_no_sam
            metadata = metadata_state6_gxy_state3_tsi_no_sam
            assert is_metadata_loaded(db_url, metadata)
            with disposing_engine(db_url) as engine:
                db = DatabaseStateCache(engine)
                assert not db.has_sqlalchemymigrate_version_table()
                assert db.has_alembic_version_table()
                assert AlembicManagerForTests.is_at_revision(engine, GXY_REVISION_2)


def _verify_databases(gxy_engine, tsi_engine=None):
    verify_databases(gxy_engine, None, None, tsi_engine, None, None, False)


class TestDatabaseStates:
    # Tests of primary function under different scenarios and database state.

    class TestNoState:
        # Initial state: database does not exist.
        # Expect: database created, initialized, versioned w/alembic.
        # (we use `metadata_state6_{gxy|tsi|combined}` for final database schema)
        def test_combined_database(self, url_factory, metadata_state6_combined):  # noqa: F811
            db_url = url_factory()
            with drop_existing_database(db_url):
                assert not database_exists(db_url)
                with disposing_engine(db_url) as engine:
                    _verify_databases(engine)
                    assert database_is_up_to_date(db_url, metadata_state6_combined, GXY)
                    assert database_is_up_to_date(db_url, metadata_state6_combined, TSI)

        def test_separate_databases(self, url_factory, metadata_state6_gxy, metadata_state6_tsi):  # noqa: F811
            db1_url, db2_url = url_factory(), url_factory()
            assert not database_exists(db1_url)
            assert not database_exists(db2_url)
            with drop_existing_database(db1_url), drop_existing_database(db2_url):
                with disposing_engine(db1_url) as engine1, disposing_engine(db2_url) as engine2:
                    _verify_databases(engine1, engine2)
                    assert database_is_up_to_date(db1_url, metadata_state6_gxy, GXY)
                    assert database_is_up_to_date(db2_url, metadata_state6_tsi, TSI)

    class TestState0:
        # Initial state: database is empty.
        # Expect: database created, initialized, versioned w/alembic.
        def test_combined_database(self, url_factory, metadata_state6_combined):  # noqa: F811
            db_url = url_factory()
            with create_and_drop_database(db_url):
                with disposing_engine(db_url) as engine:
                    assert database_exists(db_url)
                    assert database_is_empty_or_contains_kombu_tables(db_url)
                    _verify_databases(engine)
                    assert database_is_up_to_date(db_url, metadata_state6_combined, GXY)
                    assert database_is_up_to_date(db_url, metadata_state6_combined, TSI)

        def test_separate_databases(self, url_factory, metadata_state6_gxy, metadata_state6_tsi):  # noqa: F811
            db1_url, db2_url = url_factory(), url_factory()
            with create_and_drop_database(db1_url), create_and_drop_database(db2_url):
                with disposing_engine(db1_url) as engine1, disposing_engine(db2_url) as engine2:
                    assert database_exists(db1_url)
                    assert database_exists(db2_url)
                    assert database_is_empty_or_contains_kombu_tables(db1_url)
                    assert database_is_empty_or_contains_kombu_tables(db2_url)
                    _verify_databases(engine1, engine2)
                    assert database_is_up_to_date(db1_url, metadata_state6_gxy, GXY)
                    assert database_is_up_to_date(db2_url, metadata_state6_tsi, TSI)

    class TestState0Kombu:
        # Initial state: database contains kombu_message anad kombu_queue tables.
        # (ref: https://github.com/galaxyproject/galaxy/issues/13689)
        # Expect: database created, initialized, versioned w/alembic (same as empty db in state0).
        def test_combined_database(self, db_state0kombu, metadata_state6_combined):  # noqa: F811
            db_url = db_state0kombu
            with disposing_engine(db_url) as engine:
                assert database_exists(db_url)
                assert database_is_empty_or_contains_kombu_tables(db_url)
                _verify_databases(engine)
                assert database_is_up_to_date(db_url, metadata_state6_combined, GXY)
                assert database_is_up_to_date(db_url, metadata_state6_combined, TSI)

    class TestState1:
        # Initial state: non-empty database, no version table.
        # Expect: fail with appropriate message.
        def test_combined_database(self, db_state1_combined):
            with pytest.raises(NoVersionTableError):
                with disposing_engine(db_state1_combined) as engine:
                    _verify_databases(engine)

        def test_separate_databases_gxy_raises_error(self, db_state1_gxy, db_state6_tsi):
            with pytest.raises(NoVersionTableError):
                with disposing_engine(db_state1_gxy) as engine1, disposing_engine(db_state6_tsi) as engine2:
                    _verify_databases(engine1, engine2)

        def test_separate_databases_tsi_raises_error(self, db_state6_gxy, db_state1_tsi):
            with pytest.raises(NoVersionTableError):
                with disposing_engine(db_state6_gxy) as engine1, disposing_engine(db_state1_tsi) as engine2:
                    _verify_databases(engine1, engine2)

    class TestState2:
        # Initial state: non-empty database, SQLAlchemy Migrate version table present; however,
        # the stored version is not the latest after which we could transition to Alembic.
        # Expect: fail with appropriate message.
        def test_combined_database(self, db_state2_combined):
            with pytest.raises(IncorrectSAMigrateVersionError):
                with disposing_engine(db_state2_combined) as engine:
                    _verify_databases(engine)

        def test_separate_databases_gxy_raises_error(self, db_state2_gxy, db_state6_tsi):
            with pytest.raises(IncorrectSAMigrateVersionError):
                with disposing_engine(db_state2_gxy) as engine1, disposing_engine(db_state6_tsi) as engine2:
                    _verify_databases(engine1, engine2)

        def test_separate_databases_tsi_raises_error(self, db_state6_gxy, db_state2_tsi):
            with pytest.raises(IncorrectSAMigrateVersionError):
                with disposing_engine(db_state6_gxy) as engine1, disposing_engine(db_state2_tsi) as engine2:
                    _verify_databases(engine1, engine2)

    class TestState3:
        # Initial state: non-empty database, SQLAlchemy Migrate version table contains latest version
        # under SQLAlchemy Migrate.
        # Expect:
        # a) auto-migrate enabled: alembic version table added, database upgraded to current version.
        # b) auto-migrate disabled: fail with appropriate message.
        def test_combined_database_automigrate(
            self,
            db_state3_combined,
            metadata_state6_combined,
            set_automigrate,
        ):
            db_url = db_state3_combined
            with disposing_engine(db_state3_combined) as engine:
                _verify_databases(engine)
                assert database_is_up_to_date(db_url, metadata_state6_combined, GXY)
                assert database_is_up_to_date(db_url, metadata_state6_combined, TSI)

        def test_separate_databases_automigrate(
            self,
            db_state3_gxy,
            db_state3_tsi,
            metadata_state6_gxy,
            metadata_state6_tsi,
            set_automigrate,
        ):
            db1_url, db2_url = db_state3_gxy, db_state3_tsi
            with disposing_engine(db1_url) as engine1, disposing_engine(db2_url) as engine2:
                _verify_databases(engine1, engine2)
                assert database_is_up_to_date(db1_url, metadata_state6_gxy, GXY)
                assert database_is_up_to_date(db2_url, metadata_state6_tsi, TSI)

        def test_combined_database_no_automigrate(self, db_state3_combined):
            with pytest.raises(SAMigrateError):
                with disposing_engine(db_state3_combined) as engine:
                    _verify_databases(engine)

        def test_separate_databases_no_automigrate_gxy_raises_error(self, db_state3_gxy, db_state6_tsi):
            with pytest.raises(SAMigrateError):
                with disposing_engine(db_state3_gxy) as engine1, disposing_engine(db_state6_tsi) as engine2:
                    _verify_databases(engine1, engine2)

        def test_separate_databases_no_automigrate_tsi_raises_error(self, db_state6_gxy, db_state3_tsi):
            with pytest.raises(SAMigrateError):
                with disposing_engine(db_state6_gxy) as engine1, disposing_engine(db_state3_tsi) as engine2:
                    _verify_databases(engine1, engine2)

    class TestState4:
        # Initial state: non-empty database, SQLAlchemy Migrate version table present, Alembic version table present.
        # Oldest Alembic revision.
        # Expect:
        # a) auto-migrate enabled: database upgraded to current version.
        # b) auto-migrate disabled: fail with appropriate message.
        def test_combined_database_automigrate(
            self,
            db_state4_combined,
            metadata_state6_combined,
            set_automigrate,
        ):
            db_url = db_state4_combined
            with disposing_engine(db_url) as engine:
                _verify_databases(engine)
                assert database_is_up_to_date(db_url, metadata_state6_combined, GXY)
                assert database_is_up_to_date(db_url, metadata_state6_combined, TSI)

        def test_separate_databases_automigrate(
            self,
            db_state4_gxy,
            db_state4_tsi,
            metadata_state6_gxy,
            metadata_state6_tsi,
            set_automigrate,
        ):
            db1_url, db2_url = db_state4_gxy, db_state4_tsi
            with disposing_engine(db1_url) as engine1, disposing_engine(db2_url) as engine2:
                _verify_databases(engine1, engine2)
                assert database_is_up_to_date(db1_url, metadata_state6_gxy, GXY)
                assert database_is_up_to_date(db2_url, metadata_state6_tsi, TSI)

        def test_combined_database_no_automigrate(self, db_state4_combined):
            with pytest.raises(OutdatedDatabaseError):
                with disposing_engine(db_state4_combined) as engine:
                    _verify_databases(engine)

        def test_separate_databases_no_automigrate_gxy_raises_error(self, db_state4_gxy, db_state6_tsi):
            with pytest.raises(OutdatedDatabaseError):
                with disposing_engine(db_state4_gxy) as engine1, disposing_engine(db_state6_tsi) as engine2:
                    _verify_databases(engine1, engine2)

        def test_separate_databases_no_automigrate_tsi_raises_error(self, db_state6_gxy, db_state4_tsi):
            with pytest.raises(OutdatedDatabaseError):
                with disposing_engine(db_state6_gxy) as engine1, disposing_engine(db_state4_tsi) as engine2:
                    _verify_databases(engine1, engine2)

    class TestState5:
        # Initial state: non-empty database, Alembic version table present.
        # Oldest Alembic revision that does not include SQLAlchemy Migrate version table.
        # Expect:
        # a) auto-migrate enabled: database upgraded to current version.
        # b) auto-migrate disabled: fail with appropriate message.
        def test_combined_database_automigrate(self, db_state5_combined, metadata_state6_combined, set_automigrate):
            db_url = db_state5_combined
            with disposing_engine(db_url) as engine:
                _verify_databases(engine)
                assert database_is_up_to_date(db_url, metadata_state6_combined, GXY)
                assert database_is_up_to_date(db_url, metadata_state6_combined, TSI)

        def test_separate_databases_automigrate(
            self, db_state5_gxy, db_state5_tsi, metadata_state6_gxy, metadata_state6_tsi, set_automigrate
        ):
            db1_url, db2_url = db_state5_gxy, db_state5_tsi
            with disposing_engine(db1_url) as engine1, disposing_engine(db2_url) as engine2:
                _verify_databases(engine1, engine2)
                assert database_is_up_to_date(db1_url, metadata_state6_gxy, GXY)
                assert database_is_up_to_date(db2_url, metadata_state6_tsi, TSI)

        def test_combined_database_no_automigrate(self, db_state5_combined):
            with pytest.raises(OutdatedDatabaseError):
                with disposing_engine(db_state5_combined) as engine:
                    _verify_databases(engine)

        def test_separate_databases_no_automigrate_gxy_raises_error(self, db_state5_gxy, db_state6_tsi):
            with pytest.raises(OutdatedDatabaseError):
                with disposing_engine(db_state5_gxy) as engine1, disposing_engine(db_state6_tsi) as engine2:
                    _verify_databases(engine1, engine2)

        def test_separate_databases_no_automigrate_tsi_raises_error(self, db_state6_gxy, db_state5_tsi):
            with pytest.raises(OutdatedDatabaseError):
                with disposing_engine(db_state6_gxy) as engine1, disposing_engine(db_state5_tsi) as engine2:
                    _verify_databases(engine1, engine2)

    class TestState6:
        # Initial state: non-empty database, Alembic version table present, database up-to-date.
        # Expect: do nothing.
        def test_combined_database(self, db_state6_combined, metadata_state6_combined):
            db_url = db_state6_combined
            with disposing_engine(db_url) as engine:
                _verify_databases(engine)
                assert database_is_up_to_date(db_url, metadata_state6_combined, GXY)
                assert database_is_up_to_date(db_url, metadata_state6_combined, TSI)

        def test_separate_databases(self, db_state6_gxy, db_state6_tsi, metadata_state6_gxy, metadata_state6_tsi):
            db1_url, db2_url = db_state6_gxy, db_state6_tsi
            with disposing_engine(db1_url) as engine1, disposing_engine(db2_url) as engine2:
                _verify_databases(engine1, engine2)
                assert database_is_up_to_date(db1_url, metadata_state6_gxy, GXY)
                assert database_is_up_to_date(db2_url, metadata_state6_tsi, TSI)

    class TestPartiallyUpgradedCombinedDatabase:
        # This only applies to an EXISTING, NONEMPTY database, that is COMBINED (both GXY and TSI models
        # are in the same database).
        # This covers several edge cases when the GXY model is up-to-date, but the TSI model is not.
        # This may happen for a variety of reasons:
        # - a database is being upgraded to Alembic: the GXY model is processed first,
        #   so GXY will become up-to-date before TSI;
        # - a database is changed to "combined" (via setting install_database_connection): so TSI has not
        #   been initialized in this database.
        # - a glitch in the Matrix, an act of god, etc.
        # These tests verify the system's handling of such cases for 2 TSI states:
        # - Database has no TSI tables: assume new TSI install.
        # - Database has some TSI tables: assume TSI is at last pre-alembic state and can be migrated.
        # (We expect a severly outdated TSI model to have been upgraded manually together with the GXY model;
        # for there is no reasonable way to determine its state automatically without a version table.)
        #
        # Initial state for all tests:
        # - combined database
        # - GXY model: up-to-date
        # - TSI model: not in Alembic version table
        def test_case1_automigrate(self, db_state6_gxy, metadata_state6_combined, set_automigrate):
            # Initial state:
            # - no TSI tables exist
            # - auto-migrate enabled
            # Expect: database is up-to-date
            db_url = db_state6_gxy
            with disposing_engine(db_url) as engine:
                _verify_databases(engine)
                assert database_is_up_to_date(db_url, metadata_state6_combined, GXY)
                assert database_is_up_to_date(db_url, metadata_state6_combined, TSI)

        def test_case1_no_automigrate(self, db_state6_gxy, metadata_state6_combined):
            # Initial state:
            # - no TSI tables exist
            # - auto-migrate not enabled
            # Expect: database is up-to-date (same as auto-migrate enabled)
            db_url = db_state6_gxy
            with disposing_engine(db_url) as engine:
                _verify_databases(engine)
                assert database_is_up_to_date(db_url, metadata_state6_combined, GXY)
                assert database_is_up_to_date(db_url, metadata_state6_combined, TSI)

        def test_case2_automigrate(self, db_state6_gxy_state3_tsi_no_sam, metadata_state6_combined, set_automigrate):
            # Initial state:
            # - all pre-alembic TSI tables exist
            # - auto-migrate enabled
            # Expect: database is up-to-date
            db_url = db_state6_gxy_state3_tsi_no_sam
            with disposing_engine(db_url) as engine:
                _verify_databases(engine)
                assert database_is_up_to_date(db_url, metadata_state6_combined, GXY)
                assert database_is_up_to_date(db_url, metadata_state6_combined, TSI)

        def test_case2_no_automigrate(self, db_state6_gxy_state3_tsi_no_sam):
            # Initial state:
            # - all pre-alembic TSI tables exist
            # - auto-migrate not enabled
            # Expect: fail with appropriate message
            db_url = db_state6_gxy_state3_tsi_no_sam
            with pytest.raises(OutdatedDatabaseError):
                with disposing_engine(db_url) as engine:
                    _verify_databases(engine)


# Test helpers + their tests, misc. fixtures


@pytest.fixture(autouse=True)  # always override AlembicManager
def set_create_additional(monkeypatch):
    monkeypatch.setattr(DatabaseStateVerifier, "_create_additional_database_objects", lambda *_: None)


@pytest.fixture
def set_automigrate(monkeypatch):
    monkeypatch.setattr(DatabaseStateVerifier, "is_auto_migrate", True)


@pytest.fixture(autouse=True)  # always override AlembicManager
def set_alembic_manager(monkeypatch):
    monkeypatch.setattr(migrations, "get_alembic_manager", lambda engine: AlembicManagerForTests(engine))


@pytest.fixture(autouse=True)  # always override gxy_metadata
def set_gxy_metadata(monkeypatch, metadata_state6_gxy):
    monkeypatch.setattr(migrations, "get_gxy_metadata", lambda: metadata_state6_gxy)


@pytest.fixture(autouse=True)  # always override tsi_metadata
def set_tsi_metadata(monkeypatch, metadata_state6_tsi):
    monkeypatch.setattr(migrations, "get_tsi_metadata", lambda: metadata_state6_tsi)


class AlembicManagerForTests(AlembicManager):
    def __init__(self, engine):
        path1, path2 = _get_paths_to_version_locations()
        config_dict = {"version_locations": f"{path1};{path2}"}
        super().__init__(engine, config_dict)


def _get_paths_to_version_locations():
    # One does not simply use a relative path for both tests and package tests.
    basepath = os.path.abspath(os.path.dirname(__file__))
    basepath = os.path.join(basepath, "testing_utils")
    basepath = os.path.join(basepath, "versions")
    path1 = os.path.join(basepath, "db1")
    path2 = os.path.join(basepath, "db2")
    return path1, path2


def load_sqlalchemymigrate_version(db_url, version):
    with disposing_engine(db_url) as engine:
        with engine.begin() as conn:
            sql_delete = text(f"delete from {SQLALCHEMYMIGRATE_TABLE}")  # there can be only 1 row
            sql_insert = text(f"insert into {SQLALCHEMYMIGRATE_TABLE} values('_', '_', {version})")
            conn.execute(sql_delete)
            conn.execute(sql_insert)


def test_load_sqlalchemymigrate_version(url_factory, metadata_state2_gxy):  # noqa F811
    db_url = url_factory()
    with create_and_drop_database(db_url):
        with disposing_engine(db_url) as engine:
            load_metadata(metadata_state2_gxy, engine)
            sql = text(f"select version from {SQLALCHEMYMIGRATE_TABLE}")
            version = 42
            with engine.begin() as conn:
                result = conn.execute(sql).scalar()
                assert result != version
                load_sqlalchemymigrate_version(db_url, version)
                result = conn.execute(sql).scalar()
                assert result == version


def test_get_last_sqlalchemymigrate_version():
    assert get_last_sqlalchemymigrate_version(GXY) == SQLALCHEMYMIGRATE_LAST_VERSION_GXY
    assert get_last_sqlalchemymigrate_version(TSI) == SQLALCHEMYMIGRATE_LAST_VERSION_TSI


def database_is_empty_or_contains_kombu_tables(db_url):
    """
    Database is empty or contains 2 tables: kombu_message, kombu_queue.
    (ref: https://github.com/galaxyproject/galaxy/issues/13689)
    """
    with disposing_engine(db_url) as engine:
        with engine.begin() as conn:
            metadata = MetaData()
            metadata.reflect(bind=conn)
            return not bool(metadata.tables) or metadata_contains_only_kombu_tables(metadata)


def test_database_is_empty(url_factory, metadata_state1_gxy):  # noqa F811
    db_url = url_factory()
    with create_and_drop_database(db_url):
        with disposing_engine(db_url) as engine:
            assert database_is_empty_or_contains_kombu_tables(db_url)
            load_metadata(metadata_state1_gxy, engine)
            assert not database_is_empty_or_contains_kombu_tables(db_url)


def test_database_with_kombu_is_empty(url_factory, metadata_state0kombu):  # noqa F811
    db_url = url_factory()
    with create_and_drop_database(db_url):
        with disposing_engine(db_url) as engine:
            assert database_is_empty_or_contains_kombu_tables(db_url)
            load_metadata(metadata_state0kombu, engine)
            assert database_is_empty_or_contains_kombu_tables(db_url)


def database_is_up_to_date(db_url, current_state_metadata, model):
    # True if the database at `db_url` has the `current_state_metadata` loaded,
    # and is up-to-date with respect to `model` (has the most recent Alembic revision).

    # NOTE: Ideally, we'd determine the current metadata based on the model. However, since
    # metadata is a fixture, it cannot be called directly, and instead has to be
    # passed as an argument. That's why we ensure that the passed metadata is current
    # (this guards againt an incorrect test).
    if model == GXY:
        current_tables = {"gxy_table1", "gxy_table2", "gxy_table3"}
    elif model == TSI:
        current_tables = {"tsi_table1", "tsi_table2", "tsi_table3"}
    is_metadata_current = current_tables <= set(current_state_metadata.tables)

    with disposing_engine(db_url) as engine:
        is_loaded = is_metadata_loaded(db_url, current_state_metadata)
        am = AlembicManagerForTests(engine)
        return is_metadata_current and is_loaded and am.is_up_to_date(model)


def test_database_is_up_to_date(url_factory, metadata_state6_gxy):  # noqa F811
    db_url, metadata = url_factory(), metadata_state6_gxy
    with create_and_drop_database(db_url):
        with disposing_engine(db_url) as engine:
            assert not database_is_up_to_date(db_url, metadata, GXY)
            load_metadata(metadata, engine)
            am = AlembicManagerForTests(engine)
            am.stamp_revision("heads")
            assert database_is_up_to_date(db_url, metadata, GXY)


def test_database_is_up_to_date_for_passed_model_only(url_factory, metadata_state6_gxy):  # noqa F811
    db_url, metadata = url_factory(), metadata_state6_gxy
    with create_and_drop_database(db_url):
        with disposing_engine(db_url) as engine:
            assert not database_is_up_to_date(db_url, metadata, GXY)
            assert not database_is_up_to_date(db_url, metadata, TSI)
            load_metadata(metadata, engine)
            am = AlembicManagerForTests(engine)
            am.stamp_revision("heads")
            assert database_is_up_to_date(db_url, metadata, GXY)
            assert not database_is_up_to_date(db_url, metadata, TSI)


def test_database_is_not_up_to_date_if_noncurrent_metadata_passed(url_factory, metadata_state5_gxy):  # noqa F811
    db_url, metadata = url_factory(), metadata_state5_gxy
    with create_and_drop_database(db_url):
        with disposing_engine(db_url) as engine:
            load_metadata(metadata, engine)
            am = AlembicManagerForTests(engine)
            am.stamp_revision("heads")
            assert not database_is_up_to_date(db_url, metadata, GXY)


def test_database_is_not_up_to_date_if_metadata_not_loaded(url_factory, metadata_state6_gxy):  # noqa F811
    db_url, metadata = url_factory(), metadata_state6_gxy
    with create_and_drop_database(db_url):
        with disposing_engine(db_url) as engine:
            am = AlembicManagerForTests(engine)
            am.stamp_revision("heads")
            assert not database_is_up_to_date(db_url, metadata, GXY)


def test_database_is_not_up_to_date_if_alembic_not_added(url_factory, metadata_state6_gxy):  # noqa F811
    db_url, metadata = url_factory(), metadata_state6_gxy
    with create_and_drop_database(db_url):
        with disposing_engine(db_url) as engine:
            load_metadata(metadata, engine)
            assert not database_is_up_to_date(db_url, metadata, GXY)


def is_metadata_loaded(db_url, metadata):
    # True if the set of tables from the up-to-date state metadata (state6)
    # is a subset of the metadata reflected from `db_url`.
    with disposing_engine(db_url) as engine:
        with engine.begin() as conn:
            db_metadata = MetaData()
            db_metadata.reflect(bind=conn)
            tables = _get_tablenames(metadata)
            return set(tables) <= set(db_metadata.tables)


def _get_tablenames(metadata):
    metadata = listify(metadata)
    tables = set()
    for md in metadata:
        tables |= set(md.tables)
    return tables


def test_is_metadata_loaded(url_factory, metadata_state1_gxy):  # noqa F811
    db_url, metadata = url_factory(), metadata_state1_gxy
    with create_and_drop_database(db_url):
        assert not is_metadata_loaded(db_url, metadata)
        with disposing_engine(db_url) as engine:
            with engine.begin() as conn:
                metadata.create_all(bind=conn)
        assert is_metadata_loaded(db_url, metadata)


def test_is_multiple_metadata_loaded(url_factory, metadata_state1_gxy, metadata_state1_tsi):  # noqa F811
    db_url = url_factory()
    metadata = [metadata_state1_gxy, metadata_state1_tsi]
    with create_and_drop_database(db_url):
        assert not is_metadata_loaded(db_url, metadata)
        with disposing_engine(db_url) as engine:
            with engine.begin() as conn:
                metadata_state1_gxy.create_all(bind=conn)
                metadata_state1_tsi.create_all(bind=conn)
        assert is_metadata_loaded(db_url, metadata)


def test_load_metadata(url_factory, metadata_state1_gxy):  # noqa F811
    db_url, metadata = url_factory(), metadata_state1_gxy
    with create_and_drop_database(db_url):
        with disposing_engine(db_url) as engine:
            assert not is_metadata_loaded(db_url, metadata)
            load_metadata(metadata, engine)
            assert is_metadata_loaded(db_url, metadata)


# Fixtures: databases loaded with a given state.
#
# Each state has 3 versions: gxy, tsi, combined (see fixtures/schemas.py)
#
# Each fixture is constructed as follows:
# 1. Create a new sqlite database url.
# 2. Pass database url with state metadata fixture to a `_setup_db_url_state{state#}` function.
# 3. Inside the function, create database and load any state-specific data.


# state 0 with kombu tables
@pytest.fixture
def db_state0kombu(url_factory, metadata_state0kombu):  # noqa F811
    yield from _setup_db_state0kombu(url_factory(), metadata_state0kombu)


def _setup_db_state0kombu(db_url, metadata):
    with create_and_drop_database(db_url):
        with disposing_engine(db_url) as engine:
            load_metadata(metadata, engine)
            yield db_url


# state 1
@pytest.fixture
def db_state1_gxy(url_factory, metadata_state1_gxy):  # noqa F811
    yield from _setup_db_state1(url_factory(), metadata_state1_gxy)


@pytest.fixture
def db_state1_tsi(url_factory, metadata_state1_tsi):  # noqa F811
    yield from _setup_db_state1(url_factory(), metadata_state1_tsi)


@pytest.fixture
def db_state1_combined(url_factory, metadata_state1_combined):  # noqa F811
    yield from _setup_db_state1(url_factory(), metadata_state1_combined)


def _setup_db_state1(db_url, metadata):
    with create_and_drop_database(db_url):
        with disposing_engine(db_url) as engine:
            load_metadata(metadata, engine)
            yield db_url


# state 2
@pytest.fixture
def db_state2_gxy(url_factory, metadata_state2_gxy):  # noqa F811
    yield from _setup_db_state2(url_factory(), metadata_state2_gxy, SQLALCHEMYMIGRATE_LAST_VERSION_GXY)


@pytest.fixture
def db_state2_tsi(url_factory, metadata_state2_tsi):  # noqa F811
    yield from _setup_db_state2(url_factory(), metadata_state2_tsi, SQLALCHEMYMIGRATE_LAST_VERSION_TSI)


@pytest.fixture
def db_state2_combined(url_factory, metadata_state2_combined):  # noqa F811
    yield from _setup_db_state2(url_factory(), metadata_state2_combined, SQLALCHEMYMIGRATE_LAST_VERSION_GXY)


def _setup_db_state2(db_url, metadata, last_version):
    with create_and_drop_database(db_url):
        with disposing_engine(db_url) as engine:
            load_metadata(metadata, engine)
            load_sqlalchemymigrate_version(db_url, last_version - 1)
            yield db_url


# state 3
@pytest.fixture
def db_state3_gxy(url_factory, metadata_state3_gxy):  # noqa F811
    yield from _setup_db_state3(url_factory(), metadata_state3_gxy, SQLALCHEMYMIGRATE_LAST_VERSION_GXY)


@pytest.fixture
def db_state3_tsi(url_factory, metadata_state3_tsi):  # noqa F811
    yield from _setup_db_state3(url_factory(), metadata_state3_tsi, SQLALCHEMYMIGRATE_LAST_VERSION_TSI)


@pytest.fixture
def db_state3_combined(url_factory, metadata_state3_combined):  # noqa F811
    # the SQLAlchemy Migrate version in a combined database is GXY (we only stored the TSI version
    # if the database was separate).
    yield from _setup_db_state3(url_factory(), metadata_state3_combined, SQLALCHEMYMIGRATE_LAST_VERSION_GXY)


def _setup_db_state3(db_url, metadata, last_version):
    with create_and_drop_database(db_url):
        with disposing_engine(db_url) as engine:
            load_metadata(metadata, engine)
            load_sqlalchemymigrate_version(db_url, last_version)
            yield db_url


# state 4
@pytest.fixture
def db_state4_gxy(url_factory, metadata_state4_gxy):  # noqa F811
    yield from _setup_db_state4(url_factory(), metadata_state4_gxy, SQLALCHEMYMIGRATE_LAST_VERSION_GXY, GXY)


@pytest.fixture
def db_state4_tsi(url_factory, metadata_state4_tsi):  # noqa F811
    yield from _setup_db_state4(url_factory(), metadata_state4_tsi, SQLALCHEMYMIGRATE_LAST_VERSION_TSI, TSI)


@pytest.fixture
def db_state4_combined(url_factory, metadata_state4_combined):  # noqa F811
    # the SQLAlchemy Migrate version in a combined database is GXY (we only stored the TSI version
    # if the database was separate).
    yield from _setup_db_state4(url_factory(), metadata_state4_combined, SQLALCHEMYMIGRATE_LAST_VERSION_GXY)


def _setup_db_state4(db_url, metadata, last_version, model=None):
    with create_and_drop_database(db_url):
        with disposing_engine(db_url) as engine:
            load_metadata(metadata, engine)
            load_sqlalchemymigrate_version(db_url, last_version)

            revisions: Union[str, list]
            if model == GXY:
                revisions = GXY_REVISION_0
            elif model == TSI:
                revisions = TSI_REVISION_0
            else:
                revisions = [GXY_REVISION_0, TSI_REVISION_0]
            am = AlembicManagerForTests(engine)
            am.stamp_revision(revisions)

            yield db_url


# state 5
@pytest.fixture
def db_state5_gxy(url_factory, metadata_state5_gxy):  # noqa F811
    yield from _setup_db_state5(url_factory(), metadata_state5_gxy, GXY)


@pytest.fixture
def db_state5_tsi(url_factory, metadata_state5_tsi):  # noqa F811
    yield from _setup_db_state5(url_factory(), metadata_state5_tsi, TSI)


@pytest.fixture
def db_state5_combined(url_factory, metadata_state5_combined):  # noqa F811
    yield from _setup_db_state5(url_factory(), metadata_state5_combined)


def _setup_db_state5(db_url, metadata, model=None):
    with create_and_drop_database(db_url):
        with disposing_engine(db_url) as engine:
            load_metadata(metadata, engine)

            revisions: Union[str, list]
            if model == GXY:
                revisions = GXY_REVISION_1
            elif model == TSI:
                revisions = TSI_REVISION_1
            else:
                revisions = [GXY_REVISION_1, TSI_REVISION_1]
            am = AlembicManagerForTests(engine)
            am.stamp_revision(revisions)

            yield db_url


# state 6
@pytest.fixture
def db_state6_gxy(url_factory, metadata_state6_gxy):  # noqa F811
    yield from _setup_db_state6(url_factory(), metadata_state6_gxy, GXY)


@pytest.fixture
def db_state6_tsi(url_factory, metadata_state6_tsi):  # noqa F811
    yield from _setup_db_state6(url_factory(), metadata_state6_tsi, TSI)


@pytest.fixture
def db_state6_combined(url_factory, metadata_state6_combined):  # noqa F811
    yield from _setup_db_state6(url_factory(), metadata_state6_combined)


def _setup_db_state6(db_url, metadata, model=None):
    with create_and_drop_database(db_url):
        with disposing_engine(db_url) as engine:
            load_metadata(metadata, engine)

            revisions: Union[str, list]
            if model == GXY:
                revisions = GXY_REVISION_2
            elif model == TSI:
                revisions = TSI_REVISION_2
            else:
                revisions = [GXY_REVISION_2, TSI_REVISION_2]
            am = AlembicManagerForTests(engine)
            am.stamp_revision(revisions)

            yield db_url


# state 6+3
@pytest.fixture
def db_state6_gxy_state3_tsi_no_sam(url_factory, metadata_state6_gxy_state3_tsi_no_sam):  # noqa F811
    db_url = url_factory()
    metadata = metadata_state6_gxy_state3_tsi_no_sam
    with create_and_drop_database(db_url):
        with disposing_engine(db_url) as engine:
            load_metadata(metadata, engine)
            am = AlembicManagerForTests(engine)
            am.stamp_revision(GXY_REVISION_2)
            yield db_url


# Test LegacyManageDb (used by script/manage_db.py)


@pytest.fixture(autouse=True)
def legacy_manage_db(monkeypatch):
    def get_alembic_cfg():
        traversable = resource_path("galaxy.model.migrations", "alembic.ini")
        with as_file(traversable) as path:
            config = Config(path)
            path1, path2 = _get_paths_to_version_locations()
            config.set_main_option("version_locations", f"{path1};{path2}")
            # config.set_main_option() calls config.file_config() which memoizes
            # the temporary file path from the as_file() contextmanager
        return config

    monkeypatch.setattr(galaxy.model.migrations.scripts, "get_alembic_cfg", get_alembic_cfg)


@pytest.fixture(autouse=True)
def set_db_urls(monkeypatch):
    # Do not try to access galaxy config; values not needed.
    def no_config_call(self):
        self.gxy_db_url = "a string"
        self.tsi_db_url = "a stirng"

    monkeypatch.setattr(LegacyManageDb, "_set_db_urls", no_config_call)


@pytest.fixture(autouse=True)  # always override AlembicManager
def set_alembic_manager2(monkeypatch):
    monkeypatch.setattr(scripts, "get_alembic_manager", lambda engine: AlembicManagerForTests(engine))


class TestLegacyManageDbScript:
    def test_get_gxy_version(self):
        mdb = LegacyManageDb()
        version = mdb.get_gxy_version()
        assert version == GXY_REVISION_2

    class TestState2:
        # Initial state: non-empty database, SQLAlchemy Migrate version table present; however,
        # the stored version is not the latest after which we could transition to Alembic.
        def test_get_gxy_db_version__state2__gxy_database(self, db_state2_gxy):
            # Expect: fail
            db_url = db_state2_gxy
            with pytest.raises(IncorrectSAMigrateVersionError):
                mdb = LegacyManageDb()
                mdb.get_gxy_db_version(db_url)

        def test_get_gxy_db_version__state2__combined_database(self, db_state2_combined):
            # Expect: fail
            db_url = db_state2_combined
            with pytest.raises(IncorrectSAMigrateVersionError):
                mdb = LegacyManageDb()
                mdb.get_gxy_db_version(db_url)

    class TestState3:
        # Initial state: non-empty database, SQLAlchemy Migrate version table contains latest version
        # under SQLAlchemy Migrate.
        def test_get_gxy_db_version___state3__gxy_database(self, db_state3_gxy):
            # Expect: return SQLALCHEMYMIGRATE_LAST_VERSION_GXY
            db_url = db_state3_gxy
            mdb = LegacyManageDb()
            version = mdb.get_gxy_db_version(db_url)
            assert version == SQLALCHEMYMIGRATE_LAST_VERSION_GXY

        def test_get_gxy_db_version___state3__combined_database(self, db_state3_combined):
            # Expect: return SQLALCHEMYMIGRATE_LAST_VERSION_GXY
            db_url = db_state3_combined
            mdb = LegacyManageDb()
            version = mdb.get_gxy_db_version(db_url)
            assert version == SQLALCHEMYMIGRATE_LAST_VERSION_GXY

        def test_upgrade__state3__combined_database(self, db_state3_combined, metadata_state6_combined):
            # Expect: upgrade to current version
            db_url = db_state3_combined
            mdb = LegacyManageDb()
            mdb.run_upgrade(db_url, db_url)
            assert database_is_up_to_date(db_url, metadata_state6_combined, GXY)
            assert database_is_up_to_date(db_url, metadata_state6_combined, TSI)

        def test_upgrade__state3__separate_databases(
            self, db_state3_gxy, db_state3_tsi, metadata_state6_gxy, metadata_state6_tsi
        ):
            # Expect: upgrade to current version
            db1_url, db2_url = db_state3_gxy, db_state3_tsi
            mdb = LegacyManageDb()
            mdb.run_upgrade(db1_url, db2_url)
            assert database_is_up_to_date(db1_url, metadata_state6_gxy, GXY)
            assert database_is_up_to_date(db2_url, metadata_state6_tsi, TSI)

    class TestState4:
        # Initial state: non-empty database, SQLAlchemy Migrate version table present, Alembic version table present.
        # Oldest Alembic revision.
        # Expect: return GXY_REVISION_0
        def test_get_gxy_db_version___state4__gxy_database(self, db_state4_gxy):
            db_url = db_state4_gxy
            mdb = LegacyManageDb()
            version = mdb.get_gxy_db_version(db_url)
            assert version == GXY_REVISION_0

        def test_get_gxy_db_version___state4__combined_database(self, db_state4_combined):
            db_url = db_state4_combined
            mdb = LegacyManageDb()
            version = mdb.get_gxy_db_version(db_url)
            assert version == GXY_REVISION_0

        def test_upgrade__state4__combined_database(self, db_state4_combined, metadata_state6_combined):
            # Expect: upgrade to current version
            db_url = db_state4_combined
            mdb = LegacyManageDb()
            mdb.run_upgrade(db_url, db_url)
            assert database_is_up_to_date(db_url, metadata_state6_combined, GXY)
            assert database_is_up_to_date(db_url, metadata_state6_combined, TSI)

        def test_upgrade__state4__separate_databases(
            self, db_state4_gxy, db_state4_tsi, metadata_state6_gxy, metadata_state6_tsi
        ):
            # Expect: upgrade to current version
            db1_url, db2_url = db_state4_gxy, db_state4_tsi
            mdb = LegacyManageDb()
            mdb.run_upgrade(db1_url, db2_url)
            assert database_is_up_to_date(db1_url, metadata_state6_gxy, GXY)
            assert database_is_up_to_date(db2_url, metadata_state6_tsi, TSI)
