"""Integration coverage for the §19.2 "per-RFC credential delegation —
funder role + grant shape" candidate folded into §6.7.

The §6.7 settlement adds an optional `funder:` frontmatter field
(parallel to `owners:` / `arbiters:` / `models:`) plus a
`funder_consents` app-db record. Both halves are required for the
binding to be operationally active — the spec calls this the
"frontmatter + consent hybrid" two-key rule. When in effect, the
funder universe replaces (not augments) the operator universe for the
RFC's picker.

The tests below prove each branch through the API surface and the
resolver: the two-key rule under all four combinations, the
universe-replaces-not-augments rule, the three revocation paths, the
operator-enabled gate on credential registration, the consent-needs-
credentials gate, the §10.2 PR-draft and §8.12 chat routing through
funder credentials, and the entry parser round-trip for the new
frontmatter field.
"""
from __future__ import annotations

import asyncio
import json

from test_propose_vertical import (  # noqa: F401
    FakeGitea,
    app_with_fake_gitea,
    provision_user_row,
    sign_in_as,
    tmp_env,
)
from test_rfc_view_vertical import FakeProvider, seed_active_rfc  # noqa: F401


SEED_BODY = (
    "Open Human Model is a framework for representing humans.\n\n"
    "It defines consent, trait, and agency in compatible terms."
)


def _set_funder_login(slug: str, login: str | None) -> None:
    """Write `cached_rfcs.funder_login` directly. Mirrors what
    `_upsert_cached_rfc` would do on the next reconciler sweep, without
    requiring a full meta-repo refresh in tests focused on the resolver."""
    from app import db
    db.conn().execute(
        "UPDATE cached_rfcs SET funder_login = ? WHERE slug = ?",
        (login, slug),
    )


def _install_two_providers(app) -> None:
    """Operator universe of two picker keys — `claude` (Anthropic family)
    and `gemini` (Google family). The funder tests exercise the
    family-aware funder routing, so the operator must enable picker
    keys from at least two distinct families."""
    app.state.providers.clear()
    app.state.providers["claude"] = FakeProvider("operator claude says hello")
    app.state.providers["gemini"] = FakeProvider("operator gemini says hello")


def _patch_funder_construct(monkeypatch, marker: str = "from-funder-creds"):
    """Intercept `providers.construct_for_funder` so tests can verify
    when funder credentials are actually used. Returns a FakeProvider
    whose responses are tagged with the marker."""
    from app import providers as providers_mod
    funder_provider = FakeProvider(f"TITLE: {marker}\nDESCRIPTION: {marker} description")

    def fake_construct(picker_key, api_key):
        # Tag the returned provider with the key it was asked for so the
        # test can assert routing went through the funder layer.
        funder_provider._last_picker_key = picker_key
        funder_provider._last_api_key = api_key
        return funder_provider

    monkeypatch.setattr(providers_mod, "construct_for_funder", fake_construct)
    return funder_provider


# ---------------------------------------------------------------------------
# Two-key rule: frontmatter + consent both required
# ---------------------------------------------------------------------------


def test_frontmatter_only_no_consent_falls_back_to_operator(app_with_fake_gitea):
    """`funder:` named in frontmatter without a matching `funder_consents`
    row is operationally inert per §6.7. The resolver returns None for
    the funder-universe lookup; the picker reads as the operator universe."""
    from fastapi.testclient import TestClient

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        provision_user_row(user_id=1, login="ben", role="owner")
        provision_user_row(user_id=2, login="alice", role="contributor")
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)
        _install_two_providers(app)
        # Frontmatter names alice as funder, but alice has not consented
        # and has no registered credentials.
        _set_funder_login("ohm", "alice")

        sign_in_as(client, user_id=1, gitea_login="ben",
                   display_name="Ben", role="owner")
        r = client.get("/api/rfcs/ohm/models")
        assert r.status_code == 200
        assert [m["id"] for m in r.json()["models"]] == ["claude", "gemini"]


