"""End-to-end integration test for the §19.2 "cache bootstrap from a
pre-existing meta repo" candidate that Slice 8 settles.

Per §4.1, the cache is rebuildable from Gitea. Per §15.9, the cache
resolves the actor on a PR by joining against the §6.5 `actions` log,
falling back to the `On-behalf-of:` trailer in the PR body, then to
the raw Gitea login as last resort. Slice 1 chose this fallback chain
and Slice 8 exercises it against history the bot did not author —
the disaster-recovery / transferred-meta-repo case.

The tests prove:

  * When `actions` carries a matching row, the audit log wins —
    `opened_by` reads the on-behalf-of login regardless of the bot's
    appearance as Gitea opener.
  * When `actions` is empty but the PR body carries
    `On-behalf-of: Name <login>`, the trailer parses cleanly.
  * When both are absent, the raw Gitea opener wins (and if even
    that is the bot, the bot login is what surfaces — the v1
    fallback, honest about who Gitea sees).
  * The §15.9 framing holds: the bot login surfaces only in the
    Git log and the trailer, never inside the audit-log path.
"""
from __future__ import annotations

import json
import pytest

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


def _seed_meta_pr_directly_in_fake(
    fake: FakeGitea,
    *,
    slug: str,
    pr_number: int,
    head_branch: str,
    title: str,
    body: str,
    gitea_opener_login: str,
) -> None:
    """Push a PR into FakeGitea as if it existed before the app cache
    was bootstrapped — no propose endpoint, no audit log row, just a
    pull on the meta repo with a chosen `user.login`. The reconciler
    pulls this on its first sweep."""
    pr = {
        "number": pr_number,
        "title": title,
        "body": body,
        "head": {"ref": head_branch, "sha": "sha-bootstrap"},
        "base": {"ref": "main"},
        "state": "open",
        "merged": False,
        "merged_at": None,
        "closed_at": None,
        "created_at": "2026-05-23T00:00:00Z",
        "user": {"login": gitea_opener_login},
    }
    fake.pulls.setdefault(("wiggleverse", "meta"), []).append(pr)
    # Create a corresponding branch so refresh_meta_branches sees it.
    fake.branches[("wiggleverse", "meta")][head_branch] = {
        "sha": "sha-bootstrap", "ts": "2026-05-23T00:00:00Z",
    }


def test_audit_log_first_wins_over_bot_opener(app_with_fake_gitea):
    """If `actions` carries a row for this PR, that's the authoritative
    actor — even when Gitea reports the bot as the opener. This is the
    common steady-state path (the bot opened on behalf of a human, the
    audit log captured it)."""
    from fastapi.testclient import TestClient
    from app import cache as cache_mod, db

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        # Seed a cached RFC and an `actions` row for the PR before
        # the reconciler runs.
        db.conn().execute(
            """
            INSERT INTO cached_rfcs
                (slug, title, state, owners_json, arbiters_json, tags_json, body)
            VALUES ('alpha', 'Alpha', 'super-draft', '[]', '[]', '[]', 'pitch')
            """
        )
        db.conn().execute(
            """
            INSERT INTO actions
                (actor_user_id, on_behalf_of, action_kind, rfc_slug, pr_number)
            VALUES (NULL, 'alice', 'propose_rfc', 'alpha', 99)
            """
        )
        # Seed the fake's PR as if Gitea reports the bot as opener.
        _seed_meta_pr_directly_in_fake(
            fake,
            slug="alpha", pr_number=99,
            head_branch="propose/alpha",
            title="Propose: Alpha",
            body="Some idea\n",
            gitea_opener_login="rfc-bot",
        )

        import asyncio
        asyncio.run(cache_mod.refresh_meta_pulls(app.state.config, app.state.gitea))

        row = db.conn().execute(
            "SELECT opened_by FROM cached_prs WHERE pr_number = 99"
        ).fetchone()
        assert row is not None
        # Audit-log row wins.
        assert row["opened_by"] == "alice"


def test_trailer_parses_when_audit_log_missing(app_with_fake_gitea):
    """The cache-bootstrap-against-history-the-bot-did-not-author case:
    no `actions` rows, but the PR body carries the §6.5
    `On-behalf-of:` trailer. The trailer parses cleanly and the right
    actor surfaces."""
    from fastapi.testclient import TestClient
    from app import cache as cache_mod, db

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        db.conn().execute(
            """
            INSERT INTO cached_rfcs
                (slug, title, state, owners_json, arbiters_json, tags_json, body)
            VALUES ('beta', 'Beta', 'super-draft', '[]', '[]', '[]', 'pitch')
            """
        )
        # No actions row. PR body carries the trailer.
        _seed_meta_pr_directly_in_fake(
            fake,
            slug="beta", pr_number=42,
            head_branch="propose/beta",
            title="Propose: Beta",
            body="A new framing\n\nOn-behalf-of: Charlie <charlie>",
            gitea_opener_login="rfc-bot",
        )

        import asyncio
        asyncio.run(cache_mod.refresh_meta_pulls(app.state.config, app.state.gitea))

        row = db.conn().execute(
            "SELECT opened_by FROM cached_prs WHERE pr_number = 42"
        ).fetchone()
        assert row["opened_by"] == "charlie"


