"""Tests for the Phase 6 MCP tools (execution, bot, diagnose).

Tools are thin wrappers, so these tests mock the service-layer functions and
verify the dict shape each tool returns. The server pipeline (serialization,
budget, confirm gate) is covered by ``test_server.py``; here we confirm the
tools delegate correctly and project the right fields.
"""

from __future__ import annotations

from unittest.mock import MagicMock

import pytest

from a360_mcp.auth.auth_manager import A360AuthError
from a360_mcp.services import bot as bot_svc
from a360_mcp.services import diagnose as diag_svc
from a360_mcp.services import execution as exec_svc
from a360_mcp.services.execution import ExecutionSummary
from a360_mcp.tools.bot_tools import GetBotActionsParams, get_bot_actions
from a360_mcp.tools.diagnose_tools import (
    DiagnoseExecutionParams,
    diagnose_execution,
)
from a360_mcp.tools.execution_tools import (
    GetExecutionParams,
    ListExecutionsParams,
    get_execution,
    list_executions,
)
from a360_mcp.utils.snapshot_store import LocalSnapshotStore


# --------------------------------------------------------------------------- #
# get_execution
# --------------------------------------------------------------------------- #
class TestGetExecution:
    def test_returns_projected_summary(self, monkeypatch):
        summary = ExecutionSummary(
            id="exec-1",
            status="RUN_FAILED",
            message="boom",
            file_id="file-9",
            deployment_id="dep-1",
            bot_name="MyBot",
            started_on="t0",
            completed_on="t1",
            error={"message": "boom"},
            raw={},
        )
        monkeypatch.setattr(exec_svc, "fetch_execution", lambda auth, eid: {"raw": 1})
        monkeypatch.setattr(exec_svc, "parse_execution", lambda raw: summary)

        result = get_execution(MagicMock(), MagicMock(), GetExecutionParams(execution_id="exec-1"))
        assert result["id"] == "exec-1"
        assert result["status"] == "RUN_FAILED"
        assert result["is_failure"] is True
        assert result["file_id"] == "file-9"
        assert result["bot_name"] == "MyBot"
        assert result["error"] == {"message": "boom"}

    def test_is_failure_false_for_success(self, monkeypatch):
        summary = ExecutionSummary(
            id="e",
            status="RUN_SUCCESS",
            message="",
            file_id=None,
            deployment_id=None,
            bot_name=None,
            started_on=None,
            completed_on=None,
            error=None,
            raw={},
        )
        monkeypatch.setattr(exec_svc, "fetch_execution", lambda auth, eid: {})
        monkeypatch.setattr(exec_svc, "parse_execution", lambda raw: summary)
        result = get_execution(MagicMock(), MagicMock(), GetExecutionParams(execution_id="e"))
        assert result["is_failure"] is False

    def test_auth_error_propagates(self, monkeypatch):
        def raise_auth(auth, eid):
            raise A360AuthError("nope")

        monkeypatch.setattr(exec_svc, "fetch_execution", raise_auth)
        with pytest.raises(A360AuthError):
            get_execution(MagicMock(), MagicMock(), GetExecutionParams(execution_id="e"))


# --------------------------------------------------------------------------- #
# list_executions
# --------------------------------------------------------------------------- #
class TestListExecutions:
    def test_returns_rows_and_count(self, monkeypatch):
        monkeypatch.setattr(
            exec_svc,
            "find_executions",
            lambda auth, **kw: [{"id": "e1"}, {"id": "e2"}],
        )
        result = list_executions(
            MagicMock(),
            MagicMock(),
            ListExecutionsParams(status="RUN_FAILED", limit=10),
        )
        assert result["count"] == 2
        assert len(result["executions"]) == 2

    def test_empty_result(self, monkeypatch):
        monkeypatch.setattr(exec_svc, "find_executions", lambda auth, **kw: [])
        result = list_executions(MagicMock(), MagicMock(), ListExecutionsParams())
        assert result["count"] == 0
        assert result["executions"] == []


