"""Slice M1+M3 — the §22 multi-project spine.

Migration 026 introduces the `projects` and `project_members` tables, seeds
the single `default` project (the N=1 case, §22.13), and threads a
`project_id` column onto every slug-bearing table, backfilled to `default`.
M3 retires the META_REPO startup backfill; content_repo now comes from the
registry mirror (projects.yaml in REGISTRY_REPO). These tests prove the
spine lands without disturbing the single-project app.
"""
from __future__ import annotations

from fastapi.testclient import TestClient

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

# §22 three-tier (migration 029): the entry-corpus grain is the collection, so
# the 13 tables migration 028 keyed by project_id re-key to collection_id. The
# remaining tables 026 tagged keep their denormalised project_id (project grain).
COLLECTION_TABLES = [
    "cached_rfcs", "cached_branches", "branch_visibility",
    "branch_contribute_grants", "stars", "pr_seen", "branch_chat_seen",
    "watches", "funder_consents", "rfc_invitations", "rfc_collaborators",
    "proposed_use_cases", "contribution_requests",
]
PROJECT_TAG_TABLES = [
    "cached_prs", "threads", "changes", "notifications", "actions",
    "pr_resolution_branches",
]


def test_default_project_seeded_and_backfilled(app_with_fake_gitea):
    from app import db

    app, _ = app_with_fake_gitea
    with TestClient(app):
        rows = list(db.conn().execute(
            "SELECT id, name, visibility, content_repo FROM projects"
        ))
        assert len(rows) == 1
        row = rows[0]
        assert row["id"] == "default"
        # public preserves the pre-multi-project open-by-default posture.
        assert row["visibility"] == "public"
        # M3: content_repo now comes from the registry mirror (projects.yaml),
        # not the retired META_REPO startup backfill.
        assert row["content_repo"] == "meta"


def test_grain_columns_on_every_slug_table(app_with_fake_gitea):
    from app import db

    app, _ = app_with_fake_gitea
    with TestClient(app):
        # The entry-corpus tables key on collection_id (NOT NULL, 'default').
        for table in COLLECTION_TABLES:
            cols = {r["name"]: r for r in db.conn().execute(f"PRAGMA table_info({table})")}
            assert "collection_id" in cols, f"{table} missing collection_id"
            assert "project_id" not in cols, f"{table} should no longer have project_id"
            col = cols["collection_id"]
            assert col["notnull"] == 1, f"{table}.collection_id should be NOT NULL"
            assert col["dflt_value"] == "'default'", f"{table}.collection_id default"
        # The project-tag tables keep their denormalised project_id.
        for table in PROJECT_TAG_TABLES:
            cols = {r["name"] for r in db.conn().execute(f"PRAGMA table_info({table})")}
            assert "project_id" in cols, f"{table} missing project_id tag"


def test_existing_row_backfills_to_default(app_with_fake_gitea):
    """A row inserted the old way (no collection grain) lands in the default
    collection — the trick that keeps every pre-three-tier INSERT working."""
    from app import db

    app, _ = app_with_fake_gitea
    with TestClient(app):
        db.conn().execute(
            "INSERT INTO cached_rfcs (slug, title, state) VALUES (?, ?, ?)",
            ("human", "Human", "active"),
        )
        got = db.conn().execute(
            "SELECT collection_id FROM cached_rfcs WHERE slug = 'human'"
        ).fetchone()["collection_id"]
        assert got == "default"


def test_memberships_table_shape(app_with_fake_gitea):
    from app import db

    app, _ = app_with_fake_gitea
    with TestClient(app):
        # §22 three-tier: project_members generalised into memberships.
        assert db.conn().execute(
            "SELECT name FROM sqlite_master WHERE type='table' AND name='project_members'"
        ).fetchone() is None
        cols = {r["name"] for r in db.conn().execute("PRAGMA table_info(memberships)")}
        assert {"scope_type", "scope_id", "user_id", "role", "granted_by", "granted_at"} <= cols
        db.conn().execute(
            "INSERT INTO users (id, display_name, role) VALUES (1, 'Ben', 'owner')"
        )
        db.conn().execute(
            "INSERT INTO memberships (scope_type, scope_id, user_id, role) "
            "VALUES ('collection', 'default', 1, 'owner')"
        )
        import sqlite3
        try:
            db.conn().execute(
                "INSERT INTO memberships (scope_type, scope_id, user_id, role) "
                "VALUES ('collection', 'default', 1, 'nonsense')"
            )
            assert False, "CHECK should reject an unknown role"
        except sqlite3.IntegrityError:
            pass


def test_registry_mirror_is_idempotent(app_with_fake_gitea):
    """Re-running the registry mirror is safe — it upserts (overwrites) the
    projects row from projects.yaml each time without raising. M3 retirement
    of seed_default_project: the registry mirror is now the sole authority."""
    import asyncio
    from app import db, registry as registry_mod

    app, _ = app_with_fake_gitea
    with TestClient(app):
        before = db.conn().execute("SELECT COUNT(*) AS n FROM projects").fetchone()["n"]
        cfg = app.state.config
        gitea = app.state.gitea
        asyncio.run(registry_mod.refresh_registry(cfg, gitea))
        after = db.conn().execute("SELECT COUNT(*) AS n FROM projects").fetchone()["n"]
        assert after == before
        row = db.conn().execute(
            "SELECT content_repo FROM projects WHERE id='default'"
        ).fetchone()
        assert row["content_repo"] == "meta"


def test_registry_repo_config_wired(app_with_fake_gitea, monkeypatch):
    from app.config import load_config

    monkeypatch.setenv("REGISTRY_REPO", "wiggleverse-registry")
    assert load_config().registry_repo == "wiggleverse-registry"
    # M3: REGISTRY_REPO is now required — absent raises RuntimeError.
    monkeypatch.delenv("REGISTRY_REPO", raising=False)
    import pytest
    with pytest.raises(RuntimeError, match="REGISTRY_REPO"):
        load_config()
