"""End-to-end integration tests for v0.16.0's owner-only invite for
per-RFC PR or PR-less discussion (roadmap item #12, §6 / §10).

The release lands a per-RFC membership layer:

  * `rfc_invitations` — issued by the RFC's owner, addressed to an
    email, granting one of two roles ('contributor' or 'discussant').
  * `rfc_collaborators` — the accepted-invitation substrate; the
    table the per-RFC write gate consults.

The tests prove:

  * Only the RFC's owner (or a platform admin/owner) can invite —
    a platform-granted but non-owner user gets 403.
  * Creating an invitation lands a row, mints a token, and queues
    an envelope on the SMTP buffer.
  * Re-inviting the same (email, role) on the same RFC returns 409.
  * The accept endpoint requires the accepting user's email to match
    the invitee_email (case-insensitive).
  * Acceptance lands a rfc_collaborators row and flips the
    invitation to 'accepted'.
  * Re-accepting the same invitation is idempotent (200, changed=false).
  * An expired invitation refuses 409 even if the row's column status
    is still 'pending'.
  * A revoked invitation refuses 409.
  * The owner's listing carries pending + accepted in one response.
  * The per-RFC discussion-write gate refuses a non-invited
    platform-granted user 403 (was previously 200 before v0.16.0).
  * The same gate admits a user who holds an accepted 'discussant'
    invitation.
  * The same gate admits a user who holds an accepted 'contributor'
    invitation (contributor strictly includes discussion).
  * The platform admin/owner is admitted regardless of per-RFC
    membership (the platform-level capability path).
  * The /api/admin/users listing carries `rfc_invitations` per-user
    after an acceptance — the §17 admin surface hook.
"""
from __future__ import annotations

# Reuse fixtures and helpers from the propose / RFC-view harnesses.
from test_propose_vertical import (  # noqa: F401 — fixtures land via import
    FakeGitea,
    app_with_fake_gitea,
    provision_user_row,
    sign_in_as,
    tmp_env,
)
from test_rfc_view_vertical import seed_active_rfc, SEED_BODY


def _reset_outbound():
    from app import email as email_mod
    email_mod.reset_sent_envelopes()


def _invitation_envelopes(to_address: str | None = None) -> list[dict]:
    """Pluck v0.16.0 invitation envelopes out of the shared _SENT buffer.
    Same access pattern as the OTC tests use for `kind='otc'`."""
    from app import email as email_mod
    out = []
    for env in email_mod.sent_envelopes():
        if env.get("kind") != "rfc_invitation":
            continue
        if to_address is not None and env["to"] != to_address:
            continue
        out.append(env)
    return out


# ---------------------------------------------------------------------------
# Create / list / revoke (owner-side)
# ---------------------------------------------------------------------------


def test_owner_can_invite_creates_row_and_sends_email(app_with_fake_gitea):
    """The end-to-end create gesture: RFC owner posts an invitation,
    a row lands, the token comes back in the response, and an
    `rfc_invitation`-kind envelope hits the SMTP buffer."""
    from fastapi.testclient import TestClient
    from app import db

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        _reset_outbound()
        # The frontmatter owner of the seeded RFC is "alice" (per
        # seed_active_rfc's default), so we sign in as that user.
        provision_user_row(user_id=1, login="alice", role="contributor")
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)
        sign_in_as(
            client, user_id=1, gitea_login="alice",
            display_name="Alice", role="contributor",
        )

        r = client.post(
            "/api/rfcs/ohm/invitations",
            json={"invitee_email": "newperson@example.com", "role_in_rfc": "contributor"},
        )
        assert r.status_code == 200, r.text
        body = r.json()
        assert body["rfc_slug"] == "ohm"
        assert body["invitee_email"] == "newperson@example.com"
        assert body["role_in_rfc"] == "contributor"
        assert body["status"] == "pending"
        assert body["token"] and len(body["token"]) > 16

        # Row landed.
        row = db.conn().execute(
            "SELECT * FROM rfc_invitations WHERE id = ?", (body["id"],),
        ).fetchone()
        assert row["rfc_slug"] == "ohm"
        assert row["invitee_email"] == "newperson@example.com"
        assert row["inviter_user_id"] == 1
        assert row["status"] == "pending"

        # Email envelope went out.
        envs = _invitation_envelopes("newperson@example.com")
        assert len(envs) == 1
        assert "OHM" in envs[0]["subject"]
        assert body["token"] in envs[0]["body"]


