"""End-to-end integration tests for the Slice 4 vertical (§9.4–§9.7 in full).

Walks the super-draft body-editing path end-to-end against the in-process
FakeGitea from test_propose_vertical.py:

  * Seed a super-draft from the propose+merge flow already proven by Slice 1.
  * GET /api/rfcs/<slug>/main returns the canonical body + breadcrumb data.
  * POST /api/rfcs/<slug>/start-edit-branch cuts a meta-repo edit branch.
  * GET /api/rfcs/<slug>/branches/<edit-branch> returns the body extracted
    from the entry envelope, ready for the editor.
  * POST .../changes/<id>/accept commits to rfcs/<slug>.md on the edit
    branch, with the frontmatter preserved.
  * POST .../manual-flush commits a manual edit similarly.
  * POST .../open-pr opens a meta_body_edit PR.
  * POST .../prs/<n>/merge propagates the body to meta-main, where it
    surfaces back into cached_rfcs.body for the next catalog render.
  * POST .../metadata opens a metadata PR; merge propagates the title.
  * Withdraw the body-edit PR; the body on the edit branch is untouched
    but the cache shows state='withdrawn'.

The active-RFC PR flow tests in test_pr_flow_vertical.py exercise the
parallel structural surface; this file's job is to prove the dispatch
works against the meta-repo target uniformly.
"""
from __future__ import annotations

import pytest

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


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def seed_super_draft(fake: FakeGitea, *, slug: str, title: str, pitch: str,
                     proposed_by: str = "alice", tags: list[str] | None = None) -> None:
    """Seed a super-draft entry directly on meta-main, skipping the
    propose+merge round-trip the Slice 1 tests cover separately.

    The cache is also primed so the API doesn't have to wait for a
    reconciler sweep before exercising super-draft endpoints.
    """
    import json as _json
    import yaml
    from app import db

    fm = {
        "slug": slug,
        "title": title,
        "state": "super-draft",
        "id": None,
        "repo": None,
        "proposed_by": proposed_by,
        "proposed_at": "2026-05-23",
        "graduated_at": None,
        "graduated_by": None,
        "owners": [],
        "arbiters": [],
        "tags": tags or [],
    }
    body = pitch.strip() + "\n"
    entry_text = f"---\n{yaml.safe_dump(fm, sort_keys=False).rstrip()}\n---\n\n{body}"
    sha = fake._next_sha()
    fake.files[("wiggleverse", "meta", "main", f"rfcs/{slug}.md")] = {
        "content": entry_text,
        "sha": sha,
    }
    # Advance meta-main's tip sha so the bot's create_branch snapshots
    # the freshly-seeded file. (Otherwise the branch snapshot starts
    # empty and the read fails until the next file commit.)
    fake.branches[("wiggleverse", "meta")]["main"]["sha"] = sha

    db.conn().execute(
        """
        INSERT OR REPLACE INTO cached_rfcs
            (slug, title, state, rfc_id, repo, proposed_by, proposed_at,
             owners_json, arbiters_json, tags_json,
             body, body_sha, last_main_commit_at, last_entry_commit_at)
        VALUES (?, ?, 'super-draft', NULL, NULL, ?, '2026-05-23',
                ?, ?, ?,
                ?, ?, datetime('now'), datetime('now'))
        """,
        (
            slug,
            title,
            proposed_by,
            _json.dumps([]),
            _json.dumps([]),
            _json.dumps(tags or []),
            body,
            sha,
        ),
    )
    # Synthesize the per-slug meta-main row so has-commits-ahead works
    # without waiting for the reconciler.
    db.conn().execute(
        """
        INSERT OR REPLACE INTO cached_branches
            (rfc_slug, branch_name, head_sha, state, last_commit_at)
        VALUES (?, 'main', ?, 'open', datetime('now'))
        """,
        (slug, sha),
    )


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------


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