def test_consent_only_no_frontmatter_falls_back_to_operator(app_with_fake_gitea):
    """`funder_consents` row without a matching `funder:` frontmatter
    field is operationally inert per §6.7's two-key rule. The picker
    reads as the operator universe regardless of what the consenting
    user has registered."""
    from fastapi.testclient import TestClient
    from app import funder

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        provision_user_row(user_id=1, login="ben", role="owner")
        provision_user_row(user_id=2, login="alice", role="contributor")
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)
        _install_two_providers(app)
        # Alice has consented and registered, but frontmatter does not
        # name her as funder.
        funder.upsert_credential(2, "anthropic", "alice-anthropic-key")
        funder.add_consent(2, "ohm")

        sign_in_as(client, user_id=1, gitea_login="ben",
                   display_name="Ben", role="owner")
        r = client.get("/api/rfcs/ohm/models")
        assert [m["id"] for m in r.json()["models"]] == ["claude", "gemini"]


def test_both_present_activates_funder_universe(app_with_fake_gitea):
    """Frontmatter + consent + credentials match → the picker resolves
    to the intersection of the operator's enabled keys and the funder's
    registered families. Alice registered Anthropic only; the picker
    narrows to `claude`."""
    from fastapi.testclient import TestClient
    from app import funder

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        provision_user_row(user_id=1, login="ben", role="owner")
        provision_user_row(user_id=2, login="alice", role="contributor")
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)
        _install_two_providers(app)
        _set_funder_login("ohm", "alice")
        funder.upsert_credential(2, "anthropic", "alice-anthropic-key")
        funder.add_consent(2, "ohm")

        sign_in_as(client, user_id=1, gitea_login="ben",
                   display_name="Ben", role="owner")
        r = client.get("/api/rfcs/ohm/models")
        # Operator has {claude, gemini}; funder registered Anthropic only.
        # The intersection is {claude}.
        assert [m["id"] for m in r.json()["models"]] == ["claude"]
        assert r.json()["default"] == "claude"


def test_neither_present_operator_universe(app_with_fake_gitea):
    """The v1 default and status quo: no `funder:` field and no consent
    rows — the picker is the operator universe."""
    from fastapi.testclient import TestClient

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        provision_user_row(user_id=1, login="ben", role="owner")
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)
        _install_two_providers(app)

        sign_in_as(client, user_id=1, gitea_login="ben",
                   display_name="Ben", role="owner")
        r = client.get("/api/rfcs/ohm/models")
        assert [m["id"] for m in r.json()["models"]] == ["claude", "gemini"]


# ---------------------------------------------------------------------------
# Funder universe REPLACES operator universe — not augments
# ---------------------------------------------------------------------------


def test_funder_universe_replaces_operator_universe(app_with_fake_gitea):
    """§6.7's attribution-clean rule: the funder universe replaces the
    operator universe. A funder who registered Anthropic only —
    irrespective of what the operator otherwise enables — sees the
    picker narrowed to keys served by Anthropic. The funder cannot
    expand beyond the operator's enabled set, and the operator's other
    families are dropped (not added) when the funder is active."""
    from fastapi.testclient import TestClient
    from app import funder

    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)
        _install_two_providers(app)  # operator: claude + gemini
        _set_funder_login("ohm", "alice")
        funder.upsert_credential(2, "anthropic", "alice-anthropic-key")
        funder.add_consent(2, "ohm")

        sign_in_as(client, user_id=2, gitea_login="alice",
                   display_name="Alice", role="contributor")
        r = client.get("/api/rfcs/ohm/models")
        # The funder is active. Anthropic family alone serves `claude`.
        # `gemini` (a Google-family key the operator enabled) is dropped
        # because the funder hasn't registered Google credentials.
        assert [m["id"] for m in r.json()["models"]] == ["claude"]


def test_funder_with_no_matching_family_falls_into_opt_out_shape(app_with_fake_gitea):
    """When the consenting funder has no credentials whose family is
    served by the operator's enabled set, the resolved universe is
    empty — same shape as `models: []`. AI surfaces refuse honestly."""
    from fastapi.testclient import TestClient
    from app import funder

    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)
        # Operator universe: only Anthropic-family.
        app.state.providers.clear()
        app.state.providers["claude"] = FakeProvider("op-claude")
        _set_funder_login("ohm", "alice")
        # Funder has registered Google credentials only — no overlap.
        funder.upsert_credential(2, "google", "alice-google-key")
        funder.add_consent(2, "ohm")

        sign_in_as(client, user_id=2, gitea_login="alice",
                   display_name="Alice", role="contributor")
        r = client.get("/api/rfcs/ohm/models")
        assert r.json()["models"] == []


