#!/usr/bin/env python3
"""Tests for deterministic template governance and candidate import."""

from __future__ import annotations

import copy
import json
import tempfile
import unittest
from pathlib import Path

import template_library


SCRIPT_ROOT = Path(__file__).resolve().parents[1]


def valid_candidate() -> dict:
    modules = [
        {"id": "hook", "role": "hook-headline", "category": "core", "variants": ["split", "hero"]},
        {"id": "proof", "role": "source-proof", "category": "core", "variants": ["crop", "callout"]},
        {"id": "context", "role": "context-card", "category": "core", "variants": ["card"]},
        {"id": "feature", "role": "feature-list", "category": "core", "variants": ["stack", "grid"]},
        {"id": "process", "role": "process-diagram", "category": "core", "variants": ["rail", "nodes"]},
        {"id": "impact", "role": "impact-risk", "category": "core", "variants": ["balance", "alert"]},
        {"id": "takeaway", "role": "takeaway-source", "category": "core", "variants": ["summary"]},
        {"id": "outro", "role": "cta-outro", "category": "core", "variants": ["centered"]},
        {"id": "release", "role": "release-delta", "category": "domain-specialized", "variants": ["before-after"]},
        {"id": "commits", "role": "commit-timeline", "category": "domain-specialized", "variants": ["timeline"]},
        {"id": "terminal", "role": "terminal-proof", "category": "domain-specialized", "variants": ["terminal"]},
        {"id": "guardrail", "role": "permission-guardrail", "category": "domain-specialized", "variants": ["checklist"]},
    ]
    return {
        "schema_version": 1,
        "id": "complete-test",
        "name": "Complete Test",
        "domain": "developer-tools",
        "style": "workbench",
        "tier": "candidate",
        "status": "qa-ready",
        "readiness": "complete",
        "tags": ["repo"],
        "proof_treatments": ["release-crop"],
        "screenshot_rule": "Readable official crop only.",
        "modules": modules,
        "sequences": {
            "short": {"target_seconds": 30, "module_roles": ["hook-headline", "source-proof", "feature-list", "cta-outro"]},
            "standard": {"target_seconds": 50, "module_roles": ["hook-headline", "source-proof", "process-diagram", "impact-risk", "cta-outro"]},
            "extended": {"target_seconds": 75, "module_roles": ["hook-headline", "source-proof", "context-card", "process-diagram", "impact-risk", "takeaway-source", "cta-outro"]},
        },
        "sample_fixture": "templates/fixtures/ai-news-tech-sample.json",
        "qa": {"status": "pending", "failures": []},
        "promotion": {"status": "not-eligible", "approved_by_user": False},
    }


class ManifestContractTests(unittest.TestCase):
    def test_complete_candidate_passes(self) -> None:
        self.assertEqual([], template_library.validate_promotable_manifest(valid_candidate()))

    def test_missing_core_role_is_rejected(self) -> None:
        manifest = valid_candidate()
        manifest["modules"] = [module for module in manifest["modules"] if module["role"] != "source-proof"]
        self.assertIn("missing core role: source-proof", template_library.validate_promotable_manifest(manifest))

    def test_missing_variant_is_rejected(self) -> None:
        manifest = valid_candidate()
        manifest["modules"][0]["variants"] = ["hero"]
        self.assertIn(
            "role requires at least two layout variants: hook-headline",
            template_library.validate_promotable_manifest(manifest),
        )

    def test_missing_sequence_is_rejected(self) -> None:
        manifest = valid_candidate()
        del manifest["sequences"]["extended"]
        self.assertIn("missing sequence: extended", template_library.validate_promotable_manifest(manifest))

    def test_missing_metadata_is_rejected(self) -> None:
        manifest = valid_candidate()
        del manifest["proof_treatments"]
        self.assertIn("missing required metadata: proof_treatments", template_library.validate_promotable_manifest(manifest))


