from unittest import TestCase
from unittest.mock import patch

from flask import Flask

from abstra_internals.controllers.main import MainController
from abstra_internals.repositories.factory import build_editor_repositories
from abstra_internals.server.routes import mcp as mcp_routes
from tests.fixtures import clear_dir, init_dir

# The explicit name sets below MUST mirror cloud-api's manifest —
# services/ai/v2/tools/inProcessEditorToolNames.ts (IN_PROCESS_EDITOR_TOOL_NAMES):
#   stageWrite  → STAGE_CRUD_TOOLS + TRANSITION_TOOLS + WORKSPACE_WRITE_TOOLS
#                 (base names update_stage/delete_stage/... carry the approval
#                  suffix here; the manifest lists them suffix-less)
#   codebaseRead + configRead → READ_TOOLS
#   taskWrite   → TASK_WRITE_TOOLS
#   docsRead    → DOCS_TOOLS
# Cross-language automation of this equality is a documented follow-up.

STAGE_CRUD_TOOLS = {
    "create_stage",
    "update_stage__req_approval__",
    "delete_stage__req_approval__",
}

TRANSITION_TOOLS = {
    "add_transition",
    "delete_transition__req_approval__",
}

WORKSPACE_WRITE_TOOLS = {
    "update_workspace__req_approval__",
}

TASK_WRITE_TOOLS = {
    "create_task",
    "update_task_status",
    "clear_tasks__req_approval__",
}

READ_TOOLS = {
    "list_directory",
    "find_files_by_pattern",
    "grep_codebase",
    "search_file_with_context",
    "read_file_with_pagination",
    "read_stage_file_with_pagination",
    "list_all_stages",
    "get_stage",
    "get_workspace",
    "get_workflow_settings",
    "list_executions",
    "get_execution_logs",
    "get_execution_tasks",
    "list_tasks",
}

DOCS_TOOLS = {
    "read_abstra_docs",
    "get_stage_guide",
    "list_all_modules_in_abstra_lib",
    "list_objects_in_abstra_module",
    "describe_class",
    "describe_function",
}

# Git tools cloud-api serves in-process (the `git` group in
# inProcessEditorToolNames.ts) against the managed git-dir — gated off the pod
# like the sets above. revert_commit carries the approval suffix.
GIT_TOOLS = {
    "get_status",
    "get_commit_history",
    "commit_changes",
    "revert_commit__req_approval__",
}

# The 14 browser tools cloud-api serves in-process (approval suffix on the five
# gated ones) — browser_open_page included since its bounce-URL migration.
BROWSER_TOOLS = {
    "browser_open_page__req_approval__",
    "browser_navigate__req_approval__",
    "browser_get_page_summary",
    "browser_click__req_approval__",
    "browser_fill__req_approval__",
    "browser_get_text",
    "browser_get_html",
    "browser_execute_javascript__req_approval__",
    "browser_wait",
    "browser_get_console_logs",
    "browser_get_network_requests",
    "browser_screenshot",
    "browser_close",
    "browser_list_tabs",
}


class McpStageCrudGatingTest(TestCase):
    """External-player web editors must not advertise the stage-CRUD,
    transition or workspace-write MCP tools: cloud-api serves
    create/update/delete stage, add/delete transition and update_workspace
    in-process for them."""

    def setUp(self) -> None:
        self.root = init_dir()
        self.controller = MainController(build_editor_repositories())

    def tearDown(self) -> None:
        clear_dir(self.root)

    def _tool_names(self) -> set:
        app = Flask(__name__)
        app.register_blueprint(
            mcp_routes.get_editor_bp(self.controller), url_prefix="/mcp"
        )
        response = app.test_client().post("/mcp/tools/list")
        self.assertEqual(response.status_code, 200)
        return {tool["name"] for tool in response.get_json()["tools"]}

    def test_external_player_web_editor_omits_stage_crud_tools(self):
        with (
            patch.object(mcp_routes, "EDITOR_MODE", "web"),
            patch.object(
                mcp_routes, "EXTERNAL_PLAYER_URL", "https://web-editor-x.abstra.app"
            ),
        ):
            names = self._tool_names()

        self.assertTrue(STAGE_CRUD_TOOLS.isdisjoint(names))
        self.assertTrue(TRANSITION_TOOLS.isdisjoint(names))
        self.assertTrue(WORKSPACE_WRITE_TOOLS.isdisjoint(names))
        self.assertTrue(READ_TOOLS.isdisjoint(names))
        self.assertTrue(TASK_WRITE_TOOLS.isdisjoint(names))
        self.assertTrue(DOCS_TOOLS.isdisjoint(names))
        self.assertTrue(BROWSER_TOOLS.isdisjoint(names))
        # GIT FLIP DEFERRED: git tools stay on the pod for now, so they are NOT
        # gated off in external mode yet. Re-enable when flipping git to cloud-api.
        # self.assertTrue(GIT_TOOLS.isdisjoint(names))
        self.assertTrue(GIT_TOOLS.issubset(names))
        # Compute-plane tools keep being served by the pod.
        self.assertIn("list_linter_issues", names)
        self.assertIn("run_page", names)

    def test_web_editor_without_external_player_keeps_stage_crud_tools(self):
        with (
            patch.object(mcp_routes, "EDITOR_MODE", "web"),
            patch.object(mcp_routes, "EXTERNAL_PLAYER_URL", None),
        ):
            names = self._tool_names()

        self.assertTrue(STAGE_CRUD_TOOLS.issubset(names))
        self.assertTrue(TRANSITION_TOOLS.issubset(names))
        self.assertTrue(WORKSPACE_WRITE_TOOLS.issubset(names))
        self.assertTrue(READ_TOOLS.issubset(names))
        self.assertTrue(TASK_WRITE_TOOLS.issubset(names))
        self.assertTrue(DOCS_TOOLS.issubset(names))
        self.assertTrue(BROWSER_TOOLS.issubset(names))
        self.assertTrue(GIT_TOOLS.issubset(names))

    def test_local_editor_keeps_stage_crud_tools(self):
        with (
            patch.object(mcp_routes, "EDITOR_MODE", "local"),
            patch.object(mcp_routes, "EXTERNAL_PLAYER_URL", None),
        ):
            names = self._tool_names()

        self.assertTrue(STAGE_CRUD_TOOLS.issubset(names))
        self.assertTrue(TRANSITION_TOOLS.issubset(names))
        self.assertTrue(WORKSPACE_WRITE_TOOLS.issubset(names))
        self.assertTrue(READ_TOOLS.issubset(names))
        self.assertTrue(TASK_WRITE_TOOLS.issubset(names))
        self.assertTrue(DOCS_TOOLS.issubset(names))
        self.assertTrue(BROWSER_TOOLS.issubset(names))
        self.assertTrue(GIT_TOOLS.issubset(names))