def test_funder_intersects_with_models_frontmatter(app_with_fake_gitea):
    """§6.6 + §6.7 compose: the resolved list is the §6.6 `models:` list
    intersected with the funder's registered universe (not the operator
    universe). If `models: [gemini]` is set but the funder only
    registered Anthropic, the intersection is empty and the RFC drops
    into the opt-out shape."""
    from fastapi.testclient import TestClient
    from app import db, funder

    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)
        _install_two_providers(app)  # operator: claude + gemini
        _set_funder_login("ohm", "alice")
        db.conn().execute(
            "UPDATE cached_rfcs SET models_json = ? WHERE slug = 'ohm'",
            (json.dumps(["gemini"]),),
        )
        funder.upsert_credential(2, "anthropic", "alice-anthropic-key")
        funder.add_consent(2, "ohm")

        sign_in_as(client, user_id=2, gitea_login="alice",
                   display_name="Alice", role="contributor")
        r = client.get("/api/rfcs/ohm/models")
        # §6.6 names {gemini}; funder universe is {claude}; intersection
        # is empty.
        assert r.json()["models"] == []


# ---------------------------------------------------------------------------
# Three revocation paths — each restores operator-credentials status quo
# ---------------------------------------------------------------------------


def test_funder_withdraws_consent_flips_back_to_operator(app_with_fake_gitea):
    """§6.7's first revocation path: the funder withdraws consent. The
    next AI-surface call resolves through the operator universe."""
    from fastapi.testclient import TestClient
    from app import funder

    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)
        _install_two_providers(app)
        _set_funder_login("ohm", "alice")
        funder.upsert_credential(2, "anthropic", "alice-anthropic-key")
        funder.add_consent(2, "ohm")

        sign_in_as(client, user_id=2, gitea_login="alice",
                   display_name="Alice", role="contributor")
        # Sanity: the binding is active and narrows the picker.
        r = client.get("/api/rfcs/ohm/models")
        assert [m["id"] for m in r.json()["models"]] == ["claude"]

        # Alice withdraws consent via the §17 endpoint.
        r = client.delete("/api/rfcs/ohm/funder/consent")
        assert r.status_code == 200

        # The picker flips back to the operator universe immediately.
        r = client.get("/api/rfcs/ohm/models")
        assert [m["id"] for m in r.json()["models"]] == ["claude", "gemini"]


def test_frontmatter_removal_flips_back_to_operator(app_with_fake_gitea):
    """§6.7's second revocation path: an admin/owner/arbiter edits the
    frontmatter to remove the field. The cache mirror picks up the
    change and the resolution flips back without touching consent."""
    from fastapi.testclient import TestClient
    from app import funder

    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)
        _install_two_providers(app)
        _set_funder_login("ohm", "alice")
        funder.upsert_credential(2, "anthropic", "alice-anthropic-key")
        funder.add_consent(2, "ohm")

        sign_in_as(client, user_id=2, gitea_login="alice",
                   display_name="Alice", role="contributor")
        r = client.get("/api/rfcs/ohm/models")
        assert [m["id"] for m in r.json()["models"]] == ["claude"]

        # Frontmatter edit removes the field — the cache mirror reflects
        # what `_upsert_cached_rfc` would write on the next sweep.
        _set_funder_login("ohm", None)

        r = client.get("/api/rfcs/ohm/models")
        assert [m["id"] for m in r.json()["models"]] == ["claude", "gemini"]


# ---------------------------------------------------------------------------
# §17 /api/users/me/funder surface
# ---------------------------------------------------------------------------


def test_funder_self_surface_lists_credentials_without_api_keys(app_with_fake_gitea):
    """The read endpoint must never return the API key itself, only the
    presence-of-registration metadata. The list is one row per
    registered family."""
    from fastapi.testclient import TestClient
    from app import funder

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        provision_user_row(user_id=2, login="alice", role="contributor")
        _install_two_providers(app)
        funder.upsert_credential(2, "anthropic", "secret-anthropic-key")
        funder.upsert_credential(2, "google", "secret-google-key")

        sign_in_as(client, user_id=2, gitea_login="alice",
                   display_name="Alice", role="contributor")
        r = client.get("/api/users/me/funder")
        assert r.status_code == 200
        body = r.json()
        providers_listed = sorted(c["provider"] for c in body["credentials"])
        assert providers_listed == ["anthropic", "google"]
        # The API key itself must not appear anywhere in the response —
        # the surface returns only registration metadata.
        assert "secret-anthropic-key" not in r.text
        assert "secret-google-key" not in r.text
        assert "api_key" not in r.text
        assert body["consents"] == []


