"""Unit tests for the #184 plan-completeness verifier.

Covers the YAML frontmatter parser, AC ID extraction, and the
orchestrator's report shape across well-formed plans, missing
frontmatter, malformed frontmatter, missing required fields, AC
mismatch, malformed paths, and duplicate IDs.
"""
from __future__ import annotations

import pytest

from active_claude_github_mcp.validators.plan import (
    FrontmatterParseError,
    extract_ac_ids_from_body,
    parse_plan_frontmatter,
    verify_plan_completeness,
)


_GOOD_PLAN = """\
---
plan_id: "2.1.0-example"
touched_files:
  - mcp-server/src/foo.py
  - .claude/skills/active-claude-github/validation.md
entities_introduced:
  - "verify_plan_completeness"
  - "PlanFrontmatter"
entities_modified:
  - "build_server"
assumptions:
  - "All handlers are async def"
acceptance_criteria:
  - id: AC1
    description: "Tool registered in tool_schemas.py"
  - id: AC2
    description: "Test coverage for golden + sad paths"
---

# Plan: example

Body content here.

## Acceptance criteria

- [ ] (AC1) Tool registered in tool_schemas.py
- [ ] (AC2) Test coverage for golden + sad paths
"""


class TestParseFrontmatter:
    def test_no_frontmatter_returns_none(self):
        assert parse_plan_frontmatter("# Plan\n\nbody\n") is None

    def test_unterminated_frontmatter_raises(self):
        with pytest.raises(FrontmatterParseError, match="not terminated"):
            parse_plan_frontmatter("---\nplan_id: x\nno close marker\n")

    def test_parses_scalars(self):
        body = '---\nplan_id: "abc"\n---\n\nbody\n'
        out = parse_plan_frontmatter(body)
        assert out == {"plan_id": "abc"}

    def test_parses_string_list(self):
        body = '---\ntouched_files:\n  - foo.py\n  - bar.py\n---\n\nbody\n'
        out = parse_plan_frontmatter(body)
        assert out == {"touched_files": ["foo.py", "bar.py"]}

    def test_parses_dict_list(self):
        body = (
            "---\nacceptance_criteria:\n"
            "  - id: AC1\n"
            "    description: \"first\"\n"
            "  - id: AC2\n"
            "    description: \"second\"\n"
            "---\n\nbody\n"
        )
        out = parse_plan_frontmatter(body)
        assert out["acceptance_criteria"] == [
            {"id": "AC1", "description": "first"},
            {"id": "AC2", "description": "second"},
        ]

    def test_strips_inline_comments(self):
        body = '---\nplan_id: "abc"  # trailing comment\n---\n\nbody\n'
        out = parse_plan_frontmatter(body)
        assert out == {"plan_id": "abc"}

    def test_full_good_plan_parses(self):
        out = parse_plan_frontmatter(_GOOD_PLAN)
        assert out["plan_id"] == "2.1.0-example"
        assert out["touched_files"] == [
            "mcp-server/src/foo.py",
            ".claude/skills/active-claude-github/validation.md",
        ]
        assert out["entities_introduced"] == [
            "verify_plan_completeness",
            "PlanFrontmatter",
        ]
        assert out["assumptions"] == ["All handlers are async def"]
        assert len(out["acceptance_criteria"]) == 2


class TestExtractAcIdsFromBody:
    def test_pulls_ac_prefixes_from_acceptance_section(self):
        body = (
            "## Acceptance criteria\n\n"
            "- [ ] (AC1) first\n"
            "- [ ] (AC2) second\n"
            "- [ ] (AC3) third\n"
        )
        assert extract_ac_ids_from_body(body) == ["AC1", "AC2", "AC3"]

    def test_ignores_ac_outside_acceptance_section(self):
        body = (
            "## Background\n\n(AC99) not here\n\n"
            "## Acceptance criteria\n\n- [ ] (AC1) only this\n"
        )
        assert extract_ac_ids_from_body(body) == ["AC1"]

    def test_no_acceptance_section_returns_empty(self):
        assert extract_ac_ids_from_body("# Plan\n\nbody\n") == []

    def test_handles_case_insensitive_heading(self):
        body = "## ACCEPTANCE CRITERIA\n\n- [ ] (AC1) yes\n"
        assert extract_ac_ids_from_body(body) == ["AC1"]


