"""Tests for SagaStateStore persistence and saga serialization."""

import json
from collections.abc import Iterator
from pathlib import Path

import pytest

from mcp_hangar.infrastructure.persistence.database_common import (
    MigrationRunner,
    SQLiteConfig,
    SQLiteConnectionFactory,
)
from mcp_hangar.infrastructure.persistence.saga_state_store import (
    SAGA_STORE_MIGRATIONS,
    NullSagaStateStore,
    SagaStateStore,
)

# Import application.commands FIRST to break circular import chain:
# Importing commands triggers: commands.__init__ -> reload_handler -> server.config
# -> server.state -> application.sagas (completing the sagas package init).
# After that, importing individual saga submodules works normally.
from mcp_hangar.application.commands import Command  # noqa: F401
from mcp_hangar.application.sagas.group_rebalance_saga import GroupRebalanceSaga
from mcp_hangar.application.sagas.mcp_server_failover_saga import (
    FailoverConfig,
    FailoverState,
    McpServerFailoverEventSaga,
)
from mcp_hangar.application.sagas.mcp_server_recovery_saga import McpServerRecoverySaga
from mcp_hangar.infrastructure.saga_manager import get_saga_manager


@pytest.fixture
def factory() -> Iterator[SQLiteConnectionFactory]:
    """Provide an in-memory SQLite connection factory and close it on teardown.

    Closing avoids leaking the persistent in-memory connection, which otherwise
    surfaces as ``ResourceWarning: unclosed database`` at interpreter GC time.
    """
    f = SQLiteConnectionFactory(SQLiteConfig(path=":memory:"))
    try:
        yield f
    finally:
        f.close()


class TestSagaStateStoreCheckpoint:
    """Test SagaStateStore.checkpoint() persistence."""

    def test_checkpoint_persists_state(self, factory: SQLiteConnectionFactory) -> None:
        """Test that checkpoint() writes state to SQLite and load() retrieves it."""
        store = SagaStateStore(factory)

        state_data = {"retry_state": {"p1": {"retries": 2}}}
        store.checkpoint("mcp_server_recovery", "saga-123", state_data, 42)

        result = store.load("mcp_server_recovery")
        assert result is not None
        assert result["state_data"] == state_data
        assert result["last_event_position"] == 42

    def test_checkpoint_overwrites_previous_state(self, factory: SQLiteConnectionFactory) -> None:
        """Test that checkpoint() with same saga_type+saga_id overwrites previous state."""
        store = SagaStateStore(factory)

        store.checkpoint("mcp_server_recovery", "saga-123", {"version": 1}, 10)
        store.checkpoint("mcp_server_recovery", "saga-123", {"version": 2}, 20)

        result = store.load("mcp_server_recovery")
        assert result is not None
        assert result["state_data"] == {"version": 2}
        assert result["last_event_position"] == 20


class TestSagaStateStoreLoad:
    """Test SagaStateStore.load() retrieval."""

    def test_load_returns_none_for_unknown_saga_type(self, factory: SQLiteConnectionFactory) -> None:
        """Test that load() returns None for an unknown saga_type."""
        store = SagaStateStore(factory)

        result = store.load("nonexistent_saga")
        assert result is None


class TestSagaStateStoreIdempotency:
    """Test SagaStateStore.mark_processed() and is_processed()."""

    def test_mark_processed_and_is_processed(self, factory: SQLiteConnectionFactory) -> None:
        """Test that mark_processed() records event position and is_processed() returns True."""
        store = SagaStateStore(factory)

        store.mark_processed("mcp_server_recovery", 42)

        assert store.is_processed("mcp_server_recovery", 42) is True

    def test_is_processed_returns_false_for_unrecorded_position(self, factory: SQLiteConnectionFactory) -> None:
        """Test that is_processed() returns False for an unrecorded position."""
        store = SagaStateStore(factory)

        assert store.is_processed("mcp_server_recovery", 99) is False

    def test_is_processed_returns_false_for_different_saga_type(self, factory: SQLiteConnectionFactory) -> None:
        """Test that is_processed() is scoped to saga_type."""
        store = SagaStateStore(factory)

        store.mark_processed("mcp_server_recovery", 42)

        assert store.is_processed("mcp_server_failover", 42) is False


class TestSagaStateStoreMigrations:
    """Test that MigrationRunner creates the expected tables."""

    def test_migrations_create_saga_state_table(self, factory: SQLiteConnectionFactory) -> None:
        """Test that the migration creates saga_state and saga_idempotency tables."""
        runner = MigrationRunner(factory, SAGA_STORE_MIGRATIONS, table_name="saga_state_migrations")
        applied = runner.run()

        assert applied == 1

        with factory.get_connection() as conn:
            cursor = conn.execute(
                "SELECT name FROM sqlite_master WHERE type='table' "
                "AND name IN ('saga_state', 'saga_idempotency') ORDER BY name"
            )
            tables = [row[0] for row in cursor.fetchall()]

        assert "saga_idempotency" in tables
        assert "saga_state" in tables


