"""
Unit tests for orchestration.snapshot_holder (Step 12).

Author: Vasiliy Zdanovskiy
email: vasilyvz@gmail.com
"""

import threading
from types import MappingProxyType
from typing import Mapping

from mcp_proxy_adapter.core.orchestration.catalog_snapshot import CatalogSnapshot
from mcp_proxy_adapter.core.orchestration.command_record import CommandRecord
from mcp_proxy_adapter.core.orchestration.server_record import ServerRecord
from mcp_proxy_adapter.core.orchestration.snapshot_holder import SnapshotHolder
from mcp_proxy_adapter.core.orchestration.tool_context_snapshot import (
    ToolContextSnapshot,
)


def _make_catalog(version: int = 1) -> CatalogSnapshot:
    """Build a minimal CatalogSnapshot for tests."""
    srv = ServerRecord(
        server_id="00000000-0000-4000-8000-000000000001", host="h", port=1
    )
    cmd = CommandRecord(
        command_name="cmd",
        summary=None,
        parameter_definitions={},
        metadata=None,
        schema={},
        required_parameters=(),
    )
    servers: Mapping[str, ServerRecord] = MappingProxyType({"s1": srv})
    commands: Mapping[str, Mapping[str, CommandRecord]] = MappingProxyType(
        {"s1": MappingProxyType({"cmd": cmd})}
    )
    return CatalogSnapshot.create(
        version=version, servers=servers, commands_by_server=commands
    )


def _make_tool_context(version: int = 1) -> ToolContextSnapshot:
    """Build a ToolContextSnapshot aligned with a catalog of given version."""
    catalog = _make_catalog(version)
    return ToolContextSnapshot.from_catalog(catalog, version=version)


class TestSnapshotHolder:
    """Tests for SnapshotHolder get/swap and mutex-protected access."""

    def test_initial_state_returns_none(self) -> None:
        """Before any swap, get_catalog, get_tool_context, get_version return None."""
        holder = SnapshotHolder()
        assert holder.get_catalog() is None
        assert holder.get_tool_context() is None
        assert holder.get_version() is None

    def test_swap_then_get_returns_same_snapshots(self) -> None:
        """After swap(catalog, tool_context), get_* return the same instances."""
        catalog = _make_catalog(1)
        tool_ctx = _make_tool_context(1)
        holder = SnapshotHolder()
        holder.swap(catalog, tool_ctx)
        assert holder.get_catalog() is catalog
        assert holder.get_tool_context() is tool_ctx
        assert holder.get_version() == 1

    def test_swap_replaces_both_atomically(self) -> None:
        """Second swap replaces both snapshots; readers see new pair."""
        catalog1 = _make_catalog(1)
        tool_ctx1 = _make_tool_context(1)
        catalog2 = _make_catalog(2)
        tool_ctx2 = _make_tool_context(2)
        holder = SnapshotHolder()
        holder.swap(catalog1, tool_ctx1)
        assert holder.get_version() == 1
        holder.swap(catalog2, tool_ctx2)
        assert holder.get_catalog() is catalog2
        assert holder.get_tool_context() is tool_ctx2
        assert holder.get_version() == 2

    def test_swap_none_clears_snapshots(self) -> None:
        """swap(None, None) clears current snapshots."""
        holder = SnapshotHolder()
        holder.swap(_make_catalog(1), _make_tool_context(1))
        holder.swap(None, None)
        assert holder.get_catalog() is None
        assert holder.get_tool_context() is None
        assert holder.get_version() is None

    def test_get_version_uses_catalog_first(self) -> None:
        """get_version returns catalog version when both are set."""
        catalog = _make_catalog(42)
        tool_ctx = _make_tool_context(42)
        holder = SnapshotHolder()
        holder.swap(catalog, tool_ctx)
        assert holder.get_version() == 42

    def test_get_version_from_tool_context_only(self) -> None:
        """get_version returns tool_context version when only tool_context is set."""
        tool_ctx = _make_tool_context(7)
        holder = SnapshotHolder()
        holder.swap(None, tool_ctx)
        assert holder.get_version() == 7

    def test_concurrent_reads_under_lock(self) -> None:
        """Multiple threads can read concurrently; results are consistent."""
        catalog = _make_catalog(1)
        tool_ctx = _make_tool_context(1)
        holder = SnapshotHolder()
        holder.swap(catalog, tool_ctx)
        results: list[tuple[object, object]] = []

        def read() -> None:
            c = holder.get_catalog()
            tc = holder.get_tool_context()
            results.append((c, tc))

        threads = [threading.Thread(target=read) for _ in range(10)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()
        for c, tc in results:
            assert c is catalog
            assert tc is tool_ctx
