"""Tests for UpgradeService — database migration with Alembic."""

from __future__ import annotations

from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest
from sqlalchemy import create_engine, text

from ztlctl.infrastructure.vault import Vault
from ztlctl.services.upgrade import UpgradeService

# ---------------------------------------------------------------------------
# check_pending()
# ---------------------------------------------------------------------------


class TestCheckPending:
    def test_check_pending_on_fresh_vault(self, vault: Vault) -> None:
        """Freshly initialized vault (stamped at head) has 0 pending."""
        svc = UpgradeService(vault)
        # Stamp so the vault is "freshly initialized" at head
        svc.stamp_current()

        result = svc.check_pending()
        assert result.ok
        assert result.data["pending_count"] == 0
        assert result.data["current"] == result.data["head"]

    def test_check_pending_unstamped_db(self, vault: Vault) -> None:
        """Unstamped vault (tables exist, no alembic_version) shows pending migrations."""
        result = UpgradeService(vault).check_pending()
        assert result.ok
        assert result.data["current"] is None
        assert result.data["pending_count"] > 0

    def test_check_pending_reports_head_revision(self, vault: Vault) -> None:
        """check_pending() always reports the head revision."""
        result = UpgradeService(vault).check_pending()
        assert result.ok
        assert result.data["head"] == "002_node_timestamps"


# ---------------------------------------------------------------------------
# apply()
# ---------------------------------------------------------------------------


class TestApply:
    def test_apply_already_current(self, vault: Vault) -> None:
        """Applying on an up-to-date vault returns 0 applied."""
        svc = UpgradeService(vault)
        # First apply (or stamp) to bring vault to head
        svc.stamp_current()

        result = svc.apply()
        assert result.ok
        assert result.data["applied_count"] == 0
        assert "already up to date" in result.data["message"].lower()

    def test_apply_fresh_db(self, vault: Vault) -> None:
        """Apply on fresh DB succeeds (tables already exist, so it stamps)."""
        svc = UpgradeService(vault)
        result = svc.apply()
        assert result.ok
        assert result.data["applied_count"] > 0

    def test_apply_creates_backup(self, vault: Vault) -> None:
        """Apply creates a backup file."""
        svc = UpgradeService(vault)
        result = svc.apply()
        assert result.ok
        assert "backup_path" in result.data
        assert Path(result.data["backup_path"]).exists()


# ---------------------------------------------------------------------------
# stamp_current()
# ---------------------------------------------------------------------------


class TestStampCurrent:
    def test_stamp_current(self, vault: Vault) -> None:
        """stamp_current() succeeds and reports stamped revision."""
        result = UpgradeService(vault).stamp_current()
        assert result.ok
        assert result.data["stamped"] is True
        assert result.data["current"] is not None


# ---------------------------------------------------------------------------
# _tables_exist()
# ---------------------------------------------------------------------------


class TestTablesExist:
    def test_tables_exist_true(self, vault: Vault) -> None:
        """On a vault with initialized DB, _tables_exist() returns True."""
        svc = UpgradeService(vault)
        assert svc._tables_exist() is True

    def test_tables_exist_false_on_empty_db(self, tmp_path: Path) -> None:
        """On a database with no tables, _tables_exist() returns False."""
        mock_vault = MagicMock()
        mock_vault.engine = create_engine(f"sqlite:///{tmp_path / 'empty.db'}")
        mock_vault.root = tmp_path
        svc = UpgradeService(mock_vault)
        assert svc._tables_exist() is False


# ---------------------------------------------------------------------------
# _check_schema_current()
# ---------------------------------------------------------------------------


class TestCheckSchemaCurrent:
    def test_check_schema_current_returns_true_when_at_head(self, vault: Vault) -> None:
        """A vault stamped at head reports schema as current."""
        # Stamp to head so alembic_version row exists and matches head
        UpgradeService(vault).stamp_current()
        assert vault._check_schema_current() is True

    def test_check_schema_current_handles_pre_alembic_vault(self, vault: Vault) -> None:
        """A vault with tables but no alembic_version row is treated as current (not stale).

        The fixture vault starts unstamped (no alembic_version table), which is exactly
        the pre-Alembic state — _check_schema_current() must return True (not stale).
        """
        # The fresh vault fixture has tables but no alembic_version table — pre-Alembic state.
        # _check_schema_current() should treat None revision as current.
        assert vault._check_schema_current() is True

    def test_check_schema_current_returns_false_when_stale(self, vault: Vault) -> None:
        """A vault with an outdated revision is reported as stale."""
        # First stamp so alembic_version table exists, then overwrite with a stale revision
        UpgradeService(vault).stamp_current()
        with vault.engine.begin() as conn:
            conn.execute(text("DELETE FROM alembic_version"))
            conn.execute(text("INSERT INTO alembic_version (version_num) VALUES ('000_fake_old')"))
        assert vault._check_schema_current() is False

    def test_stale_schema_warning_on_vault_access(
        self, vault_root: Path, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """Accessing AppContext.vault with a stale schema emits a warning to stderr."""
        from ztlctl.commands._context import AppContext
        from ztlctl.config.settings import ZtlSettings

        settings = ZtlSettings.from_cli(vault_root=vault_root, no_reweave=True)
        ctx = AppContext(settings)

        with (
            patch.object(Vault, "_check_schema_current", return_value=False),
            patch.object(Vault, "init_event_bus"),
        ):
            v = ctx.vault  # trigger lazy init
            assert v is not None

        captured = capsys.readouterr()
        assert "WARNING: Vault schema is out of date" in captured.err
        assert "ztlctl upgrade" in captured.err