def test_super_draft_main_view_returns_body_and_breadcrumb(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_super_draft(fake, slug="ohm", title="Open Human Model", pitch=PITCH)
        sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor")

        r = client.get("/api/rfcs/ohm/main")
        assert r.status_code == 200, r.text
        d = r.json()
        assert d["state"] == "super-draft"
        assert d["id"] is None
        assert d["repo"] is None
        assert "Open Human Model is a framework" in d["body"]
        # No edit branches yet; the dropdown is empty above 'canonical body'.
        assert d["branches"] == []
        assert d["open_prs"] == []


def test_start_edit_branch_cuts_meta_branch_and_writes_cache(app_with_fake_gitea):
    from fastapi.testclient import TestClient
    from app import db

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        provision_user_row(user_id=2, login="alice", role="contributor")
        seed_super_draft(fake, slug="ohm", title="OHM", pitch=PITCH)
        sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor")

        r = client.post("/api/rfcs/ohm/start-edit-branch", json={})
        assert r.status_code == 200, r.text
        branch_name = r.json()["branch_name"]
        # §9.5: edit-<slug>-<6hex> per Slice 4's naming convention.
        assert branch_name.startswith("edit-ohm-")
        # The branch landed on Gitea.
        assert branch_name in fake.branches[("wiggleverse", "meta")]
        # The bot's audit row records the gesture.
        rows = db.conn().execute(
            "SELECT action_kind, on_behalf_of, branch_name FROM actions WHERE action_kind = 'create_branch'"
        ).fetchall()
        assert any(r["branch_name"] == branch_name and r["on_behalf_of"] == "alice" for r in rows)
        # cached_branches sees the new row.
        cached = db.conn().execute(
            "SELECT branch_name FROM cached_branches WHERE rfc_slug = 'ohm' AND branch_name = ?",
            (branch_name,),
        ).fetchone()
        assert cached is not None


def test_branch_view_extracts_body_from_entry_envelope(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_super_draft(fake, slug="ohm", title="OHM", pitch=PITCH)
        sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor")

        branch = client.post("/api/rfcs/ohm/start-edit-branch", json={}).json()["branch_name"]
        r = client.get(f"/api/rfcs/ohm/branches/{branch}")
        assert r.status_code == 200, r.text
        view = r.json()
        # The frontmatter is stripped — the editable body is the pitch.
        assert view["body"].startswith("Open Human Model is a framework")
        assert "---" not in view["body"]
        assert view["main_thread_id"]


def test_accept_change_commits_to_meta_repo_and_preserves_frontmatter(app_with_fake_gitea):
    from fastapi.testclient import TestClient
    from app import db, entry as entry_mod

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        provision_user_row(user_id=2, login="alice", role="contributor")
        seed_super_draft(fake, slug="ohm", title="OHM", pitch=PITCH)
        sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor")

        branch = client.post("/api/rfcs/ohm/start-edit-branch", json={}).json()["branch_name"]
        view = client.get(f"/api/rfcs/ohm/branches/{branch}").json()
        thread_id = view["main_thread_id"]
        cur = db.conn().execute(
            """
            INSERT INTO changes (rfc_slug, branch_name, thread_id, kind, state,
                                 original, proposed, reason)
            VALUES ('ohm', ?, ?, 'ai', 'pending', ?, ?, 'tightens scope')
            """,
            (
                branch,
                thread_id,
                "Open Human Model is a framework for representing humans.",
                "Open Human Model is a framework for representing humans across software systems.",
            ),
        )
        change_id = cur.lastrowid

        r = client.post(
            f"/api/rfcs/ohm/branches/{branch}/changes/{change_id}/accept",
            json={
                "proposed": "Open Human Model is a framework for representing humans across software systems.",
                "was_edited_before_accept": False,
            },
        )
        assert r.status_code == 200, r.text
        commit_sha = r.json()["commit_sha"]
        assert commit_sha

        # The file on the meta-repo edit branch carries both the
        # frontmatter and the mutated body — the round-trip preserved
        # the envelope.
        file_content = fake.files[("wiggleverse", "meta", branch, "rfcs/ohm.md")]["content"]
        entry = entry_mod.parse(file_content)
        assert entry.state == "super-draft"
        assert entry.slug == "ohm"
        assert "across software systems" in entry.body
        # The cached change row tracks the commit.
        row = db.conn().execute(
            "SELECT state, commit_sha FROM changes WHERE id = ?", (change_id,)
        ).fetchone()
        assert row["state"] == "accepted"
        assert row["commit_sha"] == commit_sha


def test_manual_flush_commits_through_frontmatter_envelope(app_with_fake_gitea):
    from fastapi.testclient import TestClient
    from app import entry as entry_mod, db

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        provision_user_row(user_id=2, login="alice", role="contributor")
        seed_super_draft(fake, slug="ohm", title="OHM", pitch=PITCH)
        sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor")

        branch = client.post("/api/rfcs/ohm/start-edit-branch", json={}).json()["branch_name"]
        new_body = PITCH + "\n\nA fresh closing paragraph that landed manually.\n"
        r = client.post(
            f"/api/rfcs/ohm/branches/{branch}/manual-flush",
            json={"new_content": new_body, "paragraph_count": 1},
        )
        assert r.status_code == 200, r.text
        assert r.json()["commit_sha"]

        file_content = fake.files[("wiggleverse", "meta", branch, "rfcs/ohm.md")]["content"]
        entry = entry_mod.parse(file_content)
        assert "fresh closing paragraph" in entry.body
        # §10.6: a system-author chat message records the flush.
        rows = db.conn().execute(
            """
            SELECT m.role, m.text FROM thread_messages m
              JOIN threads t ON t.id = m.thread_id
             WHERE t.rfc_slug = 'ohm' AND t.branch_name = ?
            """,
            (branch,),
        ).fetchall()
        assert any(r["role"] == "system" and "manual edit" in r["text"] for r in rows)


def test_open_pr_on_super_draft_lands_as_meta_body_edit(app_with_fake_gitea):
    from fastapi.testclient import TestClient
    from app import db

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        provision_user_row(user_id=2, login="alice", role="contributor")
        seed_super_draft(fake, slug="ohm", title="OHM", pitch=PITCH)
        sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor")

        branch = client.post("/api/rfcs/ohm/start-edit-branch", json={}).json()["branch_name"]
        # Accept one change so the branch has commits ahead of meta-main.
        view = client.get(f"/api/rfcs/ohm/branches/{branch}").json()
        thread_id = view["main_thread_id"]
        cur = db.conn().execute(
            """
            INSERT INTO changes (rfc_slug, branch_name, thread_id, kind, state,
                                 original, proposed, reason)
            VALUES ('ohm', ?, ?, 'ai', 'pending', ?, ?, 'test')
            """,
            (
                branch,
                thread_id,
                "Open Human Model is a framework for representing humans.",
                "Open Human Model is a framework for representing humans across systems.",
            ),
        )
        change_id = cur.lastrowid
        client.post(
            f"/api/rfcs/ohm/branches/{branch}/changes/{change_id}/accept",
            json={
                "proposed": "Open Human Model is a framework for representing humans across systems.",
                "was_edited_before_accept": False,
            },
        )

        r = client.post(
            f"/api/rfcs/ohm/branches/{branch}/open-pr",
            json={"title": "Tighten scope", "description": "Scope to systems."},
        )
        assert r.status_code == 200, r.text
        pr_number = r.json()["pr_number"]
        # cached_prs records pr_kind='meta_body_edit'.
        row = db.conn().execute(
            "SELECT pr_kind, repo, head_branch FROM cached_prs WHERE pr_number = ?",
            (pr_number,),
        ).fetchone()
        assert row["pr_kind"] == "meta_body_edit"
        assert row["repo"] == "wiggleverse/meta"
        assert row["head_branch"] == branch

        # The §10.3 PR view payload renders against the meta repo with
        # the body extracted from the envelope on both sides.
        d = client.get(f"/api/rfcs/ohm/prs/{pr_number}").json()
        assert "across systems" in d["branch_body"]
        assert "across systems" not in d["main_body"]
        assert d["state"] == "open"


def test_merge_super_draft_body_edit_propagates_to_canonical_body(app_with_fake_gitea):
    """The whole §9.5 loop end-to-end: cut an edit branch, accept a
    change, open a body-edit PR, merge it, and watch the super-draft's
    cached body update."""
    from fastapi.testclient import TestClient
    from app import db

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        provision_user_row(user_id=2, login="alice", role="contributor")
        provision_user_row(user_id=1, login="ben", role="owner")
        seed_super_draft(fake, slug="ohm", title="OHM", pitch=PITCH)
        sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor")

        branch = client.post("/api/rfcs/ohm/start-edit-branch", json={}).json()["branch_name"]
        view = client.get(f"/api/rfcs/ohm/branches/{branch}").json()
        thread_id = view["main_thread_id"]
        cur = db.conn().execute(
            """
            INSERT INTO changes (rfc_slug, branch_name, thread_id, kind, state,
                                 original, proposed, reason)
            VALUES ('ohm', ?, ?, 'ai', 'pending', ?, ?, 'tightens')
            """,
            (
                branch,
                thread_id,
                "It defines consent, trait, and agency in compatible terms.",
                "It defines consent, trait, harm, and agency in compatible terms.",
            ),
        )
        change_id = cur.lastrowid
        client.post(
            f"/api/rfcs/ohm/branches/{branch}/changes/{change_id}/accept",
            json={
                "proposed": "It defines consent, trait, harm, and agency in compatible terms.",
                "was_edited_before_accept": False,
            },
        )
        pr_number = client.post(
            f"/api/rfcs/ohm/branches/{branch}/open-pr",
            json={"title": "Add harm", "description": "Adds harm to the dimension list."},
        ).json()["pr_number"]

        # An unclaimed super-draft: only app admins/owners can merge per §9.5.
        # Alice is a contributor — refused.
        r = client.post(f"/api/rfcs/ohm/prs/{pr_number}/merge")
        assert r.status_code == 403

        # Ben (app owner) can merge.
        sign_in_as(client, user_id=1, gitea_login="ben", display_name="Ben", role="owner")
        r = client.post(f"/api/rfcs/ohm/prs/{pr_number}/merge")
        assert r.status_code == 200, r.text

        # Meta-main's rfcs/ohm.md now carries the body change; the
        # cache picks it up; the catalog/view render the new body.
        r = client.get("/api/rfcs/ohm/main")
        d = r.json()
        assert "harm" in d["body"]


def test_metadata_pane_pr_propagates_title_on_merge(app_with_fake_gitea):
    from fastapi.testclient import TestClient
    from app import db

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        provision_user_row(user_id=1, login="ben", role="owner")
        seed_super_draft(fake, slug="ohm", title="OHM", pitch=PITCH)
        sign_in_as(client, user_id=1, gitea_login="ben", display_name="Ben", role="owner")

        r = client.post(
            "/api/rfcs/ohm/metadata",
            json={"title": "Open Human Model", "tags": ["identity", "schema"]},
        )
        assert r.status_code == 200, r.text
        pr_number = r.json()["pr_number"]
        metadata_branch = r.json()["branch_name"]
        assert metadata_branch.startswith("metadata-ohm-")

        # cached_prs records pr_kind='meta_metadata'.
        row = db.conn().execute(
            "SELECT pr_kind, title FROM cached_prs WHERE pr_number = ?",
            (pr_number,),
        ).fetchone()
        assert row["pr_kind"] == "meta_metadata"

        # Merge the metadata PR — the per-RFC PR-flow merge endpoint
        # works only for meta_body_edit (rfc_branch) kinds. For metadata
        # PRs, we exercise the bot's merge against the meta repo directly
        # via the underlying Gitea client.
        from app.bot import Actor
        bot = app.state.bot
        actor = Actor(user_id=1, gitea_login="ben", display_name="Ben", email="ben@test")
        import asyncio
        asyncio.run(_merge_meta_pr(bot, actor, pr_number=pr_number, slug="ohm"))

        # Refresh the meta cache so the new title surfaces.
        from app import cache as cache_mod
        asyncio.run(cache_mod.refresh_meta_repo(app.state.config, app.state.gitea))
        asyncio.run(cache_mod.refresh_meta_pulls(app.state.config, app.state.gitea))

        d = client.get("/api/rfcs/ohm").json()
        assert d["title"] == "Open Human Model"
        assert "identity" in d["tags"]


async def _merge_meta_pr(bot, actor, *, pr_number, slug):
    """Helper: invoke the bot's merge path against the meta repo. The
    metadata-PR merge surface isn't exposed via api_prs (which only
    handles body-edit PRs) — admins/owners merge through Gitea directly
    via the bot for v1. A dedicated metadata-PR merge endpoint earns its
    own §19.2 candidate if usage shows demand."""
    # We use merge_branch_pr as the bot's generic meta-merge primitive;
    # it takes owner/repo, which dispatches to the meta repo here.
    await bot.merge_branch_pr(
        actor,
        owner="wiggleverse",
        repo="meta",
        pr_number=pr_number,
        head_branch=f"metadata-{slug}-stub",  # name only used in the audit log
        slug=slug,
    )


def test_super_draft_canonical_body_branch_main_is_read_only(app_with_fake_gitea):
    """branchParam='main' on a super-draft view fetches meta-main's
    rfcs/<slug>.md but contributing is refused — the only edit path is
    via start-edit-branch."""
    from fastapi.testclient import TestClient
    from app import db

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        provision_user_row(user_id=2, login="alice", role="contributor")
        seed_super_draft(fake, slug="ohm", title="OHM", pitch=PITCH)
        sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor")

        r = client.get("/api/rfcs/ohm/branches/main")
        assert r.status_code == 200, r.text
        view = r.json()
        assert view["branch_name"] == "main"
        assert "Open Human Model" in view["body"]
        # Capabilities reflect read-only.
        assert view["capabilities"]["can_contribute"] is False

        # A manual-flush against 'main' is refused as a contribute check.
        r = client.post(
            "/api/rfcs/ohm/branches/main/manual-flush",
            json={"new_content": "hijacked\n", "paragraph_count": 1},
        )
        assert r.status_code == 403


def test_metadata_pane_refused_for_plain_contributor(app_with_fake_gitea):
    """§9.5: until the §13.1 claim runs, the metadata pane is limited to
    app admins/owners. A plain contributor 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")
        seed_super_draft(fake, slug="ohm", title="OHM", pitch=PITCH)
        sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor")

        r = client.post(
            "/api/rfcs/ohm/metadata",
            json={"title": "Sneak title"},
        )
        assert r.status_code == 403
