"""Unit tests for the toc_drift module (#194).

Pure parse + diff coverage — exercises `parse_toc_references` and
`compute_toc_drift` without going through `open_session` / GitHub.
The handler-side integration is covered in `tests/tools/test_open_session.py`.
"""
from __future__ import annotations

import pytest

from active_claude_github_mcp.toc_drift import (
    compute_toc_drift,
    parse_toc_references,
)


# ----- parse_toc_references ------------------------------------------------


class TestParseTocReferencesBasics:
    def test_empty_body_yields_empty_set(self):
        assert parse_toc_references("") == set()
        assert parse_toc_references(None) == set()

    def test_extracts_single_issue_ref(self):
        assert parse_toc_references("see #42") == {42}

    def test_extracts_multiple_refs(self):
        body = "see #42 and #100 and #1"
        assert parse_toc_references(body) == {1, 42, 100}

    def test_dedup_preserved_via_set_semantics(self):
        body = "#42 was filed; revisit #42 in the followup"
        assert parse_toc_references(body) == {42}


class TestParseTocReferencesRanges:
    """AC3 — `#76–#86` (en-dash range) expands to [76..86] inclusive."""

    def test_en_dash_range_expanded(self):
        assert parse_toc_references("#76–#86") == set(range(76, 87))

    def test_ascii_hyphen_range_expanded(self):
        # Hyphen is the fallback form; both should work.
        assert parse_toc_references("#10-#12") == {10, 11, 12}

    def test_range_with_spacing(self):
        assert parse_toc_references("#103 – #108") == set(range(103, 109))

    def test_inverted_range_ignored(self):
        # `#86–#76` is malformed (lo > hi). Don't expand; pick up
        # only the two endpoints from the standalone-ref pattern.
        assert parse_toc_references("#86–#76") == {76, 86}

    def test_range_combines_with_standalone_refs(self):
        body = "milestone covers #76–#86; sibling: #200"
        assert parse_toc_references(body) == set(range(76, 87)) | {200}


class TestParseTocReferencesExclusions:
    """References that should NOT count as in-TOC refs."""

    def test_cross_repo_ref_excluded(self):
        # `mcp-dev#15` references an Issue in a different repo. The
        # negative lookbehind for `[\w/]` filters it out.
        assert parse_toc_references("see mcp-dev#15 for context") == set()

    def test_url_path_ref_excluded(self):
        body = "https://github.com/example/repo/issues/123 — discussed"
        assert parse_toc_references(body) == set()

    def test_url_in_md_link_with_explicit_pound_still_extracted(self):
        # `[#52](https://.../discussions/52)` — the bracket-prefixed
        # `#52` is preceded by `[`, not a word char. Should match.
        body = "see [#52](https://github.com/x/y/discussions/52)"
        assert parse_toc_references(body) == {52}


# ----- compute_toc_drift ---------------------------------------------------


class TestComputeTocDriftStaleVsClean:
    def test_clean_repo_no_drift(self):
        # TOC references every issue; both lists empty.
        toc = "Issues: #1 #2 #3"
        drift = compute_toc_drift(toc, [1, 2, 3], [])

        assert drift["stale"] is False
        assert drift["missing_issues"] == []
        assert drift["missing_discussions"] == []
        assert drift["highest_issue_in_toc"] == 3
        assert drift["highest_issue_in_repo"] == 3
        assert "no drift" in drift["suggested_action"].lower()

    def test_missing_issues_marked_stale(self):
        toc = "Issues: #1 #2"
        drift = compute_toc_drift(toc, [1, 2, 3, 4, 5], [])

        assert drift["stale"] is True
        assert drift["missing_issues"] == [3, 4, 5]
        assert drift["highest_issue_in_toc"] == 2
        assert drift["highest_issue_in_repo"] == 5
        assert "3 issue(s)" in drift["suggested_action"]

    def test_missing_discussions_marked_stale(self):
        toc = "Issues: #1 #2"
        drift = compute_toc_drift(toc, [1, 2], [10, 11])

        assert drift["stale"] is True
        assert drift["missing_issues"] == []
        assert drift["missing_discussions"] == [10, 11]
        assert "2 discussion(s)" in drift["suggested_action"]

    def test_missing_both_combined_in_message(self):
        toc = "Issues: #1"
        drift = compute_toc_drift(toc, [1, 2], [5])

        assert drift["stale"] is True
        assert drift["missing_issues"] == [2]
        assert drift["missing_discussions"] == [5]
        assert "1 issue(s)" in drift["suggested_action"]
        assert "1 discussion(s)" in drift["suggested_action"]


class TestComputeTocDriftEdgeCases:
    def test_empty_repo_yields_no_drift(self):
        drift = compute_toc_drift("# Project Index", [], [])

        assert drift["stale"] is False
        assert drift["missing_issues"] == []
        assert drift["missing_discussions"] == []
        assert drift["highest_issue_in_toc"] is None
        assert drift["highest_issue_in_repo"] is None

    def test_empty_toc_yields_full_drift(self):
        drift = compute_toc_drift("", [1, 2, 3], [10])

        assert drift["stale"] is True
        assert drift["missing_issues"] == [1, 2, 3]
        assert drift["missing_discussions"] == [10]
        assert drift["highest_issue_in_toc"] is None
        assert drift["highest_issue_in_repo"] == 3

    def test_range_in_milestone_marks_intermediate_issues_referenced(self):
        # AC3 — milestone descriptions like "covers #76–#86" must
        # treat #77, #78, ..., #85 as referenced even though no row
        # exists for each individually.
        toc = "Milestone 1.3.0 covers #76–#86"
        repo_issues = list(range(76, 87))  # 76..86 inclusive
        drift = compute_toc_drift(toc, repo_issues, [])

        assert drift["stale"] is False
        assert drift["missing_issues"] == []

    def test_highest_in_toc_only_counts_existing_issues(self):
        # TOC references #999 but repo only has #1, #2. The highest
        # TOC ref that *also* exists in the repo is #2; the spurious
        # #999 doesn't bump highest_issue_in_toc above repo state.
        toc = "see #1 and #2 and #999"
        drift = compute_toc_drift(toc, [1, 2], [])

        assert drift["highest_issue_in_toc"] == 2
        assert drift["highest_issue_in_repo"] == 2


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