"""Roadmap #28 Part 1 — auto-link RFC references in PR text + comments.

Two layers:

  * Unit tests over the pure scanner (`rfc_links.segment_text` /
    `_keys_for` / `LinkIndex`) — the matching rules and their
    false-positive guards, no DB.
  * End-to-end tests that the PR description, PR review comments, and
    PR-less discussion comments all surface `*_segments` enriched against
    the live accepted-RFC corpus, with self-references suppressed.

Reuses the FakeGitea + session helpers from test_propose_vertical.py and
the active-RFC seed from test_rfc_view_vertical.py.
"""
from __future__ import annotations

from app import rfc_links

from test_propose_vertical import (  # noqa: F401
    FakeGitea,
    app_with_fake_gitea,
    grant_rfc_collaborator,
    provision_user_row,
    sign_in_as,
    tmp_env,
)
from test_rfc_view_vertical import SEED_BODY, seed_active_rfc
from test_pr_flow_vertical import _cut_branch_and_accept_change


# ---------------------------------------------------------------------------
# Unit — the pure scanner
# ---------------------------------------------------------------------------


def _idx(*terms):
    """Build a LinkIndex from raw (key, slug, title) tuples (keys lower)."""
    return rfc_links.LinkIndex(list(terms))


def test_empty_text_is_single_empty_segment():
    assert rfc_links.segment_text("", []) == [{"type": "text", "text": ""}]
    assert rfc_links.segment_text(None, []) == [{"type": "text", "text": ""}]


def test_no_terms_returns_plain_text():
    out = rfc_links.segment_text("hello world", [])
    assert out == [{"type": "text", "text": "hello world"}]


def test_multiword_title_links_and_preserves_casing():
    idx = _idx(("open human model", "open-human-model", "Open Human Model"))
    out = idx.segment("See the Open Human Model for details.")
    assert out == [
        {"type": "text", "text": "See the "},
        {"type": "rfc", "slug": "open-human-model", "label": "Open Human Model",
         "title": "Open Human Model"},
        {"type": "text", "text": " for details."},
    ]


def test_match_is_case_insensitive():
    idx = _idx(("open human model", "open-human-model", "Open Human Model"))
    out = idx.segment("see the OPEN HUMAN MODEL")
    assert out[-1] == {"type": "rfc", "slug": "open-human-model",
                       "label": "OPEN HUMAN MODEL", "title": "Open Human Model"}


def test_word_boundary_prevents_substring_match():
    # "harm" must not match inside "charming" / "harmless".
    idx = _idx(("rfc-0001", "open-human-model", "Open Human Model"))
    out = idx.segment("a charming rfc-00012 not real")
    # rfc-0001 is a prefix of rfc-00012 but the trailing '2' is a word char,
    # so no match — the whole string stays plain text.
    assert out == [{"type": "text", "text": "a charming rfc-00012 not real"}]


def test_rfc_id_token_links():
    idx = _idx(("rfc-0001", "open-human-model", "Open Human Model"))
    out = idx.segment("as established in RFC-0001.")
    assert out[1] == {"type": "rfc", "slug": "open-human-model",
                      "label": "RFC-0001", "title": "Open Human Model"}


def test_longest_match_wins():
    # A bare "Open" term and the full title both present; the full title
    # (longer) must win at the position.
    idx = _idx(
        ("open", "open", "Open"),
        ("open human model", "open-human-model", "Open Human Model"),
    )
    out = idx.segment("the Open Human Model")
    assert out[-1]["slug"] == "open-human-model"
    assert out[-1]["label"] == "Open Human Model"


def test_pending_term_emits_contribute_segment():
    # Part 3: a super-draft match is an `rfc-pending` segment carrying the
    # owner display name, not a plain link.
    idx = rfc_links.LinkIndex([
        rfc_links.Term(key="open human model", kind="pending",
                       slug="open-human-model", title="Open Human Model", owner="Alice"),
    ])
    out = idx.segment("see Open Human Model please")
    assert out[1] == {
        "type": "rfc-pending", "slug": "open-human-model",
        "label": "Open Human Model", "title": "Open Human Model", "owner": "Alice",
    }


def test_candidate_term_emits_create_segment():
    # Part 2: a candidate term carries its canonical spelling for the
    # propose pre-fill; no slug (no RFC exists yet).
    idx = rfc_links.LinkIndex([
        rfc_links.Term(key="memory model", kind="candidate", term="Memory Model"),
    ])
    out = idx.segment("the memory model is unspecified")
    assert out[1] == {"type": "rfc-candidate", "label": "memory model", "term": "Memory Model"}


def test_kind_precedence_active_beats_pending_beats_candidate():
    # All three buckets contribute the same key; the highest-precedence
    # kind (active) must win at the position.
    key = "open human model"
    idx = rfc_links.LinkIndex([
        rfc_links.Term(key=key, kind="candidate", term="Open Human Model"),
        rfc_links.Term(key=key, kind="pending", slug="ohm-draft", title="Open Human Model", owner="A"),
        rfc_links.Term(key=key, kind="active", slug="open-human-model", title="Open Human Model"),
    ])
    out = idx.segment("the Open Human Model")
    assert out[-1]["type"] == "rfc"
    assert out[-1]["slug"] == "open-human-model"