def test_register_credential_refused_when_operator_has_not_enabled_family(app_with_fake_gitea):
    """§6.7: the funder cannot expand the operator universe. Registering
    Google credentials when the operator has only enabled Anthropic
    keys is refused."""
    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")
        # Operator enables only Anthropic-family keys.
        app.state.providers.clear()
        app.state.providers["claude"] = FakeProvider("op-claude")

        sign_in_as(client, user_id=2, gitea_login="alice",
                   display_name="Alice", role="contributor")
        r = client.post(
            "/api/users/me/funder/credentials",
            json={"provider": "google", "api_key": "alice-google-key"},
        )
        assert r.status_code == 409, r.text
        # Anthropic should be accepted since the operator has enabled
        # `claude` (an Anthropic-family picker key).
        r = client.post(
            "/api/users/me/funder/credentials",
            json={"provider": "anthropic", "api_key": "alice-anthropic-key"},
        )
        assert r.status_code == 200, r.text


def test_consent_refused_without_any_registered_credentials(app_with_fake_gitea):
    """§6.7: a consent without any registered credentials would be
    operationally inert (the funder universe would be empty). The
    endpoint refuses rather than silently writing the row."""
    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)
        _install_two_providers(app)

        sign_in_as(client, user_id=2, gitea_login="alice",
                   display_name="Alice", role="contributor")
        r = client.post("/api/rfcs/ohm/funder/consent")
        assert r.status_code == 409, r.text


def test_consent_endpoint_round_trip(app_with_fake_gitea):
    """The opt-in / opt-out cycle through the API surface: register a
    credential, consent to fund a slug, see it on the self-read, and
    withdraw."""
    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)
        _install_two_providers(app)

        sign_in_as(client, user_id=2, gitea_login="alice",
                   display_name="Alice", role="contributor")
        client.post(
            "/api/users/me/funder/credentials",
            json={"provider": "anthropic", "api_key": "alice-anthropic-key"},
        )
        r = client.post("/api/rfcs/ohm/funder/consent")
        assert r.status_code == 200

        r = client.get("/api/users/me/funder")
        assert r.json()["consents"] == ["ohm"]

        r = client.delete("/api/rfcs/ohm/funder/consent")
        assert r.status_code == 200

        r = client.get("/api/users/me/funder")
        assert r.json()["consents"] == []


def test_delete_credential_leaves_consents_intact(app_with_fake_gitea):
    """Per §6.7: deleting a credential leaves `funder_consents` rows in
    place; the resolved funder universe for affected RFCs simply
    shrinks. If the funder later re-registers, those RFCs resume
    drawing on the funder's universe without re-consenting."""
    from fastapi.testclient import TestClient
    from app import funder

    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)
        _install_two_providers(app)
        funder.upsert_credential(2, "anthropic", "key1")
        funder.add_consent(2, "ohm")

        sign_in_as(client, user_id=2, gitea_login="alice",
                   display_name="Alice", role="contributor")
        r = client.delete("/api/users/me/funder/credentials/anthropic")
        assert r.status_code == 200

        r = client.get("/api/users/me/funder")
        body = r.json()
        assert body["credentials"] == []
        assert body["consents"] == ["ohm"]  # consent survives


# ---------------------------------------------------------------------------
# §10.2 PR-draft and chat surfaces route through funder credentials
# ---------------------------------------------------------------------------