class CandidateImportTests(unittest.TestCase):
    def setUp(self) -> None:
        self.temp = tempfile.TemporaryDirectory()
        self.root = Path(self.temp.name)
        (self.root / "templates" / "imports").mkdir(parents=True)
        (self.root / "templates" / "fixtures").mkdir(parents=True)
        (self.root / "templates" / "imports" / "ai-news-preview.json").write_text(
            (SCRIPT_ROOT / "templates" / "imports" / "ai-news-preview.json").read_text(encoding="utf-8"),
            encoding="utf-8",
        )
        (self.root / "templates" / "fixtures" / "ai-news-tech-sample.json").write_text("{}\n", encoding="utf-8")

    def tearDown(self) -> None:
        self.temp.cleanup()

    def test_import_keeps_every_record_candidate_only(self) -> None:
        result = template_library.import_inventory(self.root)
        self.assertEqual("pass", result["status"])
        registry = json.loads((self.root / "templates" / "registry.json").read_text(encoding="utf-8"))
        self.assertGreaterEqual(len(registry["sets"]), 20)
        self.assertTrue(all(entry["tier"] == "candidate" for entry in registry["sets"]))
        self.assertTrue(all(entry["readiness"] == "needs-expansion" for entry in registry["sets"]))
        self.assertEqual("pass", template_library.validate_registry(self.root)["status"])

    def test_registry_rejects_missing_target(self) -> None:
        template_library.import_inventory(self.root)
        registry_path = self.root / "templates" / "registry.json"
        registry = json.loads(registry_path.read_text(encoding="utf-8"))
        registry["sets"][0]["manifest_path"] = "templates/domains/ai-news/candidate/missing/manifest.json"
        registry_path.write_text(json.dumps(registry), encoding="utf-8")
        self.assertEqual("fail", template_library.validate_registry(self.root)["status"])

    def test_registry_rejects_malformed_tier(self) -> None:
        template_library.import_inventory(self.root)
        registry_path = self.root / "templates" / "registry.json"
        registry = json.loads(registry_path.read_text(encoding="utf-8"))
        registry["sets"][0]["tier"] = "default"
        registry_path.write_text(json.dumps(registry), encoding="utf-8")
        self.assertEqual("fail", template_library.validate_registry(self.root)["status"])

    def test_import_preserves_already_approved_record(self) -> None:
        template_library.import_inventory(self.root)
        registry_path = self.root / "templates" / "registry.json"
        registry = json.loads(registry_path.read_text(encoding="utf-8"))
        registry["sets"][0]["tier"] = "approved"
        registry["sets"][0]["manifest_path"] = "templates/domains/ai-news/approved/chip-lab/manifest.json"
        registry_path.write_text(json.dumps(registry), encoding="utf-8")
        result = template_library.import_inventory(self.root)
        refreshed = json.loads(registry_path.read_text(encoding="utf-8"))
        record = next(item for item in refreshed["sets"] if item["id"] == "chip-lab")
        self.assertEqual("pass", result["status"])
        self.assertEqual("approved", record["tier"])


class RegistryEncodingTests(unittest.TestCase):
    def setUp(self) -> None:
        self.temp = tempfile.TemporaryDirectory()
        self.root = Path(self.temp.name)
        fixture = self.root / "templates" / "fixtures" / "ai-news-tech-sample.json"
        fixture.parent.mkdir(parents=True)
        fixture.write_text("{}\n", encoding="utf-8")
        self.manifest = valid_candidate()
        self.manifest["id"] = "repo-update-fullframe"
        self.manifest["tier"] = "approved"
        self.manifest["status"] = "approved"
        self.manifest["promotion"] = {
            "status": "approved",
            "approved_by_user": True,
            "approval_signal": "Tạm thời ổn rồi, đến bước tiếp theo",
            "follow_up_note": "Bổ sung support-zone tuỳ chọn cho phần dưới.",
        }
        self.manifest_path = (
            self.root
            / "templates"
            / "domains"
            / "developer-tools"
            / "approved"
            / "repo-update-fullframe"
            / "manifest.json"
        )
        self.manifest_path.parent.mkdir(parents=True)
        registry_path = self.root / "templates" / "registry.json"
        registry_path.parent.mkdir(parents=True, exist_ok=True)
        registry_path.write_text(
            json.dumps(
                {
                    "policy": {"implicit_approval": False},
                    "sets": [
                        {
                            "id": "repo-update-fullframe",
                            "domain": "developer-tools",
                            "tier": "approved",
                            "manifest_path": "templates/domains/developer-tools/approved/repo-update-fullframe/manifest.json",
                        }
                    ],
                },
                ensure_ascii=False,
            ),
            encoding="utf-8",
        )

    def tearDown(self) -> None:
        self.temp.cleanup()

    def write_manifest(self) -> None:
        self.manifest_path.write_text(
            json.dumps(self.manifest, ensure_ascii=False),
            encoding="utf-8",
        )

    def test_clean_vietnamese_approved_metadata_passes(self) -> None:
        self.write_manifest()
        self.assertEqual("pass", template_library.validate_registry(self.root)["status"])

    def test_mojibake_approved_metadata_is_rejected(self) -> None:
        self.manifest["promotion"]["approval_signal"] = "T\u00c3\u00a1m th\u00e1\u00bb\u009di \u00e1\u00bb\u0095n"
        self.write_manifest()
        result = template_library.validate_registry(self.root)
        self.assertEqual("fail", result["status"])
        self.assertTrue(any("mojibake" in error for error in result["errors"]))


