"""Tests for vendor-aware agent sync.

@covers ADR-0.16.0  OBPI-0.16.0-04 template-engine
@covers ADR-0.17.0  OBPI-0.17.0-02 rules-mirroring
"""

import tempfile
import unittest
from pathlib import Path

from gzkit.config import GzkitConfig, VendorConfig, VendorsConfig
from gzkit.rules import (
    CanonicalRule,
    RuleFrontmatter,
    render_rule_for_claude,
    render_rule_for_copilot,
    render_rules_to_dir,
)
from gzkit.sync import sync_all
from gzkit.traceability import covers


def _make_rule(
    rule_id: str = "test-rule",
    paths: list[str] | None = None,
    description: str = "Test rule",
    body: str = "# Test Rule\n\nRule content here.",
) -> CanonicalRule:
    """Create a CanonicalRule for testing."""
    return CanonicalRule(
        frontmatter=RuleFrontmatter(
            id=rule_id,
            paths=paths or ["**/*"],
            description=description,
        ),
        body=body,
        source_path=f".gzkit/rules/{rule_id}.md",
    )


def _write_canonical_rule(rules_dir: Path, rule_id: str, paths: list[str], body: str) -> None:
    """Write a canonical rule file to disk."""
    paths_yaml = "\n".join(f'  - "{p}"' for p in paths)
    content = f"---\nid: {rule_id}\npaths:\n{paths_yaml}\ndescription: Test rule\n---\n\n{body}\n"
    (rules_dir / f"{rule_id}.md").write_text(content, encoding="utf-8")


class TestRenderRuleForClaude(unittest.TestCase):
    """Tests for Claude rule rendering.

    @covers REQ-0.17.0-02-02
    """

    @covers("REQ-0.17.0-02-02")
    @covers("REQ-0.14.0-01-03")
    def test_global_rule_omits_frontmatter(self) -> None:
        """Global rules (paths: **/*) have no YAML frontmatter."""
        rule = _make_rule(paths=["**/*"], body="# Global Rule")
        rendered = render_rule_for_claude(rule)
        self.assertNotIn("---", rendered)
        self.assertIn("# Global Rule", rendered)
        self.assertIn("Generated by gz agent sync", rendered)

    @covers("REQ-0.14.0-02-02")
    @covers("REQ-0.17.0-02-02")
    def test_scoped_rule_has_paths_frontmatter(self) -> None:
        """Scoped rules get paths: YAML frontmatter."""
        rule = _make_rule(paths=["src/**/*.py", "tests/**"], body="# Scoped Rule")
        rendered = render_rule_for_claude(rule)
        self.assertIn("---", rendered)
        self.assertIn('  - "src/**/*.py"', rendered)
        self.assertIn('  - "tests/**"', rendered)
        self.assertIn("# Scoped Rule", rendered)
        self.assertIn("Generated by gz agent sync", rendered)

    @covers("REQ-0.17.0-02-02")
    def test_scoped_rule_frontmatter_starts_at_line_one(self) -> None:
        """Claude Code's parser requires `---` on line 1 of scoped rules.

        A leading HTML comment causes the parser to fall back to
        unconditional load, defeating path-gated scoping (GHI #361).
        """
        rule = _make_rule(paths=["tests/**"], body="# Scoped Rule")
        rendered = render_rule_for_claude(rule)
        self.assertTrue(
            rendered.startswith("---\n"),
            f"Scoped rule must start with frontmatter delimiter, got: {rendered[:80]!r}",
        )

    @covers("REQ-0.17.0-02-02")
    def test_global_rule_has_no_frontmatter_block(self) -> None:
        """Global rules (paths: **/*) keep the generator header at top.

        Global rules omit frontmatter entirely, so the do-not-edit comment
        can lead the file without breaking any parser.
        """
        rule = _make_rule(paths=["**/*"], body="# Global")
        rendered = render_rule_for_claude(rule)
        self.assertNotIn("---", rendered)
        self.assertTrue(rendered.startswith("<!-- Generated by gz agent sync"))


