"""Unit tests for the #185 plan-vs-diff comparator.

Covers diff parsing helpers, entity-survival regex matching, AC
coverage scanning, and the orchestrator's report shape across the
four drift classes plus the clean case.
"""
from __future__ import annotations

import pytest

from active_claude_github_mcp.validators.diff import (
    extract_added_lines,
    extract_changed_lines,
    parse_diff_files,
    verify_diff_covers_plan,
)


_PLAN_GOLDEN = """\
---
plan_id: "test-plan"
touched_files:
  - mcp-server/src/foo.py
  - tests/test_foo.py
entities_introduced:
  - "new_function"
  - "NewClass"
entities_modified:
  - "existing_function"
acceptance_criteria:
  - id: AC1
    description: "Adds new_function"
  - id: AC2
    description: "Adds NewClass"
---

## Acceptance criteria

- [ ] (AC1) Adds new_function
- [ ] (AC2) Adds NewClass
"""

_DIFF_GOLDEN = """\
diff --git a/mcp-server/src/foo.py b/mcp-server/src/foo.py
--- a/mcp-server/src/foo.py
+++ b/mcp-server/src/foo.py
@@ -1,5 +1,12 @@
 def existing_function():
-    return 1
+    return 2  # modified
+
+def new_function():
+    return "x"
+
+class NewClass:
+    pass
 # end
diff --git a/tests/test_foo.py b/tests/test_foo.py
--- a/tests/test_foo.py
+++ b/tests/test_foo.py
@@ -1,2 +1,4 @@
 import foo
+def test_new_function():
+    assert foo.new_function() == "x"
"""


class TestParseDiffFiles:
    def test_extracts_paths(self):
        assert parse_diff_files(_DIFF_GOLDEN) == [
            "mcp-server/src/foo.py",
            "tests/test_foo.py",
        ]

    def test_dedup_across_minus_plus_lines(self):
        diff = (
            "--- a/x.py\n"
            "+++ b/x.py\n"
            "@@ -1,1 +1,2 @@\n"
            " a\n+b\n"
        )
        assert parse_diff_files(diff) == ["x.py"]

    def test_drops_dev_null(self):
        diff = (
            "--- /dev/null\n"
            "+++ b/new.py\n"
            "@@ -0,0 +1,1 @@\n"
            "+content\n"
        )
        assert parse_diff_files(diff) == ["new.py"]

    def test_empty_diff(self):
        assert parse_diff_files("") == []


class TestExtractLines:
    def test_added_excludes_plus_plus_plus_header(self):
        diff = (
            "+++ b/foo.py\n"
            "@@ -1 +1,2 @@\n"
            "+added line\n"
            " context\n"
        )
        assert extract_added_lines(diff) == ["added line"]

    def test_changed_includes_both_directions(self):
        diff = (
            "+++ b/foo.py\n"
            "@@ -1,2 +1,2 @@\n"
            "-removed line\n"
            "+added line\n"
        )
        out = extract_changed_lines(diff)
        assert "added line" in out
        assert "removed line" in out


class TestVerifyDiffCoversPlanGolden:
    def test_clean_diff_covers_plan(self):
        result = verify_diff_covers_plan(
            _PLAN_GOLDEN, _DIFF_GOLDEN, evidence_text="(AC1) (AC2)"
        )
        assert result["covered"] is True, (
            f"missing_files={result['missing_files']} "
            f"unexpected_files={result['unexpected_files']} "
            f"missing_introduced={result['missing_entities_introduced']} "
            f"missing_modified={result['missing_entities_modified']} "
            f"unchecked_ac={result['unchecked_ac']}"
        )
        # Evidence captures the two AC mentions in evidence_text.
        assert result["ac_coverage_evidence"]["AC1"]
        assert result["ac_coverage_evidence"]["AC2"]


