# #region TestCheckMigrationChain [C:2] [TYPE Module] [SEMANTICS test,alembic,migration,chain]
# @BRIEF Tests for migration chain validation logic (mock alembic, no real DB).
# @RELATION BINDS_TO -> [check_migration_chain]
# @TEST_EDGE: check_migration_heads_single -> OK prints single head
# @TEST_EDGE: check_migration_heads_multiple -> Exits 1 with error
# @TEST_EDGE: check_migration_heads_empty -> Exits 1 with error
# @TEST_EDGE: chain_integrity_ok -> Traverses clean chain
# @TEST_EDGE: chain_integrity_broken_link -> Exits 1
# @TEST_EDGE: chain_integrity_merge_migration -> Handles tuple down_revision
# @TEST_EDGE: chain_integrity_broken_merge -> Exits 1
# @TEST_EDGE: main_runs_all_checks -> Calls both functions
# @TEST_EDGE: main_with_multiple_heads -> Exits early
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch, call

import pytest

# Ensure src is importable
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))


# =============================================================================
# check_migration_heads
# =============================================================================

class TestCheckMigrationHeads:
    """Tests for check_migration_chain.check_migration_heads()."""

    # Alembic imports are inside function body, so patch the real full paths
    @patch("alembic.script.ScriptDirectory")
    @patch("alembic.config.Config")
    def test_single_head(self, MockAlembicConfig, MockScriptDir, capsys):
        """Happy: single head prints OK."""
        mock_script = MagicMock()
        mock_script.get_heads.return_value = ["abc123"]
        rev = MagicMock()
        rev.doc = "Initial migration"
        mock_script.get_revision.return_value = rev
        MockScriptDir.from_config.return_value = mock_script

        from src.scripts.check_migration_chain import check_migration_heads
        check_migration_heads()

        captured = capsys.readouterr()
        assert "OK: Single migration head" in captured.out
        assert "abc123" in captured.out

    @patch("alembic.script.ScriptDirectory")
    @patch("alembic.config.Config")
    def test_multiple_heads_exits(self, MockAlembicConfig, MockScriptDir):
        """Edge: multiple heads prints error and exits 1."""
        mock_script = MagicMock()
        mock_script.get_heads.return_value = ["head1", "head2"]
        rev1 = MagicMock()
        rev1.doc = "Migration A"
        rev2 = MagicMock()
        rev2.doc = "Migration B"

        def get_revision_side_effect(rev_id):
            return {"head1": rev1, "head2": rev2}[rev_id]

        mock_script.get_revision.side_effect = get_revision_side_effect
        MockScriptDir.from_config.return_value = mock_script

        from src.scripts.check_migration_chain import check_migration_heads
        with pytest.raises(SystemExit) as exc:
            check_migration_heads()
        assert exc.value.code == 1

    @patch("alembic.script.ScriptDirectory")
    @patch("alembic.config.Config")
    def test_no_heads_exits(self, MockAlembicConfig, MockScriptDir):
        """Edge: no heads prints error and exits 1."""
        mock_script = MagicMock()
        mock_script.get_heads.return_value = []
        MockScriptDir.from_config.return_value = mock_script

        from src.scripts.check_migration_chain import check_migration_heads
        with pytest.raises(SystemExit) as exc:
            check_migration_heads()
        assert exc.value.code == 1


# =============================================================================
# check_migration_chain_integrity
# =============================================================================