class TestRenderRuleForCopilot(unittest.TestCase):
    """Tests for Copilot rule rendering.

    @covers REQ-0.17.0-02-03
    """

    @covers("REQ-0.17.0-02-03")
    def test_renders_apply_to_frontmatter(self) -> None:
        """Copilot rules get applyTo: frontmatter."""
        rule = _make_rule(paths=["src/**/*.py", "tests/**"], body="# Copilot Rule")
        rendered = render_rule_for_copilot(rule)
        self.assertIn('applyTo: "src/**/*.py, tests/**"', rendered)
        self.assertIn("# Copilot Rule", rendered)
        self.assertIn("Generated by gz agent sync", rendered)

    @covers("REQ-0.17.0-02-03")
    def test_global_rule_uses_star_apply_to(self) -> None:
        """Global rules use applyTo: **/*."""
        rule = _make_rule(paths=["**/*"], body="# Global")
        rendered = render_rule_for_copilot(rule)
        self.assertIn('applyTo: "**/*"', rendered)


class TestRenderRulesToDir(unittest.TestCase):
    """Tests for batch rendering to directory.

    @covers REQ-0.17.0-02-04
    @covers REQ-0.17.0-02-05
    """

    @covers("REQ-0.14.0-01-01")
    @covers("REQ-0.17.0-02-04")
    def test_claude_rendering_writes_files(self) -> None:
        """render_rules_to_dir writes Claude-format files."""
        rules = [
            _make_rule(rule_id="governance-core", paths=["**/*"], body="# Governance"),
            _make_rule(rule_id="models", paths=["src/**/*.py"], body="# Models"),
        ]
        with tempfile.TemporaryDirectory() as tmpdir:
            target = Path(tmpdir) / ".claude" / "rules"
            written = render_rules_to_dir(rules, target, "claude")
            self.assertEqual(len(written), 2)
            governance = (target / "governance-core.md").read_text(encoding="utf-8")
            self.assertNotIn("paths:", governance)
            self.assertIn("# Governance", governance)
            models = (target / "models.md").read_text(encoding="utf-8")
            self.assertIn("paths:", models)

    @covers("REQ-0.17.0-02-05")
    def test_copilot_rendering_writes_files(self) -> None:
        """render_rules_to_dir writes Copilot-format files."""
        rules = [_make_rule(rule_id="tests", paths=["tests/**"], body="# Tests")]
        with tempfile.TemporaryDirectory() as tmpdir:
            target = Path(tmpdir) / ".github" / "instructions"
            written = render_rules_to_dir(rules, target, "copilot")
            self.assertEqual(len(written), 1)
            tests_file = target / "tests.instructions.md"
            self.assertTrue(tests_file.exists())
            content = tests_file.read_text(encoding="utf-8")
            self.assertIn('applyTo: "tests/**"', content)

    @covers("REQ-0.17.0-02-04")
    def test_stale_generated_files_removed(self) -> None:
        """Generated files not in current rule set are cleaned up."""
        with tempfile.TemporaryDirectory() as tmpdir:
            target = Path(tmpdir) / "rules"
            target.mkdir()
            stale = target / "old-rule.md"
            stale.write_text(
                "<!-- Generated by gz agent sync — do not edit -->\nStale",
                encoding="utf-8",
            )

            rules = [_make_rule(rule_id="new-rule", body="# New")]
            render_rules_to_dir(rules, target, "claude")
            self.assertFalse(stale.exists())
            self.assertTrue((target / "new-rule.md").exists())

    @covers("REQ-0.14.0-02-03")
    def test_hand_authored_files_preserved(self) -> None:
        """Files without generated marker are not removed."""
        with tempfile.TemporaryDirectory() as tmpdir:
            target = Path(tmpdir) / "rules"
            target.mkdir()
            manual = target / "custom.md"
            manual.write_text("# Hand-authored rule\n", encoding="utf-8")

            rules = [_make_rule(rule_id="generated", body="# Gen")]
            render_rules_to_dir(rules, target, "claude")
            self.assertTrue(manual.exists())