def test_pr_draft_routes_through_funder_credentials(app_with_fake_gitea, monkeypatch):
    """When a consenting funder is in effect on an RFC, the §10.2 draft
    endpoint constructs a provider with the funder's API key rather
    than reusing the operator's instance. The patched
    `construct_for_funder` records the picker key and the API key it
    was called with — both must come from the funder's registration."""
    from fastapi.testclient import TestClient
    from app import db, funder

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        provision_user_row(user_id=1, login="ben", role="owner")
        provision_user_row(user_id=2, login="alice", role="contributor")
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)
        _install_two_providers(app)
        _set_funder_login("ohm", "alice")
        funder.upsert_credential(2, "anthropic", "alice-anthropic-key")
        funder.add_consent(2, "ohm")

        funder_provider = _patch_funder_construct(monkeypatch, marker="alice-funded")

        sign_in_as(client, user_id=1, gitea_login="ben",
                   display_name="Ben", role="owner")
        r = client.post("/api/rfcs/ohm/branches/main/promote-to-branch", json={})
        branch = r.json()["branch_name"]

        repo_full = "wiggleverse/rfc-0001-ohm"
        owner, repo = repo_full.split("/", 1)
        new_body = SEED_BODY + "\n\nA further sentence."
        fake.files[(owner, repo, branch, "RFC.md")] = {
            "content": new_body, "sha": fake._next_sha(),
        }
        fake.branches[(owner, repo)][branch]["sha"] = fake.files[
            (owner, repo, branch, "RFC.md")
        ]["sha"]
        db.conn().execute(
            """
            UPDATE cached_branches SET head_sha = ?
             WHERE rfc_slug = 'ohm' AND branch_name = ?
            """,
            (fake.branches[(owner, repo)][branch]["sha"], branch),
        )

        r = client.post(f"/api/rfcs/ohm/branches/{branch}/pr-draft")
        assert r.status_code == 200, r.text
        # The funder-constructed provider was the one called — its
        # marker comes back in the title.
        assert r.json()["title"] == "alice-funded"
        # And the funder's API key was the one used.
        assert funder_provider._last_api_key == "alice-anthropic-key"
        assert funder_provider._last_picker_key == "claude"


def test_pr_draft_falls_back_to_stub_when_funder_lacks_default_family(app_with_fake_gitea):
    """If the funder is consenting but has not registered credentials
    for the family that serves the RFC's default model, the §10.2
    surface falls back to the deterministic stub — same behavior as
    operator-side opt-out. The lighter half deliberately does not
    fall back to operator credentials per §6.7's attribution-clean
    rule."""
    from fastapi.testclient import TestClient
    from app import db, funder

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        provision_user_row(user_id=1, login="ben", role="owner")
        provision_user_row(user_id=2, login="alice", role="contributor")
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)
        _install_two_providers(app)
        _set_funder_login("ohm", "alice")
        # Funder consented but registered Google only. Operator's default
        # would have been `claude` (Anthropic family); the resolved
        # universe collapses to `gemini`, and the §10.2 draft proceeds
        # against `gemini`. We test the *failure* path: register no
        # credentials at all and the universe collapses to empty.
        funder.add_consent(2, "ohm")  # add consent without credentials
        # But the add_consent helper bypasses the API gate; the resolver
        # still treats this as "consenting funder with no registered
        # families" — empty universe → opt-out shape.

        sign_in_as(client, user_id=1, gitea_login="ben",
                   display_name="Ben", role="owner")
        r = client.post("/api/rfcs/ohm/branches/main/promote-to-branch", json={})
        branch = r.json()["branch_name"]

        repo_full = "wiggleverse/rfc-0001-ohm"
        owner, repo = repo_full.split("/", 1)
        new_body = SEED_BODY + "\n\nA further sentence."
        fake.files[(owner, repo, branch, "RFC.md")] = {
            "content": new_body, "sha": fake._next_sha(),
        }
        fake.branches[(owner, repo)][branch]["sha"] = fake.files[
            (owner, repo, branch, "RFC.md")
        ]["sha"]
        db.conn().execute(
            """
            UPDATE cached_branches SET head_sha = ?
             WHERE rfc_slug = 'ohm' AND branch_name = ?
            """,
            (fake.branches[(owner, repo)][branch]["sha"], branch),
        )

        r = client.post(f"/api/rfcs/ohm/branches/{branch}/pr-draft")
        assert r.status_code == 200, r.text
        # Empty resolved universe → deterministic stub per Slice 3.
        assert r.json()["title"] == "Edits to OHM"


