#!/usr/bin/env python3
from __future__ import annotations

import os
import sys
import tempfile
import threading
import unittest
import asyncio
from io import BytesIO
from pathlib import Path
from unittest.mock import patch


HERE = Path(__file__).resolve().parent
TEST_ROOT = Path(tempfile.mkdtemp(prefix="iterfact_preview_assets_"))
(TEST_ROOT / "generated").mkdir(parents=True, exist_ok=True)
(TEST_ROOT / "analytics").mkdir(parents=True, exist_ok=True)

os.environ["ITERFACT_OUTPUT_DIR"] = str(TEST_ROOT / "generated")
os.environ["ITERFACT_ANALYTICS_DIR"] = str(TEST_ROOT / "analytics")
os.environ["ITERFACT_HEADLESS_VALIDATION_ENABLED"] = "0"
os.environ["ITERFACT_LIBRARY_PREVIEW_CAPTURE_ENABLED"] = "0"
os.environ["R2_BUCKET"] = "iterfact-test"
os.environ["R2_ACCESS_KEY_ID"] = "test-key"
os.environ["R2_SECRET_ACCESS_KEY"] = "test-secret"
os.environ["R2_ACCOUNT_ID"] = "test-account"
os.environ["ITERFACT_PUBLIC_ARTIFACT_BASE_URL"] = "https://a.iterfact.test"
os.environ["ANTHROPIC_API_KEY"] = ""

if str(HERE) not in sys.path:
    sys.path.insert(0, str(HERE))

import server as mcp_server  # noqa: E402


class _RecordingR2Client:
    def __init__(self) -> None:
        self.calls: list[tuple[str, str, str]] = []
        self.put_calls: list[tuple[str, str]] = []
        self.objects: dict[str, tuple[bytes, str]] = {}

    def upload_file(self, filename: str, bucket: str, key: str, ExtraArgs: dict[str, str] | None = None) -> None:
        content_type = (ExtraArgs or {}).get("ContentType", "")
        self.calls.append((Path(filename).name, key, content_type))
        self.objects[key] = (Path(filename).read_bytes(), content_type)

    def get_object(self, Bucket: str, Key: str) -> dict[str, object]:
        if Key not in self.objects:
            raise FileNotFoundError(Key)
        body, content_type = self.objects[Key]
        return {"Body": BytesIO(body), "ContentType": content_type}

    def put_object(self, Bucket: str, Key: str, Body: bytes, ContentType: str | None = None) -> None:
        payload = Body if isinstance(Body, bytes) else bytes(Body)
        self.put_calls.append((Key, ContentType or ""))
        self.objects[Key] = (payload, ContentType or "")