class TestVerifyPlanCompleteness:
    def test_good_plan_returns_valid(self):
        result = verify_plan_completeness(_GOOD_PLAN)
        assert result["valid"] is True, result["errors"]
        assert result["errors"] == []
        # No assumptions warning since the field is set.
        assert not any(
            w["field"] == "assumptions" for w in result["warnings"]
        )

    def test_no_frontmatter_returns_valid_with_warning(self):
        """AC4 — graceful degradation."""
        result = verify_plan_completeness("# Plan\n\nbody\n")
        assert result["valid"] is True
        assert result["errors"] == []
        assert any(
            "no YAML frontmatter" in w["issue"] for w in result["warnings"]
        )

    def test_malformed_frontmatter_returns_invalid(self):
        body = "---\nplan_id: \"x\"\nno_close_marker\n"
        result = verify_plan_completeness(body)
        assert result["valid"] is False
        assert any(
            e["field"] == "frontmatter" and "parse error" in e["issue"]
            for e in result["errors"]
        )

    def test_missing_required_field_errors(self):
        body = (
            "---\nplan_id: \"x\"\n"
            "acceptance_criteria:\n  - id: AC1\n    description: \"d\"\n"
            "---\n\n## Acceptance criteria\n- [ ] (AC1) d\n"
        )
        result = verify_plan_completeness(body)
        assert result["valid"] is False
        assert any(
            e["field"] == "touched_files" and "missing" in e["issue"]
            for e in result["errors"]
        )

    def test_ac_in_yaml_but_not_in_body_errors(self):
        body = (
            "---\nplan_id: \"x\"\n"
            "touched_files:\n  - f.py\n"
            "acceptance_criteria:\n"
            "  - id: AC1\n    description: \"d1\"\n"
            "  - id: AC2\n    description: \"d2\"\n"
            "---\n\n## Acceptance criteria\n- [ ] (AC1) d1\n"
        )
        result = verify_plan_completeness(body)
        assert result["valid"] is False
        assert any(
            "AC2" in e["issue"] and "markdown checkbox list" in e["issue"]
            for e in result["errors"]
        )

    def test_ac_in_body_but_not_in_yaml_errors(self):
        body = (
            "---\nplan_id: \"x\"\n"
            "touched_files:\n  - f.py\n"
            "acceptance_criteria:\n"
            "  - id: AC1\n    description: \"d\"\n"
            "---\n\n## Acceptance criteria\n"
            "- [ ] (AC1) d\n- [ ] (AC2) extra\n"
        )
        result = verify_plan_completeness(body)
        assert result["valid"] is False
        assert any(
            "AC2" in e["field"] and "no entry in the YAML" in e["issue"]
            for e in result["errors"]
        )

    def test_duplicate_ac_id_errors(self):
        body = (
            "---\nplan_id: \"x\"\n"
            "touched_files:\n  - f.py\n"
            "acceptance_criteria:\n"
            "  - id: AC1\n    description: \"d1\"\n"
            "  - id: AC1\n    description: \"d2\"\n"
            "---\n\n## Acceptance criteria\n- [ ] (AC1) d\n"
        )
        result = verify_plan_completeness(body)
        assert result["valid"] is False
        assert any(
            "duplicate id" in e["issue"] for e in result["errors"]
        )

    def test_malformed_ac_id_errors(self):
        body = (
            "---\nplan_id: \"x\"\n"
            "touched_files:\n  - f.py\n"
            "acceptance_criteria:\n"
            "  - id: \"acceptance-1\"\n    description: \"d\"\n"
            "---\n\n## Acceptance criteria\n- [ ] (AC1) d\n"
        )
        result = verify_plan_completeness(body)
        assert result["valid"] is False
        assert any(
            "must match" in e["issue"] for e in result["errors"]
        )

    def test_absolute_path_in_touched_files_errors(self):
        body = (
            "---\nplan_id: \"x\"\n"
            "touched_files:\n  - /etc/passwd\n  - ../escape\n"
            "acceptance_criteria:\n"
            "  - id: AC1\n    description: \"d\"\n"
            "---\n\n## Acceptance criteria\n- [ ] (AC1) d\n"
        )
        result = verify_plan_completeness(body)
        assert result["valid"] is False
        assert any(
            "absolute paths" in e["issue"] for e in result["errors"]
        )
        assert any(
            "traversal" in e["issue"] for e in result["errors"]
        )

    def test_missing_assumptions_field_warns(self):
        body = (
            "---\nplan_id: \"x\"\n"
            "touched_files:\n  - f.py\n"
            "acceptance_criteria:\n"
            "  - id: AC1\n    description: \"d\"\n"
            "---\n\n## Acceptance criteria\n- [ ] (AC1) d\n"
        )
        result = verify_plan_completeness(body)
        assert result["valid"] is True
        assert any(
            w["field"] == "assumptions"
            for w in result["warnings"]
        )

    def test_empty_entities_introduced_entry_errors(self):
        body = (
            "---\nplan_id: \"x\"\n"
            "touched_files:\n  - f.py\n"
            "entities_introduced:\n  - \"\"\n"
            "acceptance_criteria:\n"
            "  - id: AC1\n    description: \"d\"\n"
            "---\n\n## Acceptance criteria\n- [ ] (AC1) d\n"
        )
        result = verify_plan_completeness(body)
        assert result["valid"] is False
        assert any(
            "entities_introduced" in e["field"] and "non-empty" in e["issue"]
            for e in result["errors"]
        )

    def test_unknown_field_warns_not_errors(self):
        body = (
            "---\nplan_id: \"x\"\n"
            "touched_files:\n  - f.py\n"
            "acceptance_criteria:\n"
            "  - id: AC1\n    description: \"d\"\n"
            "future_field: \"x\"\n"
            "---\n\n## Acceptance criteria\n- [ ] (AC1) d\n"
        )
        result = verify_plan_completeness(body)
        assert result["valid"] is True
        assert any(
            w["field"] == "future_field" and "unknown" in w["issue"]
            for w in result["warnings"]
        )


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