def test_chat_stream_routes_through_funder_credentials(app_with_fake_gitea, monkeypatch):
    """The §8.12 chat surface also picks up the funder routing. A chat
    turn on a branch of an RFC with a consenting funder constructs the
    provider with the funder's API key — same routing as §10.2."""
    from fastapi.testclient import TestClient
    from app import funder

    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)
        _install_two_providers(app)
        _set_funder_login("ohm", "alice")
        funder.upsert_credential(2, "anthropic", "alice-anthropic-key")
        funder.add_consent(2, "ohm")

        funder_provider = _patch_funder_construct(monkeypatch, marker="from-funder")

        sign_in_as(client, user_id=2, gitea_login="alice",
                   display_name="Alice", role="contributor")
        r = client.post("/api/rfcs/ohm/branches/main/promote-to-branch", json={})
        branch = r.json()["branch_name"]
        view = client.get(f"/api/rfcs/ohm/branches/{branch}").json()
        thread_id = view["main_thread_id"]

        r = client.post(
            f"/api/rfcs/ohm/branches/{branch}/threads/{thread_id}/chat",
            json={"text": "tighten this", "model": "claude"},
        )
        assert r.status_code == 200, r.text
        # The funder's API key was the one used by the constructed provider.
        assert funder_provider._last_api_key == "alice-anthropic-key"
        assert funder_provider._last_picker_key == "claude"


# ---------------------------------------------------------------------------
# Cache round-trip and entry parser
# ---------------------------------------------------------------------------


def test_meta_repo_frontmatter_funder_round_trips_through_cache(app_with_fake_gitea):
    """The production path: a meta-repo entry whose frontmatter carries
    `funder: alice` lands in `cached_rfcs.funder_login` after the next
    `refresh_meta_repo` sweep. Absent frontmatter lands NULL."""
    from fastapi.testclient import TestClient
    from app import cache as cache_mod, db

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        provision_user_row(user_id=1, login="ben", role="owner")
        entry_text = (
            "---\n"
            "slug: ohm\n"
            "title: OHM\n"
            "state: super-draft\n"
            "id: null\n"
            "repo: null\n"
            "proposed_by: ben\n"
            "proposed_at: 2026-05-23\n"
            "graduated_at: null\n"
            "graduated_by: null\n"
            "owners: []\n"
            "arbiters: []\n"
            "tags: []\n"
            "funder: alice\n"
            "---\n\n"
            "The pitch goes here.\n"
        )
        sha = fake._next_sha()
        fake.files[("wiggleverse", "meta", "main", "rfcs/ohm.md")] = {
            "content": entry_text, "sha": sha,
        }
        fake.branches[("wiggleverse", "meta")]["main"]["sha"] = sha

        asyncio.run(cache_mod.refresh_meta_repo(app.state.config, app.state.gitea))

        row = db.conn().execute(
            "SELECT funder_login FROM cached_rfcs WHERE slug = 'ohm'"
        ).fetchone()
        assert row is not None
        assert row["funder_login"] == "alice"

        # An entry without `funder:` lands NULL.
        no_funder_text = entry_text.replace("funder: alice\n", "")
        sha2 = fake._next_sha()
        fake.files[("wiggleverse", "meta", "main", "rfcs/beta.md")] = {
            "content": no_funder_text.replace("slug: ohm", "slug: beta")
                                     .replace("title: OHM", "title: Beta"),
            "sha": sha2,
        }
        fake.branches[("wiggleverse", "meta")]["main"]["sha"] = sha2

        asyncio.run(cache_mod.refresh_meta_repo(app.state.config, app.state.gitea))

        row = db.conn().execute(
            "SELECT funder_login FROM cached_rfcs WHERE slug = 'beta'"
        ).fetchone()
        assert row is not None
        assert row["funder_login"] is None


def test_entry_funder_field_round_trip():
    """parse(serialize(x)) preserves the funder field — None stays None
    (no key emitted), populated stays populated. Mirrors the §6.6
    absent/empty/populated test for `models:`."""
    from app import entry as entry_mod

    absent = entry_mod.Entry(slug="x", title="X")
    assert absent.funder is None
    text_absent = entry_mod.serialize(absent)
    assert "funder:" not in text_absent
    round_absent = entry_mod.parse(text_absent)
    assert round_absent.funder is None

    populated = entry_mod.Entry(slug="y", title="Y", funder="alice")
    text_populated = entry_mod.serialize(populated)
    assert "funder: alice" in text_populated
    round_populated = entry_mod.parse(text_populated)
    assert round_populated.funder == "alice"