class TestSyncAllVendorAware(unittest.TestCase):
    """Tests for vendor-aware sync_all()."""

    @covers("REQ-0.14.0-02-01")
    def test_disabled_vendor_produces_no_skill_mirrors(self) -> None:
        """Disabled vendors do not get skill mirrors."""
        import json

        with tempfile.TemporaryDirectory() as tmpdir:
            project_root = Path(tmpdir)
            vendors = VendorsConfig(
                claude=VendorConfig(enabled=True, surface_root=".claude"),
                copilot=VendorConfig(enabled=False, surface_root=".github"),
                codex=VendorConfig(enabled=False, surface_root=".agents"),
            )
            config = GzkitConfig(project_name="test", vendors=vendors)

            # Write manifest with vendors to enable vendor-aware gating
            gzkit_dir = project_root / ".gzkit"
            gzkit_dir.mkdir(parents=True)
            manifest = {
                "schema": "gzkit.manifest.v2",
                "vendors": {
                    "claude": {
                        "enabled": True,
                        "surface_root": ".claude",
                        "instruction_format": "claude-rules",
                    },
                    "copilot": {
                        "enabled": False,
                        "surface_root": ".github",
                        "instruction_format": "github-instructions",
                    },
                    "codex": {
                        "enabled": False,
                        "surface_root": ".agents",
                        "instruction_format": "generic",
                    },
                },
            }
            (gzkit_dir / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8")

            # Create a canonical skill
            skills_dir = project_root / ".gzkit" / "skills" / "test-skill"
            skills_dir.mkdir(parents=True)
            (skills_dir / "SKILL.md").write_text(
                "---\nname: test-skill\ndescription: Demo\n"
                "lifecycle_state: active\nowner: test\n"
                "last_reviewed: 2026-03-18\n---\n\n# Test Skill\n",
                encoding="utf-8",
            )

            sync_all(project_root, config)

            # Claude skills should be mirrored (enabled)
            claude_skill = project_root / ".claude" / "skills" / "test-skill" / "SKILL.md"
            self.assertTrue(claude_skill.exists())

            # Copilot skills should NOT be mirrored (disabled)
            copilot_skill = project_root / ".github" / "skills" / "test-skill" / "SKILL.md"
            self.assertFalse(copilot_skill.exists())

            # Codex skills should NOT be mirrored (disabled)
            codex_skill = project_root / ".agents" / "skills" / "test-skill" / "SKILL.md"
            self.assertFalse(codex_skill.exists())

    @covers("REQ-0.17.0-02-05")
    def test_enabled_vendor_gets_canonical_rules(self) -> None:
        """Enabled vendors get rules rendered from canonical source."""
        with tempfile.TemporaryDirectory() as tmpdir:
            project_root = Path(tmpdir)
            config = GzkitConfig(project_name="test")

            # Create canonical rules
            rules_dir = project_root / ".gzkit" / "rules"
            rules_dir.mkdir(parents=True)
            _write_canonical_rule(rules_dir, "models", ["src/**/*.py"], "# Model Policy")

            sync_all(project_root, config)

            # Claude rules should exist (claude enabled by default)
            claude_rule = project_root / ".claude" / "rules" / "models.md"
            self.assertTrue(claude_rule.exists())
            content = claude_rule.read_text(encoding="utf-8")
            self.assertIn("Generated by gz agent sync", content)
            self.assertIn("# Model Policy", content)

    @covers("REQ-0.14.0-01-02")
    def test_existing_skill_mirroring_preserved(self) -> None:
        """REQ-7: Existing skill mirroring behavior preserved."""
        with tempfile.TemporaryDirectory() as tmpdir:
            project_root = Path(tmpdir)
            config = GzkitConfig(project_name="test")

            skills_dir = project_root / ".gzkit" / "skills" / "demo"
            skills_dir.mkdir(parents=True)
            (skills_dir / "SKILL.md").write_text(
                "---\nname: demo\ndescription: Demo\n"
                "lifecycle_state: active\nowner: test\n"
                "last_reviewed: 2026-03-18\n---\n\n# Demo\n",
                encoding="utf-8",
            )

            sync_all(project_root, config)

            claude_skill = project_root / ".claude" / "skills" / "demo" / "SKILL.md"
            self.assertTrue(claude_skill.exists())


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