class TestCheckMigrationChainIntegrity:
    """Tests for check_migration_chain.check_migration_chain_integrity()."""

    def _make_revision(self, rev_id, down_revision, doc=""):
        rev = MagicMock()
        rev.revision = rev_id
        rev.down_revision = down_revision
        rev.doc = doc
        return rev

    @patch("alembic.script.ScriptDirectory")
    @patch("alembic.config.Config")
    def test_chain_ok(self, MockAlembicConfig, MockScriptDir, capsys):
        """Happy: linear chain traversed without error."""
        head = self._make_revision("c", "b", "Third")
        middle = self._make_revision("b", "a", "Second")
        root = self._make_revision("a", None, "First")

        mock_script = MagicMock()
        mock_script.get_heads.return_value = ["c"]

        def get_revision(rev_id):
            mapping = {"c": head, "b": middle, "a": root}
            return mapping[rev_id]

        mock_script.get_revision.side_effect = get_revision
        MockScriptDir.from_config.return_value = mock_script

        from src.scripts.check_migration_chain import check_migration_chain_integrity
        check_migration_chain_integrity()

        captured = capsys.readouterr()
        assert "Chain integrity verified" in captured.out
        assert "3 migrations traversed" in captured.out

    @patch("alembic.script.ScriptDirectory")
    @patch("alembic.config.Config")
    def test_broken_link_exits(self, MockAlembicConfig, MockScriptDir):
        """Edge: missing down_revision exits 1."""
        head = self._make_revision("c", "bogus", "Broken")

        mock_script = MagicMock()
        mock_script.get_heads.return_value = ["c"]

        def get_revision(rev_id):
            if rev_id == "c":
                return head
            raise Exception(f"Revision {rev_id} not found")

        mock_script.get_revision.side_effect = get_revision
        MockScriptDir.from_config.return_value = mock_script

        from src.scripts.check_migration_chain import check_migration_chain_integrity
        with pytest.raises(SystemExit) as exc:
            check_migration_chain_integrity()
        assert exc.value.code == 1

    @patch("alembic.script.ScriptDirectory")
    @patch("alembic.config.Config")
    def test_merge_migration_handled(self, MockAlembicConfig, MockScriptDir, capsys):
        """Edge: merge migration (tuple down_revision) traversed correctly."""
        merge = self._make_revision("merge", ("a", "b"), "Merge")
        parent_a = self._make_revision("a", None, "Root A")
        parent_b = self._make_revision("b", None, "Root B")

        mock_script = MagicMock()
        mock_script.get_heads.return_value = ["merge"]

        def get_revision(rev_id):
            mapping = {"merge": merge, "a": parent_a, "b": parent_b}
            return mapping[rev_id]

        mock_script.get_revision.side_effect = get_revision
        MockScriptDir.from_config.return_value = mock_script

        from src.scripts.check_migration_chain import check_migration_chain_integrity
        check_migration_chain_integrity()

        captured = capsys.readouterr()
        assert "Chain integrity verified" in captured.out
        assert "3 migrations traversed" in captured.out

    @patch("alembic.script.ScriptDirectory")
    @patch("alembic.config.Config")
    def test_broken_merge_link_exits(self, MockAlembicConfig, MockScriptDir):
        """Edge: merge migration with missing parent exits 1."""
        merge = self._make_revision("merge", ("a", "ghost"), "Merge")

        mock_script = MagicMock()
        mock_script.get_heads.return_value = ["merge"]

        def get_revision(rev_id):
            if rev_id == "merge":
                return merge
            if rev_id == "a":
                return self._make_revision("a", None, "Root A")
            raise Exception(f"Revision {rev_id} not found")

        mock_script.get_revision.side_effect = get_revision
        MockScriptDir.from_config.return_value = mock_script

        from src.scripts.check_migration_chain import check_migration_chain_integrity
        with pytest.raises(SystemExit) as exc:
            check_migration_chain_integrity()
        assert exc.value.code == 1


# =============================================================================
# main
# =============================================================================

class TestMain:
    """Tests for check_migration_chain.main()."""

    @patch("src.scripts.check_migration_chain.check_migration_chain_integrity")
    @patch("src.scripts.check_migration_chain.check_migration_heads")
    def test_main_calls_both(self, mock_heads, mock_integrity, capsys):
        """Happy: main() calls both checks and prints pass."""
        from src.scripts.check_migration_chain import main
        main()

        mock_heads.assert_called_once()
        mock_integrity.assert_called_once()
        captured = capsys.readouterr()
        assert "All checks passed" in captured.out

    @patch("src.scripts.check_migration_chain.check_migration_chain_integrity")
    @patch("src.scripts.check_migration_chain.check_migration_heads")
    def test_main_stops_on_heads_failure(self, mock_heads, mock_integrity):
        """Edge: main exits early if check_migration_heads fails."""
        mock_heads.side_effect = SystemExit(1)

        from src.scripts.check_migration_chain import main
        with pytest.raises(SystemExit) as exc:
            main()
        assert exc.value.code == 1
        mock_integrity.assert_not_called()
# #endregion TestCheckMigrationChain