def test_raw_gitea_login_used_when_audit_and_trailer_both_absent(app_with_fake_gitea):
    """When neither the audit log nor the trailer carries an actor,
    the raw Gitea login is the last-resort fallback per §15.9 / Slice 1.
    A non-bot login surfaces as the actor; a bot login surfaces as
    the bot (honest about what Gitea sees — the v1 contract)."""
    from fastapi.testclient import TestClient
    from app import cache as cache_mod, db

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        db.conn().execute(
            """
            INSERT INTO cached_rfcs
                (slug, title, state, owners_json, arbiters_json, tags_json, body)
            VALUES ('gamma', 'Gamma', 'super-draft', '[]', '[]', '[]', 'pitch')
            """
        )
        # No audit row, no trailer — but Gitea reports a real user as opener.
        _seed_meta_pr_directly_in_fake(
            fake,
            slug="gamma", pr_number=7,
            head_branch="propose/gamma",
            title="Propose: Gamma",
            body="No trailer here\n",
            gitea_opener_login="dana",  # a real human, not the bot
        )

        import asyncio
        asyncio.run(cache_mod.refresh_meta_pulls(app.state.config, app.state.gitea))

        row = db.conn().execute(
            "SELECT opened_by FROM cached_prs WHERE pr_number = 7"
        ).fetchone()
        assert row["opened_by"] == "dana"


def test_full_reconciler_sweep_resolves_actors_via_fallback_chain(app_with_fake_gitea):
    """End-to-end: a clean `cached_*` set plus a meta repo that has
    history with no audit-log rows. The reconciler's first sweep must
    bring everything up correctly — every PR resolves an actor through
    the fallback chain without crashing."""
    from fastapi.testclient import TestClient
    from app import cache as cache_mod, db

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        # Three PRs, three fallback paths exercised.
        # 1. With audit-log row.
        db.conn().execute(
            """
            INSERT INTO cached_rfcs
                (slug, title, state, owners_json, arbiters_json, tags_json, body)
            VALUES ('alpha', 'Alpha', 'super-draft', '[]', '[]', '[]', 'pitch')
            """
        )
        db.conn().execute(
            """
            INSERT INTO actions (actor_user_id, on_behalf_of, action_kind, rfc_slug, pr_number)
            VALUES (NULL, 'alice', 'propose_rfc', 'alpha', 1)
            """
        )
        _seed_meta_pr_directly_in_fake(
            fake, slug="alpha", pr_number=1,
            head_branch="propose/alpha", title="A", body="b",
            gitea_opener_login="rfc-bot",
        )
        # 2. Trailer only.
        db.conn().execute(
            """
            INSERT INTO cached_rfcs
                (slug, title, state, owners_json, arbiters_json, tags_json, body)
            VALUES ('beta', 'Beta', 'super-draft', '[]', '[]', '[]', 'pitch')
            """
        )
        _seed_meta_pr_directly_in_fake(
            fake, slug="beta", pr_number=2,
            head_branch="propose/beta", title="B",
            body="On-behalf-of: Bob <bob>",
            gitea_opener_login="rfc-bot",
        )
        # 3. Gitea opener as last resort.
        db.conn().execute(
            """
            INSERT INTO cached_rfcs
                (slug, title, state, owners_json, arbiters_json, tags_json, body)
            VALUES ('gamma', 'Gamma', 'super-draft', '[]', '[]', '[]', 'pitch')
            """
        )
        _seed_meta_pr_directly_in_fake(
            fake, slug="gamma", pr_number=3,
            head_branch="propose/gamma", title="G", body="naked",
            gitea_opener_login="carol",
        )

        import asyncio
        asyncio.run(cache_mod.refresh_meta_pulls(app.state.config, app.state.gitea))

        prs = {
            r["pr_number"]: r["opened_by"]
            for r in db.conn().execute(
                "SELECT pr_number, opened_by FROM cached_prs ORDER BY pr_number"
            )
        }
        assert prs[1] == "alice"   # audit log wins
        assert prs[2] == "bob"     # trailer wins
        assert prs[3] == "carol"   # raw Gitea opener wins