def test_non_owner_cannot_invite(app_with_fake_gitea):
    """A platform-granted user who isn't in the RFC's frontmatter
    owners list cannot invite — 403. Distinct from the
    require_contributor gate (which would be 401 for anonymous)."""
    from fastapi.testclient import TestClient

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        # alice is the RFC owner per the seed; bob is a regular
        # platform-granted contributor with no per-RFC role.
        provision_user_row(user_id=1, login="alice", role="contributor")
        provision_user_row(user_id=2, login="bob", role="contributor")
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)
        sign_in_as(
            client, user_id=2, gitea_login="bob",
            display_name="Bob", role="contributor",
        )
        r = client.post(
            "/api/rfcs/ohm/invitations",
            json={"invitee_email": "ignored@example.com", "role_in_rfc": "discussant"},
        )
        assert r.status_code == 403


def test_platform_admin_can_invite_to_any_rfc(app_with_fake_gitea):
    """Per §6.1 the platform admin/owner role carries the maximal
    per-RFC capability, so admins can invite on any RFC even if
    they're not in its owners list."""
    from fastapi.testclient import TestClient

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        _reset_outbound()
        provision_user_row(user_id=1, login="alice", role="contributor")
        provision_user_row(user_id=99, login="adminzero", role="admin")
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)
        sign_in_as(
            client, user_id=99, gitea_login="adminzero",
            display_name="Admin Zero", role="admin",
        )
        r = client.post(
            "/api/rfcs/ohm/invitations",
            json={"invitee_email": "another@example.com", "role_in_rfc": "discussant"},
        )
        assert r.status_code == 200, r.text


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

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)
        r = client.post(
            "/api/rfcs/ohm/invitations",
            json={"invitee_email": "x@example.com", "role_in_rfc": "discussant"},
        )
        assert r.status_code == 401


def test_re_invite_same_email_and_role_returns_409(app_with_fake_gitea):
    """Refuse a duplicate pending invitation for the same (email, role)
    on the same RFC. A different role on the same email is allowed
    (the owner may want to upgrade discussant → contributor)."""
    from fastapi.testclient import TestClient

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        _reset_outbound()
        provision_user_row(user_id=1, login="alice", role="contributor")
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)
        sign_in_as(
            client, user_id=1, gitea_login="alice",
            display_name="Alice", role="contributor",
        )

        r1 = client.post(
            "/api/rfcs/ohm/invitations",
            json={"invitee_email": "dup@example.com", "role_in_rfc": "discussant"},
        )
        assert r1.status_code == 200
        r2 = client.post(
            "/api/rfcs/ohm/invitations",
            json={"invitee_email": "dup@example.com", "role_in_rfc": "discussant"},
        )
        assert r2.status_code == 409

        # Same email, different role is allowed.
        r3 = client.post(
            "/api/rfcs/ohm/invitations",
            json={"invitee_email": "dup@example.com", "role_in_rfc": "contributor"},
        )
        assert r3.status_code == 200


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

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        _reset_outbound()
        provision_user_row(user_id=1, login="alice", role="contributor")
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)
        sign_in_as(
            client, user_id=1, gitea_login="alice",
            display_name="Alice", role="contributor",
        )

        client.post("/api/rfcs/ohm/invitations",
                    json={"invitee_email": "a@example.com", "role_in_rfc": "discussant"})
        client.post("/api/rfcs/ohm/invitations",
                    json={"invitee_email": "b@example.com", "role_in_rfc": "contributor"})

        r = client.get("/api/rfcs/ohm/invitations")
        assert r.status_code == 200, r.text
        items = r.json()["items"]
        emails = sorted(i["invitee_email"] for i in items)
        assert emails == ["a@example.com", "b@example.com"]
        assert all(i["status"] == "pending" for i in items)
        # The inviter is named.
        assert all(i["inviter_login"] == "alice" for i in items)


def test_revoke_pending_invitation_works_already_accepted_refuses(app_with_fake_gitea):
    """Revoke flips a pending invitation to 'revoked'. An already-
    accepted invitation refuses 409 — accepted membership is removed
    via a different (future) surface; the v0.16.0 revoke only lifts
    the pending link."""
    from fastapi.testclient import TestClient
    from app import db

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        _reset_outbound()
        provision_user_row(user_id=1, login="alice", role="contributor")
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)
        sign_in_as(
            client, user_id=1, gitea_login="alice",
            display_name="Alice", role="contributor",
        )

        r = client.post(
            "/api/rfcs/ohm/invitations",
            json={"invitee_email": "revokee@example.com", "role_in_rfc": "discussant"},
        )
        invitation_id = r.json()["id"]

        r = client.post(f"/api/rfcs/ohm/invitations/{invitation_id}/revoke")
        assert r.status_code == 200
        assert r.json()["status"] == "revoked"

        # Re-revoke refuses 409.
        r2 = client.post(f"/api/rfcs/ohm/invitations/{invitation_id}/revoke")
        assert r2.status_code == 409

        row = db.conn().execute(
            "SELECT status FROM rfc_invitations WHERE id = ?", (invitation_id,),
        ).fetchone()
        assert row["status"] == "revoked"


