"""Unit tests for the #189 bootstrap-template parity check.

These pin the parser + schema-validation logic against synthetic
fixtures in tmp_path. The live-file pin (the actual regression
gate) is wired into ``validate_release`` so it runs against the
target project_dir outside the bazel sandbox.
"""
from __future__ import annotations

from pathlib import Path

import pytest

from active_claude_github_mcp.bootstrap_template_parity import (
    BOOTSTRAP_REL,
    SECTION_HEADING,
    TemplateParityError,
    extract_section_json,
    find_bootstrap_md,
    main,
    normalize_placeholders,
    validate_template,
)
from active_claude_github_mcp.models import GithubConfig


_GOOD_TEMPLATE = """\
preamble line
#### 5g — Commit github-config.json

```json
{
  "version": "1.0.0",
  "skill": "active-claude-github",
  "account_type": "org | personal",
  "owner": "[org-name]",
  "repos": {
    "public": "[org]/[project-name]",
    "private": "[org]/[project-name]-dev"
  },
  "project_board": {
    "id": "[node-id]",
    "number": 1,
    "url": "[url]",
    "status_field_id": "[node-id]",
    "status_options": {
      "Backlog": "[option-id]",
      "In Progress": "[option-id]",
      "In Review": "[option-id]",
      "Done": "[option-id]",
      "Parked": "[option-id]"
    }
  },
  "project_discussion_id": "[node-id]",
  "session_lock_discussion_id": "[node-id]",
  "discussion_categories": {
    "Announcements": { "id": "[node-id]", "isAnswerable": false },
    "Architecture": { "id": "[node-id]", "isAnswerable": true },
    "UX / UI": { "id": "[node-id]", "isAnswerable": true },
    "Ideas": { "id": "[node-id]", "isAnswerable": false },
    "claude-use-only": { "id": "[node-id]", "isAnswerable": false }
  },
  "skill_version_at_bootstrap": "2.0.1",
  "bootstrapped_at": "2026-04-27T00:00:00Z"
}
```

trailing
"""


def _seed_bootstrap_md(tmp_path: Path, content: str = _GOOD_TEMPLATE) -> Path:
    skill_dir = tmp_path / ".claude" / "skills" / "active-claude-github"
    skill_dir.mkdir(parents=True)
    path = skill_dir / "bootstrap.md"
    path.write_text(content)
    return path


class TestExtractSectionJson:
    def test_extracts_block(self) -> None:
        text = (
            "preamble\n"
            "#### 5g — Commit github-config.json\n"
            "\n"
            "```json\n"
            '{"hello": "world"}\n'
            "```\n"
            "trailing\n"
        )
        assert extract_section_json(text) == '{"hello": "world"}'

    def test_missing_heading_raises(self) -> None:
        with pytest.raises(TemplateParityError, match="heading not found"):
            extract_section_json("nothing here", heading="### Missing")

    def test_missing_fence_raises(self) -> None:
        text = SECTION_HEADING + "\n\nno fence ahead\n"
        with pytest.raises(TemplateParityError, match="no ```json fence"):
            extract_section_json(text)

    def test_unterminated_fence_raises(self) -> None:
        text = SECTION_HEADING + "\n\n```json\n{ open forever\n"
        with pytest.raises(TemplateParityError, match="unterminated"):
            extract_section_json(text)


class TestNormalizePlaceholders:
    def test_rewrites_account_type_pick(self) -> None:
        raw = '{"account_type": "org | personal"}'
        assert normalize_placeholders(raw) == '{"account_type": "org"}'

    def test_leaves_explicit_account_type_alone(self) -> None:
        raw = '{"account_type": "personal"}'
        assert normalize_placeholders(raw) == raw

    def test_rewrites_node_id_placeholder(self) -> None:
        raw = '{"id": "[node-id]"}'
        assert "[node-id]" not in normalize_placeholders(raw)
        # Stand-in must be long enough for the ids_look_like_node_ids check.
        rewritten = normalize_placeholders(raw)
        # Extract the rewritten id value and assert length >= 10.
        import json as _json
        assert len(_json.loads(rewritten)["id"]) >= 10


class TestValidateTemplateGoldenPath:
    def test_synthetic_template_validates(self, tmp_path: Path) -> None:
        _seed_bootstrap_md(tmp_path)
        config = validate_template(tmp_path)
        assert isinstance(config, GithubConfig)
        # Owner is a free-form string placeholder; node-ids get
        # rewritten by normalize_placeholders before validation.
        assert config.owner == "[org-name]"
        assert config.project_discussion_id != "[node-id]"

    def test_find_bootstrap_md_returns_path(self, tmp_path: Path) -> None:
        path = _seed_bootstrap_md(tmp_path)
        assert find_bootstrap_md(tmp_path) == path

    def test_find_bootstrap_md_returns_none_when_missing(
        self, tmp_path: Path
    ) -> None:
        assert find_bootstrap_md(tmp_path) is None


class TestValidateTemplateErrors:
    def test_missing_bootstrap_md_raises(self, tmp_path: Path) -> None:
        with pytest.raises(TemplateParityError, match="bootstrap.md not found"):
            validate_template(tmp_path)

    def test_invalid_json_raises(self, tmp_path: Path) -> None:
        _seed_bootstrap_md(
            tmp_path,
            f"{SECTION_HEADING}\n\n```json\nnot valid json\n```\n",
        )
        with pytest.raises(TemplateParityError, match="not valid JSON"):
            validate_template(tmp_path)

    def test_schema_drift_raises_on_legacy_org_field(
        self, tmp_path: Path
    ) -> None:
        """The exact #189 regression — `org` instead of `owner` must fail."""
        drifted = _GOOD_TEMPLATE.replace(
            '"owner": "[org-name]"', '"org": "[org-name]"'
        )
        _seed_bootstrap_md(tmp_path, drifted)
        with pytest.raises(TemplateParityError, match="GithubConfig schema"):
            validate_template(tmp_path)


class TestMain:
    def test_main_returns_zero_on_pass(
        self, tmp_path: Path, capsys: pytest.CaptureFixture[str]
    ) -> None:
        _seed_bootstrap_md(tmp_path)
        rc = main(["--project-dir", str(tmp_path)])
        assert rc == 0
        captured = capsys.readouterr()
        assert "parses as GithubConfig" in captured.out

    def test_main_returns_one_on_failure(
        self, tmp_path: Path, capsys: pytest.CaptureFixture[str]
    ) -> None:
        rc = main(["--project-dir", str(tmp_path)])
        assert rc == 1
        captured = capsys.readouterr()
        assert "::error::" in captured.err


if __name__ == "__main__":
    import sys
    sys.exit(pytest.main([__file__, "-v"]))
