import json
import tempfile
import unittest
from pathlib import Path
from typing import Any, List, Tuple

from abstra_internals.cloud.browser_service.dispatch import (
    BROWSER_SERVICE_TOOL_NAMES,
    MissingToolArgument,
    UnknownBrowserTool,
    dispatch,
)


class RecordingSession:
    """Records (method, args) and returns a scripted value per method."""

    def __init__(self, returns: dict) -> None:
        self.returns = returns
        self.calls: List[Tuple[str, tuple]] = []

    def call(self, method_name: str, *args: Any) -> Any:
        self.calls.append((method_name, args))
        return self.returns.get(method_name)


class DispatchTest(unittest.TestCase):
    def test_manifest_is_the_14_tools_including_open_page(self) -> None:
        self.assertEqual(len(BROWSER_SERVICE_TOOL_NAMES), 14)
        self.assertIn("browser_screenshot", BROWSER_SERVICE_TOOL_NAMES)
        self.assertIn("browser_open_page", BROWSER_SERVICE_TOOL_NAMES)

    def test_maps_each_tool_to_its_browsertools_method_positionally(self) -> None:
        cases = [
            (
                "browser_navigate",
                {"url": "https://x"},
                ("navigate_to_url", ("https://x",)),
            ),
            # open_page = navigate to the cloud-api-built bounce URL, with the
            # pod's page-run defaults (domcontentloaded + 150s nav timeout).
            (
                "browser_open_page",
                {"url": "https://bounce"},
                (
                    "navigate_to_url",
                    ("https://bounce", None, "domcontentloaded", 150_000),
                ),
            ),
            ("browser_get_page_summary", {"tab_id": "t"}, ("get_page_summary", ("t",))),
            ("browser_click", {"tab_id": "t", "index": 2}, ("click_element", ("t", 2))),
            (
                "browser_fill",
                {"tab_id": "t", "index": 1, "value": "hi"},
                ("fill_element", ("t", 1, "hi")),
            ),
            (
                "browser_get_text",
                {"tab_id": "t", "selector": "h1"},
                ("get_text", ("t", "h1")),
            ),
            (
                "browser_get_html",
                {"tab_id": "t", "selector": "table"},
                ("get_element_html", ("t", "table")),
            ),
            (
                "browser_execute_javascript",
                {"tab_id": "t", "script": "return 1"},
                ("execute_javascript", ("t", "return 1")),
            ),
            ("browser_get_console_logs", {"tab_id": "t"}, ("get_console_logs", ("t",))),
            (
                "browser_get_network_requests",
                {"tab_id": "t"},
                ("get_network_requests", ("t",)),
            ),
            ("browser_close", {"tab_id": "t"}, ("close_page", ("t",))),
            ("browser_list_tabs", {}, ("list_pages", ())),
        ]
        for tool, args, expected in cases:
            s = RecordingSession({})
            dispatch(s, tool, args)
            self.assertEqual(s.calls[-1], expected, tool)

    def test_wait_applies_tool_level_defaults(self) -> None:
        s = RecordingSession({})
        dispatch(s, "browser_wait", {})
        self.assertEqual(s.calls[-1], ("wait", (None, 1000)))
        s2 = RecordingSession({})
        dispatch(s2, "browser_wait", {"tab_id": "t", "milliseconds": 0})
        self.assertEqual(s2.calls[-1], ("wait", ("t", 0)))  # 0 preserved, not defaulted

    def test_serialization_matches_the_pod(self) -> None:
        # dict -> pretty JSON
        s = RecordingSession(
            {"navigate_to_url": {"tab_id": "t1", "url": "u", "title": "T"}}
        )
        out = dispatch(s, "browser_navigate", {"url": "u"})
        self.assertEqual(
            out, json.dumps({"tab_id": "t1", "url": "u", "title": "T"}, indent=2)
        )
        # list -> JSON + count footer
        s = RecordingSession({"get_page_summary": [{"index": 0}, {"index": 1}]})
        out = dispatch(s, "browser_get_page_summary", {"tab_id": "t"})
        self.assertTrue(out.endswith("[Returned 2 items]"))
        # None -> resource-not-found text
        s = RecordingSession({"close_page": None})
        self.assertEqual(
            dispatch(s, "browser_close", {"tab_id": "t"}),
            "null (resource not found or not accessible)",
        )
        # scalar -> str()
        s = RecordingSession({"get_text": "hello"})
        self.assertEqual(
            dispatch(s, "browser_get_text", {"tab_id": "t", "selector": "h1"}), "hello"
        )

    def test_screenshot_reads_the_temp_file_into_imageContent_and_unlinks_it(
        self,
    ) -> None:
        tmp = tempfile.NamedTemporaryFile(suffix=".jpeg", delete=False)
        tmp.write(b"fake-jpeg-bytes")
        tmp.close()
        path = Path(tmp.name)

        s = RecordingSession({"screenshot": str(path)})
        out = dispatch(s, "browser_screenshot", {"tab_id": "t", "show_markers": True})

        # screenshot(tab_id, show_markers, highlight_element) called positionally
        self.assertEqual(s.calls[-1], ("screenshot", ("t", True, None)))
        parsed = json.loads(out)
        self.assertTrue(parsed["__imageContent"])
        self.assertEqual(parsed["mimeType"], "image/jpeg")
        self.assertTrue(parsed["dataUri"].startswith("data:image/jpeg;base64,"))
        self.assertIn("with interactive-element markers", parsed["description"])
        self.assertFalse(path.exists())  # temp file cleaned up

    def test_missing_required_arg_and_unknown_tool(self) -> None:
        with self.assertRaises(MissingToolArgument):
            dispatch(RecordingSession({}), "browser_navigate", {})
        with self.assertRaises(MissingToolArgument):
            dispatch(RecordingSession({}), "browser_screenshot", {})
        with self.assertRaises(MissingToolArgument):
            dispatch(RecordingSession({}), "browser_open_page", {})
        with self.assertRaises(UnknownBrowserTool):
            dispatch(RecordingSession({}), "browser_download_file", {})


if __name__ == "__main__":
    unittest.main()
