"""FB-02/FB-03 conformance over a repo's own FEEDBACK.md papers.

The parsing half of these tests is aimed squarely at the four structural defects
an earlier inline version of this work produced against the real fleet — a heading
inside a fence read as a paper boundary, a standalone section displaced, a
duplicated header, and content swallowed when a file had two open sections. Each
has a named test below.
"""

from __future__ import annotations

from pathlib import Path

import pytest

from mgf.fed._papers import (
    STATUSES,
    check_text,
    papers,
    run_papers,
    skipped_entries,
)

TABLE = """\
| Field | Value |
|---|---|
| Status | {status} |
| Severity | 🟢 Low |
"""


def _paper(n: int, status: str = "OPEN", extra: str = "") -> str:
    return f"### PAPER-{n:02d} — title {n}\n\n" + TABLE.format(status=status) + extra + "\n"


def _doc(*bodies: str) -> str:
    return "# FEEDBACK — mgf-x\n\n## §2 Open feedback (active)\n\n" + "".join(bodies)


# --- parsing -------------------------------------------------------------------


def test_finds_each_paper_once() -> None:
    found = papers(_doc(_paper(1), _paper(2), _paper(3)), "mgf-x")
    assert [p.heading for p in found] == [
        "PAPER-01 — title 1",
        "PAPER-02 — title 2",
        "PAPER-03 — title 3",
    ]


def test_a_heading_inside_a_fence_is_not_a_paper() -> None:
    """Defect 1: the naive split cut a paper in half in mgf-hrb on exactly this."""
    doc = _doc(
        _paper(1),
        "Here is the shape to copy:\n\n```markdown\n### PAPER-99 — not a real paper\n\n"
        "| Status | OPEN |\n```\n\n",
        _paper(2),
    )
    found = papers(doc, "mgf-x")
    assert [p.heading for p in found] == ["PAPER-01 — title 1", "PAPER-02 — title 2"]
    assert "PAPER-99" in found[0].text  # it stays WITH paper 1, not extracted


def test_a_paper_body_is_a_byte_identical_slice() -> None:
    doc = _doc(_paper(1, extra="\n**Concern.** trailing prose\n\n> a quote\n"), _paper(2))
    first = papers(doc, "mgf-x")[0]
    assert first.text == doc[doc.index("### PAPER-01") : doc.index("### PAPER-02")]


def test_a_section_after_the_last_paper_is_not_absorbed_into_it() -> None:
    """Defect 2: mgf-common's standalone `## §3 …` was displaced by the last entry.

    Papers are *reported*, never relocated, so nothing can be displaced — but the
    last paper's slice must still not silently claim a following section as its own
    body, or a Status there would be attributed to the wrong paper.
    """
    doc = _doc(_paper(1)) + "## §3 Reviewer's meta-feedback\n\n| Status | SHIPPED |\n"
    found = papers(doc, "mgf-x")
    assert len(found) == 1
    # The section is not a paper, and its stray row must not be read as paper 1's.
    assert check_text(doc, "mgf-x") == []


def test_two_open_sections_do_not_swallow_each_other() -> None:
    """Defect 4: a split on a marker returned only the first chunk when a file had
    two open sections, silently dropping the rest of the document."""
    doc = (
        _doc(_paper(1), _paper(2))
        + "\n## §2b More open feedback\n\n"
        + _paper(3)
        + _paper(4, status="SHIPPED")
    )
    found = papers(doc, "mgf-x")
    assert [p.heading.split(" ")[0] for p in found] == [
        "PAPER-01",
        "PAPER-02",
        "PAPER-03",
        "PAPER-04",
    ]


def test_a_subsection_inside_a_paper_is_not_a_new_paper() -> None:
    doc = _doc(_paper(1, extra="\n#### Notes\n\nmore\n"), _paper(2))
    assert len(papers(doc, "mgf-x")) == 2


def test_no_papers_in_an_empty_or_placeholder_file() -> None:
    assert (
        papers("# FEEDBACK — mgf-x\n\n## §2 Open feedback (active)\n\n*No papers yet.*\n", "x")
        == []
    )


# --- FB-02 ---------------------------------------------------------------------


def test_clean_papers_pass() -> None:
    doc = _doc(_paper(1, "OPEN"), _paper(2, "DEFERRED"), _paper(3, "WONTFIX"))
    assert check_text(doc, "mgf-x") == []


@pytest.mark.parametrize("status", STATUSES)
def test_every_enum_member_is_accepted(status: str) -> None:
    extra = "| Shipped in | `abc1234` (v1.2.3) |\n" if status == "SHIPPED" else ""
    assert check_text(_doc(_paper(1, status, extra)), "mgf-x") == []


def test_bullet_form_status_is_reported_as_wrong_shape_not_absent() -> None:
    """FB-02 says *table*. The distinction matters: 'wrong shape' is a 1-line fix,
    'absent' means the information does not exist and someone must go find it."""
    doc = _doc("### PAPER-01 — title\n\n- **Status:** OPEN\n- **Severity:** 🟢\n\n")
    (v,) = check_text(doc, "mgf-x")
    assert v.rule == "FB-02"
    assert "bullet form" in v.detail and "OPEN" in v.detail


