#!/usr/bin/env python3

from __future__ import annotations

import importlib.util
import json
from pathlib import Path
import tempfile
import unittest
from unittest import mock


MODULE_PATH = Path(__file__).with_name("ui_gallery.py")
SPEC = importlib.util.spec_from_file_location("ui_gallery", MODULE_PATH)
assert SPEC is not None and SPEC.loader is not None
UI_GALLERY = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(UI_GALLERY)


class UiGalleryWorkflowTests(unittest.TestCase):
    def test_compiler_artifact_reports_identity_target_and_freshness(self) -> None:
        message = {
            "reason": "compiler-artifact",
            "package_id": "path+file:///workspace#editor_ui@0.1.0",
            "target": {"name": "editor_ui"},
            "fresh": True,
        }
        self.assertEqual(
            UI_GALLERY.compiler_artifact(message),
            (
                "path+file:///workspace#editor_ui@0.1.0",
                "editor_ui",
                True,
            ),
        )

    def test_compiler_artifact_rejects_non_artifact_messages(self) -> None:
        self.assertIsNone(UI_GALLERY.compiler_artifact({"reason": "build-finished"}))

    def test_bounded_output_rejects_oversized_machine_documents(self) -> None:
        with self.assertRaises(RuntimeError):
            UI_GALLERY.bounded_command_output(
                [
                    "python3",
                    "-c",
                    "import sys; sys.stdout.write('x' * 4097)",
                ],
                4096,
            )

    def test_bounded_output_returns_small_json(self) -> None:
        document = {"packages": [], "resolve": {"nodes": []}}
        output = UI_GALLERY.bounded_command_output(
            ["python3", "-c", f"print({json.dumps(json.dumps(document))})"],
            4096,
        )
        self.assertEqual(json.loads(output), document)

    def test_source_snapshot_tracks_only_rust_and_manifests(self) -> None:
        with tempfile.TemporaryDirectory() as temporary:
            root = Path(temporary)
            source = root / "src"
            source.mkdir()
            (source / "panel.rs").write_text("fn panel() {}\n", encoding="utf-8")
            (source / "theme.json").write_text("{}\n", encoding="utf-8")
            original_roots = UI_GALLERY.WATCH_ROOTS
            original_files = UI_GALLERY.WATCH_FILES
            try:
                UI_GALLERY.WATCH_ROOTS = (source,)
                UI_GALLERY.WATCH_FILES = ()
                snapshot = UI_GALLERY.source_snapshot()
            finally:
                UI_GALLERY.WATCH_ROOTS = original_roots
                UI_GALLERY.WATCH_FILES = original_files
            self.assertEqual(list(snapshot), [source / "panel.rs"])

    def test_build_graph_cache_requires_matching_manifest_key(self) -> None:
        with tempfile.TemporaryDirectory() as temporary:
            cache = Path(temporary) / "graph.json"
            original = UI_GALLERY.BUILD_GRAPH_CACHE
            UI_GALLERY.BUILD_GRAPH_CACHE = cache
            try:
                UI_GALLERY.save_build_graph("current", {"package-a", "package-b"})
                self.assertEqual(
                    UI_GALLERY.cached_build_graph("current"),
                    {"package-a", "package-b"},
                )
                self.assertIsNone(UI_GALLERY.cached_build_graph("stale"))
            finally:
                UI_GALLERY.BUILD_GRAPH_CACHE = original

    def test_failed_rebuild_preserves_the_last_good_gallery(self) -> None:
        current = mock.Mock()
        with (
            mock.patch.object(UI_GALLERY, "build_gallery", return_value=1),
            mock.patch.object(UI_GALLERY, "launch_gallery") as launch,
            mock.patch.object(UI_GALLERY, "stop_gallery") as stop,
        ):
            replacement = UI_GALLERY.rebuild_and_replace_gallery(current)
        self.assertIs(replacement, current)
        launch.assert_not_called()
        stop.assert_not_called()

    def test_successful_rebuild_launches_before_retiring_the_last_good_gallery(self) -> None:
        current = mock.Mock()
        next_gallery = mock.Mock()
        order: list[str] = []
        with (
            mock.patch.object(UI_GALLERY, "build_gallery", return_value=0),
            mock.patch.object(
                UI_GALLERY,
                "launch_gallery",
                side_effect=lambda: order.append("launch") or next_gallery,
            ),
            mock.patch.object(
                UI_GALLERY,
                "stop_gallery",
                side_effect=lambda process: order.append("stop"),
            ) as stop,
        ):
            replacement = UI_GALLERY.rebuild_and_replace_gallery(current)
        self.assertIs(replacement, next_gallery)
        self.assertEqual(order, ["launch", "stop"])
        stop.assert_called_once_with(current)


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