import os
import tempfile
import unittest
from contextlib import ExitStack
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
from uuid import uuid4

from tools.paper_server import PaperFetchTool, PaperSearchTool, _media_url
from tools.run_python import RunPythonTool


class PaperServerToolTests(unittest.TestCase):
    def test_search_forwards_publication_type_to_server_filter(self):
        captured = {}

        def fake_get_json(url, *, api_key, params=None):
            captured.update(params or {})
            self.assertEqual(api_key, "secret")
            return {"results": [{"id": "p1", "type": "book", "title": "Book"}]}

        with (
            patch(
                "tools.paper_server._config",
                return_value=(
                    "https://paper.test",
                    "https://paper.test/api/resm/paper",
                    "secret",
                ),
            ),
            patch("tools.paper_server._get_json", side_effect=fake_get_json),
        ):
            result = PaperSearchTool().execute(publication_type="book", limit=20)

        self.assertEqual(captured["type"], "book")
        self.assertEqual(captured["page_size"], 20)
        self.assertIn('"type": "book"', result)
        self.assertNotIn("secret", result)

    def test_search_accepts_open_raw_type_without_closed_enum(self):
        with (
            patch(
                "tools.paper_server._config",
                return_value=(
                    "https://paper.test",
                    "https://paper.test/api/resm/paper",
                    "secret",
                ),
            ),
            patch("tools.paper_server._get_json", return_value=[]) as request,
        ):
            result = PaperSearchTool().execute(publication_type="future-type")
        self.assertEqual(request.call_args.kwargs["params"]["type"], "future-type")
        self.assertEqual(result, "[]")

    def test_search_rejects_unsafe_raw_type(self):
        with patch("tools.paper_server._get_json") as request:
            result = PaperSearchTool().execute(publication_type="book&api_key=leak")
        request.assert_not_called()
        self.assertTrue(result.startswith("[Error]"))

    def test_media_download_url_must_match_configured_origin(self):
        self.assertEqual(
            _media_url("/media/a.pdf", "https://paper.test"),
            "https://paper.test/media/a.pdf",
        )
        with self.assertRaisesRegex(RuntimeError, "out-of-origin"):
            _media_url("https://attacker.test/a.pdf", "https://paper.test")

    def test_fetch_schema_limits_format(self):
        with tempfile.TemporaryDirectory() as tmp:
            tool = PaperFetchTool(working_dir=Path(tmp))
            self.assertEqual(
                tool.parameters["properties"]["format"]["enum"], ["pdf", "xml"]
            )

    def test_fetch_downloads_atomically_and_reuses_existing_file(self):
        class FakeStreamResponse:
            status_code = 200

            def __enter__(self):
                return self

            def __exit__(self, *_args):
                return False

            def iter_bytes(self, chunk_size=0):
                self.test_chunk_size = chunk_size
                yield b"%PDF-test"

        paper = {
            "id": "p1",
            "doi": "10.1/example",
            "has_fulltext_pdf": True,
            "pdf_url": "/media/example.pdf",
        }
        with (
            tempfile.TemporaryDirectory() as tmp,
            patch("tools.paper_server._get_paper", return_value=paper),
            patch(
                "tools.paper_server._config",
                return_value=(
                    "https://paper.test",
                    "https://paper.test/api/resm/paper",
                    "secret",
                ),
            ),
            patch(
                "tools.paper_server.httpx.stream", return_value=FakeStreamResponse()
            ) as stream,
        ):
            tool = PaperFetchTool(working_dir=Path(tmp))
            first = tool.execute(id_or_doi="p1", format="pdf")
            second = tool.execute(id_or_doi="p1", format="pdf")
            destination = Path(tmp) / "papers" / "10.1_example.pdf"
            saved_bytes = destination.read_bytes()

        self.assertEqual(saved_bytes, b"%PDF-test")
        self.assertTrue(first.startswith("saved:"))
        self.assertTrue(second.endswith("(existing)"))
        self.assertEqual(stream.call_count, 1)

    def test_run_python_no_longer_passes_paper_server_key(self):
        with patch.dict(os.environ, {"PAPER_SERVER_API_KEY": "secret"}, clear=False):
            env = RunPythonTool()._filtered_env()
        self.assertNotIn("PAPER_SERVER_API_KEY", env)

    def test_registry_exposes_host_tools_only_when_platform_key_exists(self):
        from core.tool_registry import ToolContext, build_tools

        with tempfile.TemporaryDirectory() as tmp:
            root = Path(tmp)
            common = dict(
                tool_base=root,
                ur_path=root,
                working_dir_path=root,
                task_id=uuid4(),
                uid=uuid4(),
                cfg={},
                caps=SimpleNamespace(enable_run_python=False),
                skills=SimpleNamespace(skills={}),
                cancel_check=None,
                scheduled_run=False,
                deferred_actions=SimpleNamespace(),
                ark_cfg=None,
                img_provider="",
                img_key="",
                img_cfg=None,
                img_provider_cfg=None,
                video_variant="",
                office_to_pdf_available=False,
            )
            gates = dict(
                DOCUMENT_SEARCH_API_KEY="",
                MP_API_KEY="",
                PAPER_SERVER_API_KEY="",
            )

            def enter_common_patches(stack):
                stack.enter_context(
                    patch(
                        "core.tool_registry._external_system_status_available",
                        return_value=False,
                    )
                )
                stack.enter_context(
                    patch(
                        "core.tool_registry._external_systems_available",
                        return_value=False,
                    )
                )
                stack.enter_context(
                    patch("core.tool_registry.smtp_configured", return_value=False)
                )
                stack.enter_context(
                    patch(
                        "core.tool_registry.wechat_push_available", return_value=False
                    )
                )
                stack.enter_context(
                    patch("core.tool_registry.lfasr_configured", return_value=False)
                )
                stack.enter_context(
                    patch("core.tool_registry.BochaConfig.load", return_value=None)
                )

            with ExitStack() as stack:
                stack.enter_context(patch.dict(os.environ, gates, clear=False))
                enter_common_patches(stack)
                without_key = build_tools(ToolContext(**common))
            gates["PAPER_SERVER_API_KEY"] = "secret"
            with ExitStack() as stack:
                stack.enter_context(patch.dict(os.environ, gates, clear=False))
                enter_common_patches(stack)
                with_key = build_tools(ToolContext(**common))

        self.assertNotIn("paper_search", without_key)
        self.assertTrue({"paper_search", "paper_get", "paper_fetch"}.issubset(with_key))


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