class TestVerifyDiffCoversPlanDriftClasses:
    def test_missing_file_flagged(self):
        plan = _PLAN_GOLDEN
        # Diff omits tests/test_foo.py entirely.
        diff = """\
diff --git a/mcp-server/src/foo.py b/mcp-server/src/foo.py
--- a/mcp-server/src/foo.py
+++ b/mcp-server/src/foo.py
@@ -1,1 +1,4 @@
 def existing_function():
+def new_function():
+    pass
+class NewClass: pass
"""
        result = verify_diff_covers_plan(plan, diff, evidence_text="(AC1) (AC2)")
        assert result["covered"] is False
        assert "tests/test_foo.py" in result["missing_files"]

    def test_unexpected_file_flagged(self):
        plan = _PLAN_GOLDEN
        # Diff touches an extra file not declared in touched_files.
        diff = _DIFF_GOLDEN + """\
diff --git a/orphan.py b/orphan.py
--- a/orphan.py
+++ b/orphan.py
@@ -1 +1,2 @@
 a
+b
"""
        result = verify_diff_covers_plan(plan, diff, evidence_text="(AC1) (AC2)")
        assert result["covered"] is False
        assert "orphan.py" in result["unexpected_files"]

    def test_missing_entity_introduced_flagged(self):
        plan = _PLAN_GOLDEN
        # Diff lacks NewClass.
        diff = """\
diff --git a/mcp-server/src/foo.py b/mcp-server/src/foo.py
--- a/mcp-server/src/foo.py
+++ b/mcp-server/src/foo.py
@@ -1,1 +1,3 @@
 def existing_function():
+def new_function():
+    pass
diff --git a/tests/test_foo.py b/tests/test_foo.py
--- a/tests/test_foo.py
+++ b/tests/test_foo.py
@@ -1 +1,2 @@
 a
+def test_new_function(): pass
"""
        result = verify_diff_covers_plan(plan, diff, evidence_text="(AC1) (AC2)")
        assert result["covered"] is False
        assert "NewClass" in result["missing_entities_introduced"]
        assert "new_function" not in result["missing_entities_introduced"]

    def test_missing_entity_modified_flagged(self):
        plan = _PLAN_GOLDEN
        # Diff doesn't touch existing_function.
        diff = """\
diff --git a/mcp-server/src/foo.py b/mcp-server/src/foo.py
--- a/mcp-server/src/foo.py
+++ b/mcp-server/src/foo.py
@@ -1,1 +1,5 @@
 unchanged
+def new_function():
+    pass
+class NewClass:
+    pass
diff --git a/tests/test_foo.py b/tests/test_foo.py
--- a/tests/test_foo.py
+++ b/tests/test_foo.py
@@ -1 +1,2 @@
 a
+b
"""
        result = verify_diff_covers_plan(plan, diff, evidence_text="(AC1) (AC2)")
        assert result["covered"] is False
        assert "existing_function" in result["missing_entities_modified"]

    def test_unchecked_ac_flagged(self):
        plan = _PLAN_GOLDEN
        # Diff is clean, but no evidence text mentions AC1/AC2.
        result = verify_diff_covers_plan(plan, _DIFF_GOLDEN, evidence_text="")
        assert result["covered"] is False
        assert set(result["unchecked_ac"]) == {"AC1", "AC2"}


class TestVerifyDiffCoversPlanGracefulDegradation:
    def test_no_frontmatter_returns_covered_with_warning(self):
        result = verify_diff_covers_plan(
            "# Plan with no frontmatter\n", _DIFF_GOLDEN
        )
        assert result["covered"] is True
        assert any(
            "no YAML frontmatter" in w["issue"] for w in result["warnings"]
        )

    def test_malformed_frontmatter_returns_uncovered_with_warning(self):
        plan = "---\nplan_id: x\nno close marker\n"
        result = verify_diff_covers_plan(plan, _DIFF_GOLDEN)
        assert result["covered"] is False
        assert any(
            "parse error" in w["issue"] for w in result["warnings"]
        )


class TestAcCoverageEvidence:
    def test_evidence_records_diff_and_evidence_sources(self):
        plan = _PLAN_GOLDEN
        diff = _DIFF_GOLDEN + "\n# (AC1) referenced in diff context\n"
        result = verify_diff_covers_plan(
            plan, diff, evidence_text="commit msg: (AC2) done\n"
        )
        # AC1 was found in the diff context line, AC2 in evidence_text.
        ac1_ev = result["ac_coverage_evidence"]["AC1"]
        ac2_ev = result["ac_coverage_evidence"]["AC2"]
        assert any(e.startswith("diff:") for e in ac1_ev)
        assert any(e.startswith("evidence:") for e in ac2_ev)


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