def test_keys_for_gating():
    keys = lambda **kw: set(rfc_links._keys_for(**kw))
    # rfc_id always contributes.
    assert "rfc-0001" in keys(slug="x", title="X", rfc_id="RFC-0001")
    # multi-word title contributes; single common word does NOT.
    assert "open human model" in keys(slug="ohm", title="Open Human Model", rfc_id=None)
    assert keys(slug="human", title="Human", rfc_id=None) == set()
    # hyphenated slug contributes; single-token slug does NOT.
    assert "open-human-model" in keys(slug="open-human-model", title="X", rfc_id=None)
    assert "ohm" not in keys(slug="ohm", title="OHM", rfc_id=None)


# ---------------------------------------------------------------------------
# End-to-end — enrichment surfaces on the read paths
# ---------------------------------------------------------------------------


def _open_pr_on(client, fake, *, host_slug: str, description: str):
    """Seed branch + accepted change on host_slug and open a PR. Returns
    the pr_number."""
    # `original` must exist verbatim in SEED_BODY or the accept is "stale".
    branch, _ = _cut_branch_and_accept_change(
        client, fake, slug=host_slug,
        original="It defines consent, trait, and agency in compatible terms.",
        proposed="It defines consent, trait, harm, and agency in compatible terms.",
    )
    r = client.post(
        f"/api/rfcs/{host_slug}/branches/{branch}/open-pr",
        json={"title": "A change", "description": description},
    )
    assert r.status_code == 200, r.text
    return r.json()["pr_number"]


def _rfc_segments(segments):
    return [s for s in segments if s["type"] == "rfc"]


def test_pr_description_autolinks_other_rfc(app_with_fake_gitea):
    from fastapi.testclient import TestClient

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        provision_user_row(user_id=2, login="alice", role="contributor")
        # Two accepted RFCs: a host for the PR + a referenceable target.
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)
        seed_active_rfc(fake, slug="open-human-model", title="Open Human Model", body=SEED_BODY)
        sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor")

        pr_number = _open_pr_on(
            client, fake, host_slug="ohm",
            description="This builds on the Open Human Model definition.",
        )
        r = client.get(f"/api/rfcs/ohm/prs/{pr_number}")
        assert r.status_code == 200, r.text
        pr = r.json()
        links = _rfc_segments(pr["description_segments"])
        assert len(links) == 1
        assert links[0]["slug"] == "open-human-model"
        assert links[0]["label"] == "Open Human Model"
        # The plain text is still present for non-segment callers (the bot
        # appends a §6.5 On-behalf-of trailer, so this is a containment check).
        assert "This builds on the Open Human Model definition." in pr["description"]


def test_pr_review_comment_autolinked(app_with_fake_gitea):
    from fastapi.testclient import TestClient

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        provision_user_row(user_id=2, login="alice", role="contributor")
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)
        seed_active_rfc(fake, slug="open-human-model", title="Open Human Model", body=SEED_BODY)
        sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor")

        pr_number = _open_pr_on(client, fake, host_slug="ohm", description="plain.")
        r = client.post(
            f"/api/rfcs/ohm/prs/{pr_number}/review",
            json={"text": "See Open Human Model and RFC-0001.", "anchor_payload": {}},
        )
        assert r.status_code == 200, r.text

        pr = client.get(f"/api/rfcs/ohm/prs/{pr_number}").json()
        all_msgs = [m for msgs in pr["messages_by_thread"].values() for m in msgs]
        review_msgs = [m for m in all_msgs if "Open Human Model" in (m["text"] or "")]
        assert review_msgs, "review comment not found in payload"
        links = _rfc_segments(review_msgs[0]["text_segments"])
        # Both "Open Human Model" (title) and "RFC-0001" (id) point to the
        # one referenceable RFC.
        assert {s["slug"] for s in links} == {"open-human-model"}
        assert {s["label"] for s in links} == {"Open Human Model", "RFC-0001"}


def test_discussion_comment_autolinked(app_with_fake_gitea):
    from fastapi.testclient import TestClient

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        provision_user_row(user_id=2, login="alice", role="contributor")
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)
        seed_active_rfc(fake, slug="open-human-model", title="Open Human Model", body=SEED_BODY)
        # alice is the seeded owner of ohm (owners=["alice"]); grant the
        # per-RFC collaborator row explicitly so the #12 discuss gate passes.
        grant_rfc_collaborator(user_id=2, rfc_slug="ohm", role_in_rfc="contributor")
        sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor")

        r = client.post(
            "/api/rfcs/ohm/discussion/threads",
            json={"message": "Compare with the Open Human Model."},
        )
        assert r.status_code == 200, r.text
        thread_id = r.json()["thread_id"]

        r = client.get(f"/api/rfcs/ohm/discussion/threads/{thread_id}/messages")
        assert r.status_code == 200, r.text
        msgs = r.json()["messages"]
        assert msgs and "text_segments" in msgs[0]
        links = _rfc_segments(msgs[0]["text_segments"])
        assert len(links) == 1
        assert links[0]["slug"] == "open-human-model"


def test_self_reference_not_linked(app_with_fake_gitea):
    from fastapi.testclient import TestClient

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        provision_user_row(user_id=2, login="alice", role="contributor")
        # The host RFC has a multi-word title, so absent exclude_slug it
        # WOULD self-link. exclude_slug must suppress it.
        seed_active_rfc(fake, slug="open-human-model", title="Open Human Model", body=SEED_BODY)
        sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor")

        pr_number = _open_pr_on(
            client, fake, host_slug="open-human-model",
            description="Refines the Open Human Model definition.",
        )
        pr = client.get(f"/api/rfcs/open-human-model/prs/{pr_number}").json()
        assert _rfc_segments(pr["description_segments"]) == []