class TestMcpServerRecoverySagaSerialization:
    """Test McpServerRecoverySaga.to_dict() / from_dict() round-trip."""

    def test_to_dict_serializes_retry_state(self):
        """Test that to_dict() returns retry state dict."""
        saga = McpServerRecoverySaga(saga_manager=get_saga_manager())
        saga._retry_state = {
            "p1": {"retries": 2, "last_attempt": 1000.0, "next_retry": 1010.0},
            "p2": {"retries": 0, "last_attempt": 0, "next_retry": 0},
        }

        result = saga.to_dict()

        assert "retry_state" in result
        assert result["retry_state"]["p1"]["retries"] == 2

    def test_from_dict_restores_retry_state(self):
        """Test that from_dict() restores retry state from serialized data."""
        saga = McpServerRecoverySaga(saga_manager=get_saga_manager())
        data = {
            "retry_state": {
                "p1": {"retries": 3, "last_attempt": 500.0, "next_retry": 510.0},
            }
        }

        saga.from_dict(data)

        assert saga._retry_state["p1"]["retries"] == 3
        assert saga._retry_state["p1"]["last_attempt"] == 500.0

    def test_round_trip_serialization(self):
        """Test full round-trip: to_dict -> from_dict preserves state."""
        saga = McpServerRecoverySaga(saga_manager=get_saga_manager())
        saga._retry_state = {
            "p1": {"retries": 1, "last_attempt": 100.0, "next_retry": 105.0},
        }

        serialized = saga.to_dict()

        restored_saga = McpServerRecoverySaga(saga_manager=get_saga_manager())
        restored_saga.from_dict(serialized)

        assert restored_saga._retry_state == saga._retry_state


class TestMcpServerFailoverSagaSerialization:
    """Test McpServerFailoverEventSaga.to_dict() / from_dict() round-trip."""

    def test_to_dict_serializes_failover_state(self):
        """Test that to_dict() serializes failover_configs and active_failovers."""
        saga = McpServerFailoverEventSaga(saga_manager=get_saga_manager())
        saga._failover_configs["p1"] = FailoverConfig(
            primary_id="p1", backup_id="p1-backup", auto_failback=True, failback_delay_s=30.0
        )
        saga._active_failovers["p1"] = FailoverState(
            primary_id="p1", backup_id="p1-backup", failed_at=1000.0, backup_started_at=1001.0, is_active=True
        )
        saga._active_backups = {"p1-backup"}
        saga._pending_failback_timers = {"p1": "timer-id-abc"}

        result = saga.to_dict()

        assert "failover_configs" in result
        assert result["failover_configs"]["p1"]["primary_id"] == "p1"
        assert "active_failovers" in result
        assert result["active_failovers"]["p1"]["failed_at"] == 1000.0
        assert "active_backups" in result
        assert "p1-backup" in result["active_backups"]
        assert "pending_failback_timers" in result

    def test_from_dict_restores_failover_state(self):
        """Test that from_dict() restores all failover state."""
        saga = McpServerFailoverEventSaga(saga_manager=get_saga_manager())
        data = {
            "failover_configs": {
                "p1": {
                    "primary_id": "p1",
                    "backup_id": "p1-backup",
                    "auto_failback": False,
                    "failback_delay_s": 60.0,
                },
            },
            "active_failovers": {
                "p1": {
                    "primary_id": "p1",
                    "backup_id": "p1-backup",
                    "failed_at": 2000.0,
                    "backup_started_at": None,
                    "is_active": True,
                },
            },
            "active_backups": ["p1-backup"],
            "pending_failback_timers": {},
        }

        saga.from_dict(data)

        assert saga._failover_configs["p1"].primary_id == "p1"
        assert saga._failover_configs["p1"].auto_failback is False
        assert saga._active_failovers["p1"].failed_at == 2000.0
        assert saga._active_failovers["p1"].backup_started_at is None
        assert "p1-backup" in saga._active_backups
        # Timers are not restored across restarts
        assert saga._pending_failback_timers == {}

    def test_round_trip_serialization(self):
        """Test full round-trip: to_dict -> json -> from_dict preserves state."""
        saga = McpServerFailoverEventSaga(saga_manager=get_saga_manager())
        saga._failover_configs["p1"] = FailoverConfig(
            primary_id="p1", backup_id="p1-backup", auto_failback=True, failback_delay_s=30.0
        )
        saga._active_failovers["p1"] = FailoverState(
            primary_id="p1", backup_id="p1-backup", failed_at=1000.0, backup_started_at=1001.0, is_active=True
        )
        saga._active_backups = {"p1-backup"}
        saga._pending_failback_timers = {}

        serialized = saga.to_dict()
        # Simulate JSON round-trip (what SagaStateStore does)
        json_str = json.dumps(serialized)
        deserialized = json.loads(json_str)

        restored = McpServerFailoverEventSaga(saga_manager=get_saga_manager())
        restored.from_dict(deserialized)

        assert restored._failover_configs["p1"].primary_id == "p1"
        assert restored._active_failovers["p1"].failed_at == 1000.0
        assert "p1-backup" in restored._active_backups
        assert restored._pending_failback_timers == {}