class PreviewAssetTests(unittest.TestCase):
    def test_run_threaded_capture_job_uses_worker_thread_inside_async_loop(self) -> None:
        async def run_case() -> str:
            return mcp_server.run_threaded_capture_job(
                name="iterfact-preview-test",
                timeout_seconds=2,
                worker=lambda: threading.current_thread().name,
            )

        worker_name = asyncio.run(run_case())

        self.assertNotEqual(worker_name, threading.current_thread().name)
        self.assertEqual(worker_name, "iterfact-preview-test")

    def test_build_library_preview_asset_paths(self) -> None:
        artifact_path = TEST_ROOT / "generated" / "artifact-123" / "demo-card.html"
        expected_dir = artifact_path.parent

        asset_paths = mcp_server.build_library_preview_asset_paths(artifact_path)

        self.assertEqual(asset_paths["poster"], expected_dir / "demo-card.preview-poster.png")
        self.assertEqual(
            asset_paths["frames"],
            [
                expected_dir / "demo-card.preview-frame-1.png",
                expected_dir / "demo-card.preview-frame-2.png",
                expected_dir / "demo-card.preview-frame-3.png",
            ],
        )

    def test_upload_to_r2_includes_cached_preview_sidecars(self) -> None:
        artifact_dir = TEST_ROOT / "generated" / "artifact-sidecars"
        artifact_dir.mkdir(parents=True, exist_ok=True)
        artifact_path = artifact_dir / "animated-card.html"
        artifact_path.write_text("<html><body>Hello</body></html>", encoding="utf-8")

        for sidecar in [
            artifact_dir / "animated-card.preview-poster.png",
            artifact_dir / "animated-card.preview-frame-1.png",
            artifact_dir / "animated-card.preview-frame-2.png",
        ]:
            sidecar.write_bytes(b"png")

        recorder = _RecordingR2Client()
        with patch.object(mcp_server.boto3, "client", return_value=recorder):
            object_key, preview_url = mcp_server.upload_to_r2(artifact_path, "artifact-sidecars")

        self.assertEqual(object_key, f"{mcp_server.SETTINGS.r2_prefix}/artifact-sidecars/animated-card.html")
        self.assertEqual(
            preview_url,
            f"{mcp_server.SETTINGS.public_artifact_base_url}/{mcp_server.SETTINGS.r2_prefix}/artifact-sidecars/animated-card.html",
        )
        self.assertEqual(
            [name for name, _, _ in recorder.calls],
            [
                "animated-card.html",
                "animated-card.preview-poster.png",
                "animated-card.preview-frame-1.png",
                "animated-card.preview-frame-2.png",
                "animated-card.html",
            ],
        )
        self.assertEqual(
            [content_type for _, _, content_type in recorder.calls],
            [
                "text/html; charset=utf-8",
                "image/png",
                "image/png",
                "image/png",
                "text/html; charset=utf-8",
            ],
        )

    def test_upload_to_r2_includes_poster_only_sidecar(self) -> None:
        artifact_dir = TEST_ROOT / "generated" / "artifact-poster-only"
        artifact_dir.mkdir(parents=True, exist_ok=True)
        artifact_path = artifact_dir / "poster-card.html"
        artifact_path.write_text("<html><body>Hello</body></html>", encoding="utf-8")

        poster = artifact_dir / "poster-card.preview-poster.png"
        poster.write_bytes(b"png")

        recorder = _RecordingR2Client()
        with patch.object(mcp_server.boto3, "client", return_value=recorder):
            object_key, preview_url = mcp_server.upload_to_r2(artifact_path, "artifact-poster-only")

        self.assertEqual(object_key, f"{mcp_server.SETTINGS.r2_prefix}/artifact-poster-only/poster-card.html")
        self.assertEqual(
            preview_url,
            f"{mcp_server.SETTINGS.public_artifact_base_url}/{mcp_server.SETTINGS.r2_prefix}/artifact-poster-only/poster-card.html",
        )
        self.assertEqual(
            [name for name, _, _ in recorder.calls],
            [
                "poster-card.html",
                "poster-card.preview-poster.png",
                "poster-card.html",
            ],
        )

    def test_ensure_library_preview_assets_falls_back_to_poster(self) -> None:
        artifact_dir = TEST_ROOT / "generated" / "artifact-fallback"
        artifact_dir.mkdir(parents=True, exist_ok=True)
        artifact_path = artifact_dir / "fallback-card.html"
        html = "<html><head><title>Fallback Card</title></head><body></body></html>"
        artifact_path.write_text(html, encoding="utf-8")

        fallback_poster = artifact_dir / "fallback-card.preview-poster.png"
        fallback_poster.write_bytes(b"png")

        with (
            patch.object(mcp_server, "capture_cached_library_preview_assets", return_value=[]),
            patch.object(
                mcp_server,
                "capture_library_preview_fallback_poster",
                return_value=[fallback_poster],
            ) as fallback_capture,
        ):
            generated = mcp_server.ensure_library_preview_assets(artifact_path, html)

        self.assertEqual(generated, [fallback_poster])
        fallback_capture.assert_called_once_with(artifact_path, "Fallback Card")

    def test_capture_library_preview_fallback_poster_uses_bundled_png_without_browser(self) -> None:
        artifact_dir = TEST_ROOT / "generated" / "artifact-bundled-fallback"
        artifact_dir.mkdir(parents=True, exist_ok=True)
        artifact_path = artifact_dir / "bundled-card.html"
        artifact_path.write_text("<html><body>Fallback</body></html>", encoding="utf-8")

        with (
            patch.object(mcp_server.SETTINGS, "library_preview_capture_enabled", False),
            patch.object(mcp_server, "sync_playwright", None),
        ):
            generated = mcp_server.capture_library_preview_fallback_poster(
                artifact_path,
                "Bundled Fallback",
            )

        self.assertEqual(len(generated), 1)
        self.assertTrue(generated[0].exists())
        self.assertEqual(
            generated[0].read_bytes(),
            (HERE / "static" / "library-preview-fallback.png").read_bytes(),
        )

    def test_upload_to_r2_backs_up_existing_html_before_overwrite(self) -> None:
        artifact_dir = TEST_ROOT / "generated" / "artifact-overwrite"
        artifact_dir.mkdir(parents=True, exist_ok=True)
        artifact_path = artifact_dir / "existing-card.html"
        artifact_path.write_text("<html><body>New</body></html>", encoding="utf-8")

        recorder = _RecordingR2Client()
        existing_key = f"{mcp_server.SETTINGS.r2_prefix}/artifact-overwrite/existing-card.html"
        recorder.objects[existing_key] = (b"<html><body>Old</body></html>", "text/html; charset=utf-8")

        with patch.object(mcp_server.boto3, "client", return_value=recorder):
            mcp_server.upload_to_r2(artifact_path, "artifact-overwrite")

        self.assertTrue(
            any(key.startswith(f"{mcp_server.SETTINGS.r2_prefix}/artifact-overwrite/_versions/") for key, _ in recorder.put_calls)
        )
        self.assertIn(
            f"{mcp_server.SETTINGS.r2_prefix}/artifact-overwrite/_source/existing-card.html",
            recorder.objects,
        )


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