# ---------------------------------------------------------------------------
# Accept (invitee-side)
# ---------------------------------------------------------------------------


def test_accept_invitation_lands_collaborator_row(app_with_fake_gitea):
    """The end-to-end accept gesture: the invitee signs in, posts the
    token, and an rfc_collaborators row lands at the issued role."""
    from fastapi.testclient import TestClient
    from app import db

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        _reset_outbound()
        provision_user_row(user_id=1, login="alice", role="contributor")
        # provision_user_row sets the email to "<login>@test", so the
        # invitee row we'll create needs the same email shape.
        provision_user_row(user_id=2, login="newbie", role="contributor")
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)

        # alice (owner) invites newbie@test.
        sign_in_as(
            client, user_id=1, gitea_login="alice",
            display_name="Alice", role="contributor",
        )
        r = client.post(
            "/api/rfcs/ohm/invitations",
            json={"invitee_email": "newbie@test", "role_in_rfc": "contributor"},
        )
        assert r.status_code == 200, r.text
        token = r.json()["token"]

        # Switch to newbie, accept.
        sign_in_as(
            client, user_id=2, gitea_login="newbie",
            display_name="Newbie", role="contributor",
            email="newbie@test",
        )
        r = client.post("/api/invitations/accept", json={"token": token})
        assert r.status_code == 200, r.text
        body = r.json()
        assert body["ok"] is True
        assert body["changed"] is True
        assert body["rfc_slug"] == "ohm"
        assert body["role_in_rfc"] == "contributor"

        # Collaborator row landed; invitation flipped.
        collab = db.conn().execute(
            "SELECT role_in_rfc FROM rfc_collaborators WHERE rfc_slug = 'ohm' AND user_id = 2",
        ).fetchone()
        assert collab is not None
        assert collab["role_in_rfc"] == "contributor"

        inv = db.conn().execute(
            "SELECT status, accepted_by_user_id FROM rfc_invitations WHERE token = ?",
            (token,),
        ).fetchone()
        assert inv["status"] == "accepted"
        assert inv["accepted_by_user_id"] == 2


def test_accept_refuses_when_email_does_not_match(app_with_fake_gitea):
    """The accepting user's email must match the invitation's
    invitee_email (case-insensitive)."""
    from fastapi.testclient import TestClient

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        _reset_outbound()
        provision_user_row(user_id=1, login="alice", role="contributor")
        provision_user_row(user_id=2, login="mallory", role="contributor")
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)

        sign_in_as(client, user_id=1, gitea_login="alice",
                   display_name="Alice", role="contributor")
        r = client.post(
            "/api/rfcs/ohm/invitations",
            json={"invitee_email": "intended@example.com", "role_in_rfc": "discussant"},
        )
        token = r.json()["token"]

        # mallory's email is "mallory@test", not "intended@example.com".
        sign_in_as(client, user_id=2, gitea_login="mallory",
                   display_name="Mallory", role="contributor",
                   email="mallory@test")
        r = client.post("/api/invitations/accept", json={"token": token})
        assert r.status_code == 403


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

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        _reset_outbound()
        provision_user_row(user_id=1, login="alice", role="contributor")
        provision_user_row(user_id=2, login="newbie", role="contributor")
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)

        sign_in_as(client, user_id=1, gitea_login="alice",
                   display_name="Alice", role="contributor")
        r = client.post(
            "/api/rfcs/ohm/invitations",
            json={"invitee_email": "newbie@test", "role_in_rfc": "discussant"},
        )
        invitation_id = r.json()["id"]
        token = r.json()["token"]

        client.post(f"/api/rfcs/ohm/invitations/{invitation_id}/revoke")

        sign_in_as(client, user_id=2, gitea_login="newbie",
                   display_name="Newbie", role="contributor",
                   email="newbie@test")
        r = client.post("/api/invitations/accept", json={"token": token})
        assert r.status_code == 409


