import contextlib
import io
import json
import sys
import tempfile
import types
import unittest
from pathlib import Path
from unittest.mock import patch


ROOT = Path(__file__).resolve().parents[1]
SCRIPT_PATH = ROOT / "scripts" / "patch_render_missing.py"


class PatchRenderCliSmokeTests(unittest.TestCase):
    def _load_script_module(self):
        module = types.ModuleType("patch_render_missing_test_module")
        module.__file__ = str(SCRIPT_PATH)
        code = compile(SCRIPT_PATH.read_text(encoding="utf-8"), str(SCRIPT_PATH), "exec")
        exec(code, module.__dict__)
        return module

    async def _noop_async(self, *_args, **_kwargs):
        return {}

    def _make_state_module(self):
        module = types.ModuleType("core.ppt_generator.thought_to_ppt.state")

        class PPTPage:
            def __init__(self, **kwargs):
                self.__dict__.update(kwargs)

        class PageType:
            COVER_THANKS = 4
            TOC = 2
            SEPARATOR = 3
            CONTENT = 1

        module.PPTPage = PPTPage
        module.PageType = PageType
        return module

    def _make_html_page_generators_node(self):
        module = types.ModuleType("core.ppt_generator.thought_to_ppt.page_generators.node")

        async def prepare_generation_context_node(state, _writer):
            return {
                "save_dir": state["save_dir"],
                "ppt_prompt": "prompt",
                "language": "中文",
                "render_mode": "html",
                "template_name": "common_light",
                "template": "<html></html>",
            }

        module.prepare_generation_context_node = prepare_generation_context_node
        return module

    def _make_svg_page_generators_node(self):
        module = types.ModuleType("core.ppt_generator.thought_to_ppt.svg_page_generators.node")

        async def prepare_generation_context_node(state, _writer):
            return {
                "save_dir": state["save_dir"],
                "ppt_prompt": "prompt",
                "language": "中文",
                "render_mode": "svg",
                "template_name": "common_light",
                "template": "<svg></svg>",
            }

        async def quality_check_node(_state, _writer):
            return {}

        module.prepare_generation_context_node = prepare_generation_context_node
        module.quality_check_node = quality_check_node
        return module

    def _make_html_subgraph_modules(self):
        cover_module = types.ModuleType(
            "core.ppt_generator.thought_to_ppt.page_generators.cover_thanks_pages_generator.graph"
        )
        cover_module.generate_cover_thanks_pages_app = types.SimpleNamespace(ainvoke=self._noop_async)

        content_module = types.ModuleType(
            "core.ppt_generator.thought_to_ppt.page_generators.content_pages_generator.graph"
        )
        content_module.content_page_worker_app = types.SimpleNamespace(ainvoke=self._noop_async)

        sep_module = types.ModuleType(
            "core.ppt_generator.thought_to_ppt.page_generators.sep_pages_generator.node"
        )

        async def generate_sep_template_node(_state):
            return {"sep_template": "template"}

        async def generate_sep_page_node(_state):
            return {}

        sep_module.generate_sep_template_node = generate_sep_template_node
        sep_module.generate_sep_page_node = generate_sep_page_node

        toc_module = types.ModuleType(
            "core.ppt_generator.thought_to_ppt.page_generators.toc_page_generator.node"
        )

        async def generate_toc_page_node(_state):
            return {}

        toc_module.generate_toc_page_node = generate_toc_page_node

        return cover_module, content_module, sep_module, toc_module

    def _make_svg_subgraph_modules(self):
        cover_module = types.ModuleType(
            "core.ppt_generator.thought_to_ppt.svg_page_generators.cover_thanks_pages_generator.graph"
        )
        cover_module.generate_cover_thanks_pages_app = types.SimpleNamespace(ainvoke=self._noop_async)

        content_module = types.ModuleType(
            "core.ppt_generator.thought_to_ppt.svg_page_generators.content_pages_generator.graph"
        )
        content_module.content_page_worker_app = types.SimpleNamespace(ainvoke=self._noop_async)

        sep_module = types.ModuleType(
            "core.ppt_generator.thought_to_ppt.svg_page_generators.sep_pages_generator.node"
        )

        async def generate_sep_template_node(_state):
            return {"sep_template": "template"}

        async def generate_sep_page_node(_state):
            return {}

        sep_module.generate_sep_template_node = generate_sep_template_node
        sep_module.generate_sep_page_node = generate_sep_page_node

        toc_module = types.ModuleType(
            "core.ppt_generator.thought_to_ppt.svg_page_generators.toc_page_generator.node"
        )

        async def generate_toc_page_node(_state):
            return {}

        toc_module.generate_toc_page_node = generate_toc_page_node

        return cover_module, content_module, sep_module, toc_module

    def _make_common_module(self):
        module = types.ModuleType("core.ppt_generator.utils.common")

        def sanitize_filename(name):
            return name.replace(" ", "_")

        async def htmls_to_pptx(_htmls, save_dir, filename):
            return str(Path(save_dir) / f"{filename}.pdf"), str(Path(save_dir) / f"{filename}.pptx")

        module.sanitize_filename = sanitize_filename
        module.htmls_to_pptx = htmls_to_pptx
        return module

    def _make_svg_export_module(self):
        module = types.ModuleType("core.ppt_generator.utils.svg_export")

        async def svgs_to_pptx(_svgs, save_dir, filename):
            return "", str(Path(save_dir) / f"{filename}.pptx")

        module.svgs_to_pptx = svgs_to_pptx
        return module

    def _run_main(self, argv, cwd, render_mode="html"):
        cover_h, content_h, sep_h, toc_h = self._make_html_subgraph_modules()
        cover_s, content_s, sep_s, toc_s = self._make_svg_subgraph_modules()
        fake_modules = {
            "core.ppt_generator.thought_to_ppt.state": self._make_state_module(),
            "core.ppt_generator.thought_to_ppt.page_generators.node": self._make_html_page_generators_node(),
            "core.ppt_generator.thought_to_ppt.page_generators.cover_thanks_pages_generator.graph": cover_h,
            "core.ppt_generator.thought_to_ppt.page_generators.sep_pages_generator.node": sep_h,
            "core.ppt_generator.thought_to_ppt.page_generators.toc_page_generator.node": toc_h,
            "core.ppt_generator.thought_to_ppt.page_generators.content_pages_generator.graph": content_h,
            "core.ppt_generator.thought_to_ppt.svg_page_generators.node": self._make_svg_page_generators_node(),
            "core.ppt_generator.thought_to_ppt.svg_page_generators.cover_thanks_pages_generator.graph": cover_s,
            "core.ppt_generator.thought_to_ppt.svg_page_generators.sep_pages_generator.node": sep_s,
            "core.ppt_generator.thought_to_ppt.svg_page_generators.toc_page_generator.node": toc_s,
            "core.ppt_generator.thought_to_ppt.svg_page_generators.content_pages_generator.graph": content_s,
            "core.ppt_generator.utils.common": self._make_common_module(),
            "core.ppt_generator.utils.svg_export": self._make_svg_export_module(),
        }
        stdout = io.StringIO()

        def local_run_dir(run_id):
            out_dir = Path(cwd) / "output" / run_id
            out_dir.mkdir(parents=True, exist_ok=True)
            return str(out_dir)

        with contextlib.ExitStack() as stack:
            stack.enter_context(patch.dict(sys.modules, fake_modules))
            module = self._load_script_module()
            stack.enter_context(patch.object(sys, "argv", ["patch_render_missing.py", *argv]))
            stack.enter_context(patch.object(module, "run_dir", side_effect=local_run_dir))
            stack.enter_context(contextlib.redirect_stdout(stdout))
            module.asyncio.run(module.main())

        lines = [line for line in stdout.getvalue().splitlines() if line.strip()]
        return json.loads(lines[-1])

    def test_missing_outline_returns_structured_error(self):
        with tempfile.TemporaryDirectory() as tmp_dir:
            payload = self._run_main(["--run-id", "missing"], cwd=tmp_dir)

        self.assertEqual(payload["stage"], "missing_outline")
        self.assertIn("outline", payload["output"]["message"].lower())

    def test_empty_outline_returns_structured_error(self):
        with tempfile.TemporaryDirectory() as tmp_dir:
            outline_dir = Path(tmp_dir) / "output" / "empty" / "outline"
            outline_dir.mkdir(parents=True, exist_ok=True)
            (outline_dir / "outline.json").write_text(json.dumps({"topic": "Demo", "outline": []}), encoding="utf-8")

            payload = self._run_main(["--run-id", "empty"], cwd=tmp_dir)

        self.assertEqual(payload["stage"], "empty_outline")
        self.assertIn("empty", payload["output"]["message"].lower())

    def test_success_returns_structured_completed_payload(self):
        with tempfile.TemporaryDirectory() as tmp_dir:
            run_id = "success"
            out_dir = Path(tmp_dir) / "output" / run_id
            outline_dir = out_dir / "outline"
            slides_dir = Path(tmp_dir) / "rendered"
            outline_dir.mkdir(parents=True, exist_ok=True)
            slides_dir.mkdir(parents=True, exist_ok=True)
            (outline_dir / "outline.json").write_text(
                json.dumps(
                    {
                        "run_id": run_id,
                        "topic": "Demo Topic",
                        "outline": [
                            {
                                "title": "Cover",
                                "abstract": "Intro",
                                "type": 1,
                                "index": 0,
                                "reference_doc": "",
                                "reference_images": [],
                            }
                        ],
                    }
                ),
                encoding="utf-8",
            )
            (out_dir / "ppt.json").write_text(
                json.dumps({
                    "run_id": run_id,
                    "topic": "Demo Topic",
                    "render_mode": "html",
                    "slides_dir": str(slides_dir),
                }),
                encoding="utf-8",
            )
            (slides_dir / "0.html").write_text("<html></html>", encoding="utf-8")

            payload = self._run_main(["--run-id", run_id, "--indices", "0"], cwd=tmp_dir)
            ppt_payload = json.loads((out_dir / "ppt.json").read_text(encoding="utf-8"))

        self.assertEqual(payload["stage"], "completed")
        self.assertEqual(payload["output"]["stage"], "completed")
        self.assertTrue(payload["output"]["pdf_path"].endswith(".pdf"))
        self.assertEqual(ppt_payload["run_id"], run_id)
        self.assertEqual(ppt_payload["slides_dir"], str(slides_dir))

    def test_svg_success_returns_structured_completed_payload(self):
        with tempfile.TemporaryDirectory() as tmp_dir:
            run_id = "svg-success"
            out_dir = Path(tmp_dir) / "output" / run_id
            outline_dir = out_dir / "outline"
            slides_dir = Path(tmp_dir) / "rendered"
            outline_dir.mkdir(parents=True, exist_ok=True)
            slides_dir.mkdir(parents=True, exist_ok=True)
            (outline_dir / "outline.json").write_text(
                json.dumps(
                    {
                        "run_id": run_id,
                        "topic": "SVG Demo",
                        "outline": [
                            {
                                "title": "Cover",
                                "abstract": "Intro",
                                "type": 1,
                                "index": 0,
                                "reference_doc": "",
                                "reference_images": [],
                            }
                        ],
                    }
                ),
                encoding="utf-8",
            )
            (out_dir / "ppt.json").write_text(
                json.dumps(
                    {
                        "run_id": run_id,
                        "topic": "SVG Demo",
                        "render_mode": "svg",
                        "slides_dir": str(slides_dir),
                    }
                ),
                encoding="utf-8",
            )

            svg_dir = slides_dir
            svg_dir.mkdir(parents=True, exist_ok=True)
            (svg_dir / "01_Cover.svg").write_text(
                '<svg width="1280" height="720" viewBox="0 0 1280 720" xmlns="http://www.w3.org/2000/svg"></svg>',
                encoding="utf-8",
            )

            payload = self._run_main(
                ["--run-id", run_id, "--indices", "0"],
                cwd=tmp_dir,
                render_mode="svg",
            )
            ppt_payload = json.loads((out_dir / "ppt.json").read_text(encoding="utf-8"))

        self.assertEqual(payload["stage"], "completed")
        self.assertEqual(payload["output"]["stage"], "completed")
        self.assertEqual(payload["output"]["target_indices"], [0])
        self.assertEqual(ppt_payload["render_mode"], "svg")
        self.assertTrue(ppt_payload["pptx_path"].endswith(".pptx"))


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