class TestGroupRebalanceSagaSerialization:
    """Test GroupRebalanceSaga.to_dict() / from_dict()."""

    def test_to_dict_returns_empty_dict(self):
        """Test that to_dict() returns empty dict for stateless saga."""
        saga = GroupRebalanceSaga()

        result = saga.to_dict()

        assert result == {}

    def test_from_dict_is_noop(self):
        """Test that from_dict() is a no-op for stateless saga."""
        saga = GroupRebalanceSaga()
        saga._member_to_group = {"p1": "g1"}

        # from_dict should not affect the transient _member_to_group
        saga.from_dict({})

        assert saga._member_to_group == {"p1": "g1"}


class TestNullSagaStateStore:
    """Test NullSagaStateStore (null object pattern)."""

    def test_checkpoint_does_nothing(self):
        """Test that checkpoint() on NullSagaStateStore does not raise."""
        store = NullSagaStateStore()
        store.checkpoint("test", "id", {"key": "val"}, 1)

    def test_load_returns_none(self):
        """Test that load() returns None."""
        store = NullSagaStateStore()
        assert store.load("test") is None

    def test_mark_processed_does_nothing(self):
        """Test that mark_processed() does not raise."""
        store = NullSagaStateStore()
        store.mark_processed("test", 1)

    def test_is_processed_returns_false(self):
        """Test that is_processed() returns False."""
        store = NullSagaStateStore()
        assert store.is_processed("test", 1) is False


class TestBootstrapSagaWiring:
    """Test init_saga() bootstrap wiring for state persistence and restoration."""

    def test_init_saga_creates_saga_state_store_with_sqlite(self):
        """init_saga() creates SagaStateStore when event_store config has SQLite driver."""
        from mcp_hangar.server.bootstrap.cqrs import _create_saga_state_store

        config = {"event_store": {"driver": "sqlite", "path": "data/events.db"}}
        store = _create_saga_state_store(config)

        try:
            assert isinstance(store, SagaStateStore)
        finally:
            # The bootstrap helper builds an internal file-backed factory whose
            # connection would otherwise leak (ResourceWarning: unclosed database)
            # and leave a stray data/saga_state.db behind.
            store._conn_factory.close()  # type: ignore[union-attr]
            Path("data/saga_state.db").unlink(missing_ok=True)

    def test_init_saga_uses_null_store_when_no_event_store(self):
        """init_saga() uses NullSagaStateStore when event_store is not configured."""
        from mcp_hangar.server.bootstrap.cqrs import _create_saga_state_store

        store = _create_saga_state_store(None)
        assert isinstance(store, NullSagaStateStore)

    def test_init_saga_uses_null_store_when_memory_driver(self):
        """init_saga() uses NullSagaStateStore when event_store driver is memory."""
        from mcp_hangar.server.bootstrap.cqrs import _create_saga_state_store

        config = {"event_store": {"driver": "memory"}}
        store = _create_saga_state_store(config)

        assert isinstance(store, NullSagaStateStore)

    def test_restore_saga_state_loads_recovery_saga(self, factory: SQLiteConnectionFactory) -> None:
        """init_saga helper restores McpServerRecoverySaga state from store."""
        from mcp_hangar.server.bootstrap.cqrs import _restore_saga_state

        store = SagaStateStore(factory)

        # Pre-populate state in the store
        state_data = {"retry_state": {"p1": {"retries": 3, "last_attempt": 100.0, "next_retry": 110.0}}}
        store.checkpoint("mcp_server_recovery", "saga-id", state_data, 42)

        saga = McpServerRecoverySaga(saga_manager=get_saga_manager())
        _restore_saga_state(store, saga)

        assert saga._retry_state["p1"]["retries"] == 3

    def test_restore_saga_state_loads_failover_saga(self, factory: SQLiteConnectionFactory) -> None:
        """init_saga helper restores McpServerFailoverEventSaga state from store."""
        from mcp_hangar.server.bootstrap.cqrs import _restore_saga_state

        store = SagaStateStore(factory)

        # Pre-populate state
        state_data = {
            "failover_configs": {
                "p1": {"primary_id": "p1", "backup_id": "p1-backup", "auto_failback": True, "failback_delay_s": 30.0}
            },
            "active_failovers": {},
            "active_backups": [],
            "pending_failback_timers": {},
        }
        store.checkpoint("mcp_server_failover_event", "saga-id", state_data, 10)

        saga = McpServerFailoverEventSaga(saga_manager=get_saga_manager())
        _restore_saga_state(store, saga)

        assert saga._failover_configs["p1"].primary_id == "p1"

    def test_restore_saga_state_handles_missing_state(self, factory: SQLiteConnectionFactory) -> None:
        """init_saga helper handles missing persisted state gracefully (first boot)."""
        from mcp_hangar.server.bootstrap.cqrs import _restore_saga_state

        store = SagaStateStore(factory)

        saga = McpServerRecoverySaga(saga_manager=get_saga_manager())
        # Should not raise
        _restore_saga_state(store, saga)

        # State should be default (empty)
        assert saga._retry_state == {}