def test_accept_refuses_expired_invitation(app_with_fake_gitea):
    """An invitation past its `expires_at` is refused 409 even if
    the row's column status is still 'pending'. We backdate the
    expires_at directly to model the elapsed-window state."""
    from fastapi.testclient import TestClient
    from app import db

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        _reset_outbound()
        provision_user_row(user_id=1, login="alice", role="contributor")
        provision_user_row(user_id=2, login="newbie", role="contributor")
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)

        sign_in_as(client, user_id=1, gitea_login="alice",
                   display_name="Alice", role="contributor")
        r = client.post(
            "/api/rfcs/ohm/invitations",
            json={"invitee_email": "newbie@test", "role_in_rfc": "discussant"},
        )
        token = r.json()["token"]
        invitation_id = r.json()["id"]

        # Backdate.
        db.conn().execute(
            "UPDATE rfc_invitations SET expires_at = datetime('now', '-1 day') WHERE id = ?",
            (invitation_id,),
        )

        sign_in_as(client, user_id=2, gitea_login="newbie",
                   display_name="Newbie", role="contributor",
                   email="newbie@test")
        r = client.post("/api/invitations/accept", json={"token": token})
        assert r.status_code == 409


def test_accept_is_idempotent_on_re_accept(app_with_fake_gitea):
    """Re-accepting the same already-accepted invitation reads as a
    200 no-op with `changed=false`. The collaborator row is unchanged."""
    from fastapi.testclient import TestClient
    from app import db

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        _reset_outbound()
        provision_user_row(user_id=1, login="alice", role="contributor")
        provision_user_row(user_id=2, login="newbie", role="contributor")
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)

        sign_in_as(client, user_id=1, gitea_login="alice",
                   display_name="Alice", role="contributor")
        r = client.post(
            "/api/rfcs/ohm/invitations",
            json={"invitee_email": "newbie@test", "role_in_rfc": "discussant"},
        )
        token = r.json()["token"]

        sign_in_as(client, user_id=2, gitea_login="newbie",
                   display_name="Newbie", role="contributor",
                   email="newbie@test")
        r1 = client.post("/api/invitations/accept", json={"token": token})
        assert r1.status_code == 200
        assert r1.json()["changed"] is True

        r2 = client.post("/api/invitations/accept", json={"token": token})
        assert r2.status_code == 200
        assert r2.json()["changed"] is False

        # Still exactly one collaborator row.
        rows = db.conn().execute(
            "SELECT COUNT(*) AS n FROM rfc_collaborators WHERE rfc_slug = 'ohm' AND user_id = 2"
        ).fetchone()
        assert rows["n"] == 1


# ---------------------------------------------------------------------------
# Discussion-write gate enforcement
# ---------------------------------------------------------------------------


def test_non_invited_user_cannot_post_to_discussion(app_with_fake_gitea):
    """v0.16.0 narrows the discussion-write gate: a platform-granted
    user with no per-RFC role gets 403 when posting to the
    discussion. (v0.6.0 left the gate at require_contributor only;
    item #12 layers can_discuss_rfc on top.)"""
    from fastapi.testclient import TestClient

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        provision_user_row(user_id=1, login="alice", role="contributor")
        provision_user_row(user_id=2, login="bob", role="contributor")
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)

        # bob is platform-granted but not in OHM's owners list and has
        # no invitation. The thread-create surface refuses 403.
        sign_in_as(client, user_id=2, gitea_login="bob",
                   display_name="Bob", role="contributor")
        r = client.post(
            "/api/rfcs/ohm/discussion/threads",
            json={"label": "Question", "message": "Should I be allowed?"},
        )
        assert r.status_code == 403


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

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        _reset_outbound()
        provision_user_row(user_id=1, login="alice", role="contributor")
        provision_user_row(user_id=2, login="newbie", role="contributor")
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)

        # alice invites newbie as a discussant.
        sign_in_as(client, user_id=1, gitea_login="alice",
                   display_name="Alice", role="contributor")
        r = client.post(
            "/api/rfcs/ohm/invitations",
            json={"invitee_email": "newbie@test", "role_in_rfc": "discussant"},
        )
        token = r.json()["token"]

        # newbie accepts.
        sign_in_as(client, user_id=2, gitea_login="newbie",
                   display_name="Newbie", role="contributor",
                   email="newbie@test")
        client.post("/api/invitations/accept", json={"token": token})

        # newbie can now post to the discussion.
        r = client.post(
            "/api/rfcs/ohm/discussion/threads",
            json={"label": "Question", "message": "Now I can speak."},
        )
        assert r.status_code == 200, r.text