def test_missing_status_entirely_is_reported() -> None:
    doc = _doc("### PAPER-01 — title\n\nno metadata at all\n\n")
    (v,) = check_text(doc, "mgf-x")
    assert v.rule == "FB-02" and "no Status field at all" in v.detail


@pytest.mark.parametrize(
    "bad", ["RESOLVED", "CLOSED", "ACCEPTED", "ADOPTED", "✅ RESOLVED ON ARRIVAL"]
)
def test_out_of_enum_status_is_a_violation(bad: str) -> None:
    """RESOLVED/CLOSED/ACCEPTED must NOT be folded into SHIPPED. An out-of-enum
    state that tooling quietly accepts is how the credential plane stayed down for
    two days (F-36/F-37)."""
    (v,) = check_text(_doc(_paper(1, bad)), "mgf-x")
    assert v.rule == "FB-02" and "outside the enum" in v.detail


def test_a_decorated_status_cell_still_resolves() -> None:
    doc = _doc(
        _paper(1, "✅ SHIPPED (Approach 1) in **0.52.0**", "| Shipped in | `abc` (0.52.0) |\n")
    )
    assert check_text(doc, "mgf-x") == []


def test_status_is_matched_as_a_word_not_a_substring() -> None:
    """`REOPENED` contains `OPEN`; it is not the enum member `OPEN`."""
    (v,) = check_text(_doc(_paper(1, "REOPENED")), "mgf-x")
    assert "outside the enum" in v.detail


# --- FB-03 ---------------------------------------------------------------------


def test_shipped_without_shipped_in_is_a_violation() -> None:
    (v,) = check_text(_doc(_paper(1, "SHIPPED")), "mgf-x")
    assert v.rule == "FB-03" and "Shipped in" in v.detail


def test_shipped_with_shipped_in_passes() -> None:
    doc = _doc(_paper(1, "SHIPPED", "| Shipped in | `4f6cb9f` (v0.52.0) |\n"))
    assert check_text(doc, "mgf-x") == []


def test_shipped_in_belonging_to_the_NEXT_paper_does_not_satisfy_this_one() -> None:
    """The bound between papers must be real, or one `Shipped in` row silently
    absolves every SHIPPED paper above it."""
    doc = _doc(_paper(1, "SHIPPED"), _paper(2, "SHIPPED", "| Shipped in | `abc` (v1) |\n"))
    found = check_text(doc, "mgf-x")
    assert [v.heading.split(" ")[0] for v in found] == ["PAPER-01"]


def test_only_shipped_needs_shipped_in() -> None:
    assert check_text(_doc(_paper(1, "MOVED"), _paper(2, "OPEN")), "mgf-x") == []


def test_violations_carry_a_usable_line_number() -> None:
    doc = _doc(_paper(1), _paper(2, "SHIPPED"))
    (v,) = check_text(doc, "mgf-x")
    assert doc.splitlines()[v.line - 1].startswith("### PAPER-02")


# --- the runner's exit codes ---------------------------------------------------


def _repo(tmp_path: Path, name: str, body: str) -> Path:
    d = tmp_path / name
    d.mkdir()
    (d / "FEEDBACK.md").write_text(body, encoding="utf-8")
    return d


def test_run_papers_clean_is_zero(tmp_path: Path) -> None:
    d = _repo(tmp_path, "mgf-x", _doc(_paper(1)))
    assert run_papers(d, all_repos=False) == 0


def test_run_papers_violation_is_one(tmp_path: Path) -> None:
    d = _repo(tmp_path, "mgf-x", _doc(_paper(1, "SHIPPED")))
    assert run_papers(d, all_repos=False) == 1