class QAEvidenceTests(unittest.TestCase):
    def setUp(self) -> None:
        self.temp = tempfile.TemporaryDirectory()
        self.root = Path(self.temp.name)
        fixture = self.root / "templates" / "fixtures" / "ai-news-tech-sample.json"
        fixture.parent.mkdir(parents=True)
        fixture.write_text("{}\n", encoding="utf-8")
        manifest = valid_candidate()
        manifest["id"] = "repo-update-fullframe"
        candidate_path = self.root / "templates" / "domains" / "developer-tools" / "candidate" / manifest["id"] / "manifest.json"
        candidate_path.parent.mkdir(parents=True)
        candidate_path.write_text(json.dumps(manifest), encoding="utf-8")
        registry = {
            "policy": {"implicit_approval": False},
            "sets": [{
                "id": manifest["id"],
                "domain": "developer-tools",
                "tier": "candidate",
                "manifest_path": "templates/domains/developer-tools/candidate/repo-update-fullframe/manifest.json",
            }],
        }
        registry_path = self.root / "templates" / "registry.json"
        registry_path.parent.mkdir(parents=True, exist_ok=True)
        registry_path.write_text(json.dumps(registry), encoding="utf-8")
        self.evidence = self.root / "evidence"
        for sequence in template_library.SEQUENCE_RANGES:
            composition = self.evidence / sequence / "index.html"
            composition.parent.mkdir(parents=True)
            composition.write_text("SOURCE PROOF EXPLAINER", encoding="utf-8")
            render = self.evidence / "renders" / f"{sequence}.mp4"
            render.parent.mkdir(parents=True, exist_ok=True)
            render.write_bytes(b"render")
            contact = self.evidence / "capture" / f"{sequence}-contact-sheet.jpg"
            contact.parent.mkdir(parents=True, exist_ok=True)
            contact.write_bytes(b"sheet")

    def tearDown(self) -> None:
        self.temp.cleanup()

    def test_qa_evidence_pass_never_promotes_candidate(self) -> None:
        result = template_library.qa_evidence(self.root, "repo-update-fullframe", self.evidence, "pass")
        report = json.loads((self.evidence / "qa-report.json").read_text(encoding="utf-8"))
        self.assertEqual("pass", result["status"])
        self.assertFalse(report["promotion"]["approved_by_user"])
        self.assertEqual("blocked-pending-user-approval", report["promotion"]["status"])

    def test_qa_evidence_requires_all_three_contact_sheets(self) -> None:
        (self.evidence / "capture" / "extended-contact-sheet.jpg").unlink()
        result = template_library.qa_evidence(self.root, "repo-update-fullframe", self.evidence, "pass")
        self.assertEqual("fail", result["status"])

    def test_promotion_requires_explicit_approval_and_qa(self) -> None:
        template_library.qa_evidence(self.root, "repo-update-fullframe", self.evidence, "pass")
        report_path = self.evidence / "qa-report.json"
        rejected = template_library.promote_candidate(self.root, "repo-update-fullframe", report_path, "continue", None)
        self.assertEqual("fail", rejected["status"])
        accepted = template_library.promote_candidate(self.root, "repo-update-fullframe", report_path, "approved", "Add support-zone later.")
        self.assertEqual("pass", accepted["status"])
        registry = json.loads((self.root / "templates" / "registry.json").read_text(encoding="utf-8"))
        self.assertEqual("approved", registry["sets"][0]["tier"])


class RecommendationTests(unittest.TestCase):
    def setUp(self) -> None:
        self.temp = tempfile.TemporaryDirectory()
        self.root = Path(self.temp.name)
        registry_path = self.root / "templates" / "registry.json"
        registry_path.parent.mkdir(parents=True)
        registry_path.write_text(json.dumps({
            "sets": [
                {"id": "repo", "tier": "approved", "domain": "developer-tools", "tags": ["github", "release"], "proof_treatments": ["release-crop"]},
                {"id": "terminal", "tier": "approved", "domain": "developer-tools", "tags": ["cli"], "proof_treatments": ["docs-callout"]},
                {"id": "news", "tier": "approved", "domain": "ai-news", "tags": ["launch"], "proof_treatments": ["official-source-card"]},
                {"id": "candidate-only", "tier": "candidate", "domain": "developer-tools", "tags": ["github"], "proof_treatments": ["release-crop"]},
            ]
        }), encoding="utf-8")

    def tearDown(self) -> None:
        self.temp.cleanup()

    def test_recommendation_prioritizes_proof_fit_and_never_returns_candidate(self) -> None:
        result = template_library.recommend(
            self.root, "developer-tools", ["github"], ["release-crop"], "ai-news-247", ["repo"]
        )
        self.assertEqual("repo", result["choices"][0]["id"])
        self.assertTrue(result["choices"][0]["warnings"])
        self.assertTrue(all(choice["tier"] == "approved" for choice in result["choices"]))
        self.assertNotIn("candidate-only", [choice["id"] for choice in result["choices"]])

    def test_recommendation_reports_approved_shortage(self) -> None:
        result = template_library.recommend(
            self.root, "security", ["mcp"], ["official-source-only"], "ai-news-247", []
        )
        self.assertEqual(3 - len(result["choices"]), result["approved_shortage"])


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