def test_contributor_role_includes_discussion(app_with_fake_gitea):
    """A 'contributor' per-RFC role strictly includes discussion
    permission — accepting a contributor invitation admits the user
    to the discussion endpoint too."""
    from fastapi.testclient import TestClient

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        _reset_outbound()
        provision_user_row(user_id=1, login="alice", role="contributor")
        provision_user_row(user_id=2, login="newbie", role="contributor")
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)

        sign_in_as(client, user_id=1, gitea_login="alice",
                   display_name="Alice", role="contributor")
        r = client.post(
            "/api/rfcs/ohm/invitations",
            json={"invitee_email": "newbie@test", "role_in_rfc": "contributor"},
        )
        token = r.json()["token"]

        sign_in_as(client, user_id=2, gitea_login="newbie",
                   display_name="Newbie", role="contributor",
                   email="newbie@test")
        client.post("/api/invitations/accept", json={"token": token})

        r = client.post(
            "/api/rfcs/ohm/discussion/threads",
            json={"label": "Q", "message": "Hello."},
        )
        assert r.status_code == 200


def test_platform_admin_can_post_to_discussion_without_invitation(app_with_fake_gitea):
    """Per §6.1 / item #12's permission shape: platform admins/owners
    can write to any RFC's discussion regardless of per-RFC
    membership."""
    from fastapi.testclient import TestClient

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        provision_user_row(user_id=1, login="alice", role="contributor")
        provision_user_row(user_id=99, login="adminzero", role="admin")
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)

        sign_in_as(client, user_id=99, gitea_login="adminzero",
                   display_name="Admin Zero", role="admin")
        r = client.post(
            "/api/rfcs/ohm/discussion/threads",
            json={"label": "Admin chime", "message": "Drive-by from admin."},
        )
        assert r.status_code == 200


def test_rfc_owner_can_post_to_discussion(app_with_fake_gitea):
    """The frontmatter RFC owner is admitted by virtue of being on
    the owners list — they don't need to invite themselves."""
    from fastapi.testclient import TestClient

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        provision_user_row(user_id=1, login="alice", role="contributor")
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)
        sign_in_as(client, user_id=1, gitea_login="alice",
                   display_name="Alice", role="contributor")
        r = client.post(
            "/api/rfcs/ohm/discussion/threads",
            json={"label": "Owner thought", "message": "Kicking off the conversation."},
        )
        assert r.status_code == 200


# ---------------------------------------------------------------------------
# Admin-page hook (additive on /api/admin/users)
# ---------------------------------------------------------------------------


def test_admin_users_listing_surfaces_per_rfc_invitations(app_with_fake_gitea):
    """v0.16.0 hook into the v0.9.0 admin user-management surface:
    each user row carries an `rfc_invitations` array listing the
    per-RFC roles they hold. Empty array for users without any."""
    from fastapi.testclient import TestClient

    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        _reset_outbound()
        provision_user_row(user_id=1, login="alice", role="contributor")
        provision_user_row(user_id=2, login="newbie", role="contributor")
        provision_user_row(user_id=99, login="adminzero", role="admin")
        seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY)

        # alice invites newbie; newbie accepts.
        sign_in_as(client, user_id=1, gitea_login="alice",
                   display_name="Alice", role="contributor")
        r = client.post(
            "/api/rfcs/ohm/invitations",
            json={"invitee_email": "newbie@test", "role_in_rfc": "contributor"},
        )
        token = r.json()["token"]

        sign_in_as(client, user_id=2, gitea_login="newbie",
                   display_name="Newbie", role="contributor",
                   email="newbie@test")
        client.post("/api/invitations/accept", json={"token": token})

        # Admin lists.
        sign_in_as(client, user_id=99, gitea_login="adminzero",
                   display_name="Admin Zero", role="admin")
        r = client.get("/api/admin/users")
        assert r.status_code == 200
        items = r.json()["items"]
        newbie_row = next(i for i in items if i["gitea_login"] == "newbie")
        assert isinstance(newbie_row["rfc_invitations"], list)
        assert len(newbie_row["rfc_invitations"]) == 1
        invite = newbie_row["rfc_invitations"][0]
        assert invite["rfc_slug"] == "ohm"
        assert invite["role_in_rfc"] == "contributor"
        assert invite["inviter_login"] == "alice"

        # Users with no invitations carry an empty array, not null.
        alice_row = next(i for i in items if i["gitea_login"] == "alice")
        assert alice_row["rfc_invitations"] == []