def test_run_papers_without_a_feedback_file_is_two_not_zero(
    tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
    """'I could not verify' must never share an exit code with 'I verified'."""
    (tmp_path / "mgf-x").mkdir()
    assert run_papers(tmp_path / "mgf-x", all_repos=False) == 2
    assert "not a pass" in capsys.readouterr().out


def test_run_papers_all_scans_every_repo(
    tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
    _repo(tmp_path, "mgf-a", _doc(_paper(1)))
    _repo(tmp_path, "mgf-b", _doc(_paper(1, "SHIPPED")))
    assert run_papers(None, all_repos=True, root=tmp_path) == 1
    out = capsys.readouterr().out
    assert "mgf-b" in out and "FB-03" in out
    assert "2 paper(s)" in out  # both repos were read, not just the failing one


def test_run_papers_all_with_no_files_is_two(tmp_path: Path) -> None:
    assert run_papers(None, all_repos=True, root=tmp_path) == 2


@pytest.mark.parametrize("ph", ["—", "-", "", "  ", "n/a", "TBD", "pending", "none"])
def test_shipped_with_a_placeholder_shipped_in_is_still_a_violation(ph: str) -> None:
    """The canonical template seeds `| Shipped in | — |`. That is honest while a paper
    is OPEN and a lie once it says SHIPPED — and accepting a present-but-empty cell is
    how a gate passes a repo it never really checked."""
    doc = _doc(_paper(1, "SHIPPED", f"| Shipped in | {ph} |\n"))
    (v,) = check_text(doc, "mgf-x")
    assert v.rule == "FB-03" and "placeholder" in v.detail


def test_a_placeholder_shipped_in_is_fine_while_the_paper_is_open() -> None:
    assert check_text(_doc(_paper(1, "OPEN", "| Shipped in | — |\n")), "mgf-x") == []


@pytest.mark.parametrize(
    "qualified",
    [
        "PARTIALLY SHIPPED",
        "NOT SHIPPED",
        "never shipped",
        "🟡 PARTIALLY SHIPPED (2026-07-24)",
        "half shipped",
        "NOT OPEN",
        "mostly deferred",
    ],
)
def test_a_qualifier_in_front_of_an_enum_word_is_out_of_enum(qualified: str) -> None:
    """`NOT SHIPPED` matched `\\bSHIPPED\\b` and was certified as SHIPPED. Found on
    mgf-secrets PAPER-02, whose remaining third is explicitly founder-gated —
    reading that as closed is the misreport FB-02 exists to stop."""
    (v,) = check_text(_doc(_paper(1, qualified)), "mgf-x")
    assert v.rule == "FB-02" and "outside the enum" in v.detail


@pytest.mark.parametrize(
    "fine",
    [
        "SHIPPED",
        "✅ SHIPPED (Approach 1) in **0.52.0**",
        "SHIPPED, not deferred",
        "SHIPPED — the partial rollout is done",
    ],
)
def test_a_qualifier_that_is_not_adjacent_does_not_disqualify(fine: str) -> None:
    """Only an ADJACENT qualifier negates. "SHIPPED, not deferred" is SHIPPED."""
    doc = _doc(_paper(1, fine, "| Shipped in | `abc1234` (v1.0.0) |\n"))
    assert check_text(doc, "mgf-x") == []


# --- FB-01: ids are never reused -----------------------------------------------


def test_a_duplicate_paper_id_is_a_violation() -> None:
    """FB-01 — "papers keep their PAPER-NN forever for stable git-blame +
    cross-reference". Two papers sharing an id breaks every reference to it, and
    afterwards neither is identifiable. mgf-common carried two different
    PAPER-119s while mgf-standard PAPER-141 rated the risk "nothing is broken
    today"."""
    doc = _doc(_paper(1), _paper(1))
    (v,) = check_text(doc, "mgf-x")
    assert v.rule == "FB-01"
    assert "already used at line" in v.detail and "never reused" in v.detail


def test_the_original_concern_suffix_is_not_a_duplicate() -> None:
    """`PAPER-80-original-concern` is a legitimate convention in mgf-common
    (four papers use it). Counting it as a second PAPER-80 would make the gate
    cry wolf on papers that are fine — and a gate that cries wolf is ignored."""
    doc = _doc(
        _paper(1),
        "### `PAPER-01-original-concern` — (see the note above)\n\n" + TABLE.format(status="OPEN"),
    )
    assert [v.rule for v in check_text(doc, "mgf-x")] == []


def test_distinct_ids_are_fine() -> None:
    assert check_text(_doc(_paper(1), _paper(2), _paper(3)), "mgf-x") == []


def test_a_duplicate_is_reported_at_the_SECOND_occurrence() -> None:
    """The first use owns the id; the newcomer is the one to renumber, and the
    message names where the id was taken."""
    doc = _doc(_paper(1), _paper(2), _paper(1))
    (v,) = check_text(doc, "mgf-x")
    assert v.line > doc.count("\n", 0, doc.index("### PAPER-02"))


# --- no silent caps: the gate names what it does not judge ----------------------


def test_non_paper_ids_are_reported_not_silently_skipped() -> None:
    """mgf-common's FEEDBACK.md holds `ASPIRE-01..06` in exactly the paper shape.

    This gate judges `PAPER-NN` only — a deliberate bound, because widening the id
    matcher would catch rule ids in prose headings. But a bound that is not
    ANNOUNCED reads as coverage: `PAPER-53`'s block ran 871 lines with six other
    entries inside it, their metadata tables attributed to a paper that does not
    own them, and the gate reported clean.
    """
    doc = (
        _doc(_paper(1))
        + "\n### `ASPIRE-01` — a different id family\n\n"
        + TABLE.format(status="OPEN")
    )
    assert skipped_entries(doc, "mgf-x") == ["ASPIRE-01"]
    assert check_text(doc, "mgf-x") == []  # still not judged — only reported


def test_a_paper_id_is_not_reported_as_skipped() -> None:
    assert skipped_entries(_doc(_paper(1), _paper(2)), "mgf-x") == []


def test_a_non_paper_id_inside_a_fence_is_not_reported() -> None:
    """The same fence-awareness the rest of the module has — a documented example
    must not be announced as unjudged content."""
    doc = _doc(_paper(1)) + "\n```markdown\n### `ASPIRE-99` — an example in a fence\n```\n"
    assert skipped_entries(doc, "mgf-x") == []
