"""
Unit tests for orchestration.orchestration_api (Step 13).

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

from __future__ import annotations

from typing import Any, Dict
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

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.orchestration_api import (
    get_command_schema,
    get_required_parameters,
    get_server,
    get_server_commands,
    get_servers,
    invoke_remote_command,
    validate_call,
)
from mcp_proxy_adapter.core.orchestration.orchestration_errors import (
    CommandNotFoundError,
    InvalidParameterError,
    MissingRequiredParametersError,
    SchemaUnavailableError,
    ServerNotFoundError,
    StaleCatalogError,
)
from mcp_proxy_adapter.core.orchestration.server_record import ServerRecord
from mcp_proxy_adapter.core.orchestration.snapshot_holder import SnapshotHolder


def _make_catalog(
    servers: Dict[str, ServerRecord],
    commands_by_server: Dict[str, Dict[str, CommandRecord]],
    version: int = 1,
) -> CatalogSnapshot:
    """Build a CatalogSnapshot for tests."""
    return CatalogSnapshot.create(
        version=version,
        servers=servers,
        commands_by_server=commands_by_server,
    )


@pytest.fixture
def sample_holder() -> SnapshotHolder:
    """Holder with one server and one command."""
    s1 = ServerRecord(
        server_id="s1",
        host="localhost",
        port=8080,
        base_url=None,
        status="ok",
    )
    cmd_echo = CommandRecord(
        command_name="echo",
        summary="Echo command",
        parameter_definitions={"message": {"type": "string"}},
        metadata=None,
        schema={
            "type": "object",
            "properties": {"message": {"type": "string"}},
            "required": ["message"],
        },
        required_parameters=["message"],
    )
    catalog = _make_catalog(
        servers={"s1": s1},
        commands_by_server={"s1": {"echo": cmd_echo}},
    )
    holder = SnapshotHolder()
    holder.swap(catalog, None)
    return holder


@pytest.fixture
def empty_holder() -> SnapshotHolder:
    """Holder with no snapshot (stale)."""
    return SnapshotHolder()


class TestGetServers:
    """Tests for get_servers."""

    def test_returns_list_from_catalog(self, sample_holder: SnapshotHolder) -> None:
        servers = get_servers(sample_holder)
        assert len(servers) == 1
        assert servers[0]["server_id"] == "s1"
        assert servers[0]["host"] == "localhost"
        assert servers[0]["port"] == 8080
        assert servers[0].get("status") == "ok"

    def test_stale_raises(self, empty_holder: SnapshotHolder) -> None:
        with pytest.raises(StaleCatalogError):
            get_servers(empty_holder)

    def test_empty_catalog_returns_empty_list(self) -> None:
        catalog = _make_catalog(servers={}, commands_by_server={})
        holder = SnapshotHolder()
        holder.swap(catalog, None)
        assert get_servers(holder) == []


class TestGetServer:
    """Tests for get_server."""

    def test_returns_server(self, sample_holder: SnapshotHolder) -> None:
        s = get_server(sample_holder, "s1")
        assert s.server_id == "s1"
        assert s.host == "localhost"
        assert s.port == 8080

    def test_not_found_raises(self, sample_holder: SnapshotHolder) -> None:
        with pytest.raises(ServerNotFoundError) as exc_info:
            get_server(sample_holder, "nonexistent")
        assert exc_info.value.server_id == "nonexistent"

    def test_stale_raises(self, empty_holder: SnapshotHolder) -> None:
        with pytest.raises(StaleCatalogError):
            get_server(empty_holder, "s1")


class TestGetServerCommands:
    """Tests for get_server_commands."""

    def test_returns_commands(self, sample_holder: SnapshotHolder) -> None:
        cmds = get_server_commands(sample_holder, "s1")
        assert len(cmds) == 1
        assert cmds[0]["name"] == "echo"
        assert cmds[0]["summary"] == "Echo command"

    def test_server_not_found_raises(self, sample_holder: SnapshotHolder) -> None:
        with pytest.raises(ServerNotFoundError):
            get_server_commands(sample_holder, "nonexistent")

    def test_stale_raises(self, empty_holder: SnapshotHolder) -> None:
        with pytest.raises(StaleCatalogError):
            get_server_commands(empty_holder, "s1")

    def test_empty_commands_returns_empty_list(self) -> None:
        s1 = ServerRecord(server_id="s1", host="h", port=1)
        catalog = _make_catalog(
            servers={"s1": s1},
            commands_by_server={"s1": {}},
        )
        holder = SnapshotHolder()
        holder.swap(catalog, None)
        assert get_server_commands(holder, "s1") == []


class TestGetCommandSchema:
    """Tests for get_command_schema."""

    def test_returns_schema(self, sample_holder: SnapshotHolder) -> None:
        schema = get_command_schema(sample_holder, "s1", "echo")
        assert schema.get("type") == "object"
        assert "properties" in schema
        assert schema["properties"]["message"]["type"] == "string"

    def test_server_not_found_raises(self, sample_holder: SnapshotHolder) -> None:
        with pytest.raises(ServerNotFoundError):
            get_command_schema(sample_holder, "nonexistent", "echo")

    def test_command_not_found_raises(self, sample_holder: SnapshotHolder) -> None:
        with pytest.raises(CommandNotFoundError):
            get_command_schema(sample_holder, "s1", "nonexistent")

    def test_schema_unavailable_raises(self) -> None:
        s1 = ServerRecord(server_id="s1", host="h", port=1)
        cmd = CommandRecord(
            command_name="no_schema",
            summary="",
            parameter_definitions={},
            metadata=None,
            schema={},
            required_parameters=[],
        )
        catalog = _make_catalog(
            servers={"s1": s1},
            commands_by_server={"s1": {"no_schema": cmd}},
        )
        holder = SnapshotHolder()
        holder.swap(catalog, None)
        with pytest.raises(SchemaUnavailableError):
            get_command_schema(holder, "s1", "no_schema")


class TestGetRequiredParameters:
    """Tests for get_required_parameters."""

    def test_returns_required_list(self, sample_holder: SnapshotHolder) -> None:
        req = get_required_parameters(sample_holder, "s1", "echo")
        assert req == ["message"]

    def test_server_not_found_raises(self, sample_holder: SnapshotHolder) -> None:
        with pytest.raises(ServerNotFoundError):
            get_required_parameters(sample_holder, "nonexistent", "echo")

    def test_command_not_found_raises(self, sample_holder: SnapshotHolder) -> None:
        with pytest.raises(CommandNotFoundError):
            get_required_parameters(sample_holder, "s1", "nonexistent")


class TestValidateCall:
    """Tests for validate_call."""

    def test_success(self, sample_holder: SnapshotHolder) -> None:
        validate_call(sample_holder, "s1", "echo", {"message": "hello"})

    def test_missing_required_raises(self, sample_holder: SnapshotHolder) -> None:
        with pytest.raises(MissingRequiredParametersError) as exc_info:
            validate_call(sample_holder, "s1", "echo", {})
        assert "message" in exc_info.value.parameter_names

    def test_invalid_type_raises(self, sample_holder: SnapshotHolder) -> None:
        with pytest.raises(InvalidParameterError) as exc_info:
            validate_call(sample_holder, "s1", "echo", {"message": 123})
        assert exc_info.value.parameter_name == "message"

    def test_stale_raises(self, empty_holder: SnapshotHolder) -> None:
        with pytest.raises(StaleCatalogError):
            validate_call(empty_holder, "s1", "echo", {"message": "x"})

    def test_server_not_found_raises(self, sample_holder: SnapshotHolder) -> None:
        with pytest.raises(ServerNotFoundError):
            validate_call(sample_holder, "nonexistent", "echo", {"message": "x"})

    def test_command_not_found_raises(self, sample_holder: SnapshotHolder) -> None:
        with pytest.raises(CommandNotFoundError):
            validate_call(sample_holder, "s1", "nonexistent", {})


class TestInvokeRemoteCommand:
    """Tests for invoke_remote_command."""

    @pytest.mark.asyncio
    async def test_validation_failure_no_network(
        self, sample_holder: SnapshotHolder
    ) -> None:
        """Missing params must not trigger network call."""
        with patch(
            "mcp_proxy_adapter.core.orchestration.orchestration_api.httpx"
        ) as m_httpx:
            with pytest.raises(MissingRequiredParametersError):
                await invoke_remote_command(
                    sample_holder, "s1", "localhost", 8080, "echo", {}
                )
            m_httpx.AsyncClient.assert_not_called()

    @pytest.mark.asyncio
    async def test_success_returns_result(self, sample_holder: SnapshotHolder) -> None:
        mock_response = MagicMock()
        mock_response.json.return_value = {
            "jsonrpc": "2.0",
            "result": {"success": True, "data": {"message": "hi"}},
            "id": 1,
        }
        mock_response.raise_for_status = MagicMock()

        async def post(*args: Any, **kwargs: Any) -> MagicMock:
            return mock_response

        with patch(
            "mcp_proxy_adapter.core.orchestration.orchestration_api.httpx.AsyncClient"
        ) as m_client:
            instance = AsyncMock()
            instance.post = AsyncMock(side_effect=post)
            instance.__aenter__ = AsyncMock(return_value=instance)
            instance.__aexit__ = AsyncMock(return_value=None)
            m_client.return_value = instance

            result = await invoke_remote_command(
                sample_holder,
                "s1",
                "localhost",
                8080,
                "echo",
                {"message": "hi"},
            )
            assert result == {"success": True, "data": {"message": "hi"}}