# --------------------------------------------------------------------------- #
# get_bot_actions
# --------------------------------------------------------------------------- #
class TestGetBotActions:
    def test_returns_parsed_surface_not_raw_json(self, monkeypatch):
        from a360_mcp.services.bot import BotNode

        nodes = [
            BotNode(
                order=0,
                path=(0,),
                command="excel.open",
                name="Open",
                line_number=1,
                index=None,
                kind="action",
                depth=0,
                has_children=False,
            ),
            BotNode(
                order=1,
                path=(1,),
                command="try",
                name=None,
                line_number=2,
                index=None,
                kind="try",
                depth=0,
                has_children=False,
            ),
        ]
        monkeypatch.setattr(bot_svc, "fetch_bot_content", lambda auth, fid: {"raw": 1})
        monkeypatch.setattr(bot_svc, "walk_actions", lambda bj: nodes)
        monkeypatch.setattr(bot_svc, "has_error_handler", lambda ns: True)

        result = get_bot_actions(MagicMock(), MagicMock(), GetBotActionsParams(file_id="file-9"))
        assert result["file_id"] == "file-9"
        assert result["action_count"] == 2
        assert result["returned_count"] == 2
        assert result["has_error_handler"] is True
        assert result["actions"][0]["command"] == "excel.open"
        assert result["actions"][0]["name"] == "Open"
        assert result["actions"][1]["kind"] == "try"
        # raw JSON must NOT leak
        assert "raw" not in result

    def test_limit_truncates_returned_not_count(self, monkeypatch):
        from a360_mcp.services.bot import BotNode

        nodes = [
            BotNode(
                order=i,
                path=(i,),
                command=f"c{i}",
                name=None,
                line_number=None,
                index=None,
                kind="action",
                depth=0,
                has_children=False,
            )
            for i in range(5)
        ]
        monkeypatch.setattr(bot_svc, "fetch_bot_content", lambda auth, fid: {})
        monkeypatch.setattr(bot_svc, "walk_actions", lambda bj: nodes)
        monkeypatch.setattr(bot_svc, "has_error_handler", lambda ns: False)

        result = get_bot_actions(
            MagicMock(), MagicMock(), GetBotActionsParams(file_id="f", limit=2)
        )
        assert result["action_count"] == 5
        assert result["returned_count"] == 2

    def test_auth_error_propagates(self, monkeypatch):
        def raise_auth(auth, fid):
            raise A360AuthError("nope")

        monkeypatch.setattr(bot_svc, "fetch_bot_content", raise_auth)
        with pytest.raises(A360AuthError):
            get_bot_actions(MagicMock(), MagicMock(), GetBotActionsParams(file_id="f"))


# --------------------------------------------------------------------------- #
# diagnose_execution
# --------------------------------------------------------------------------- #
class TestDiagnoseExecution:
    def test_delegates_to_service(self, monkeypatch, server_config):
        expected = {
            "verdict": "action 'X' failed: boom.",
            "execution_id": "exec-1",
            "status": "RUN_FAILED",
        }
        captured = {}

        def fake_diagnose(auth, eid, *, snapshot_store=None, refresh=False):
            captured["eid"] = eid
            captured["snapshot_store"] = snapshot_store
            captured["refresh"] = refresh
            return expected

        monkeypatch.setattr(diag_svc, "diagnose", fake_diagnose)
        result = diagnose_execution(
            server_config, MagicMock(), DiagnoseExecutionParams(execution_id="exec-1")
        )
        assert result == expected
        assert captured["eid"] == "exec-1"
        # Phase 7: a real LocalSnapshotStore is constructed and passed.
        assert isinstance(captured["snapshot_store"], LocalSnapshotStore)
        # Cache-first stays the default; refresh is the explicit escape hatch.
        assert captured["refresh"] is False

    def test_refresh_flag_reaches_the_service(self, monkeypatch, server_config):
        captured = {}

        def fake_diagnose(auth, eid, *, snapshot_store=None, refresh=False):
            captured["refresh"] = refresh
            return {"verdict": "fresh"}

        monkeypatch.setattr(diag_svc, "diagnose", fake_diagnose)
        diagnose_execution(
            server_config,
            MagicMock(),
            DiagnoseExecutionParams(execution_id="exec-1", refresh=True),
        )
        assert captured["refresh"] is True

    def test_auth_error_propagates(self, monkeypatch, server_config):
        def raise_auth(auth, eid, *, snapshot_store=None, refresh=False):
            raise A360AuthError("nope")

        monkeypatch.setattr(diag_svc, "diagnose", raise_auth)
        with pytest.raises(A360AuthError):
            diagnose_execution(
                server_config, MagicMock(), DiagnoseExecutionParams(execution_id="e")
            )


# --------------------------------------------------------------------------- #
# Registration (the tools must be discoverable by the registry)
# --------------------------------------------------------------------------- #
class TestRegistration:
    def test_tools_registered(self):
        # Import is cached, so after another test file clears the registry,
        # re-importing won't re-run the decorators. Reload forces re-registration.
        import importlib

        import a360_mcp.tools.bot_tools
        import a360_mcp.tools.diagnose_tools
        import a360_mcp.tools.execution_tools
        from a360_mcp.utils.registry import clear_registry, get_registry

        clear_registry()
        importlib.reload(a360_mcp.tools.execution_tools)
        importlib.reload(a360_mcp.tools.bot_tools)
        importlib.reload(a360_mcp.tools.diagnose_tools)

        registry = get_registry()
        for name in (
            "get_execution",
            "list_executions",
            "get_bot_actions",
            "diagnose_execution",
        ):
            assert name in registry, f"{name} not registered"

    def test_diagnose_execution_is_read_only(self):
        # Must be carved out as read-only so it needs no confirm gate.
        from a360_mcp.policies.write_guards import is_read_only_call

        assert is_read_only_call("diagnose_execution") is True
