"""Slice S3 — scope-role enforcement + collection-grain visibility (@S3).

The acceptance gate for S3 is "every Part C.1 scenario passes" (the design doc
docs/design/2026-06-05-three-tier-projects-collections.md, §C.1, tagged @S3) plus
the operator's S3 visibility requirements (a collection settable public/hidden;
hidden = visible to project/global scope contributors but not the public; a
collection's visibility may be set only as strict or stricter than its project).

The §B.2 resolver folds four layers — global → project → collection → per-entry —
most-permissively, with no negative override. The scenarios below are exercised
directly against the resolver/gate helpers, and the visibility ones additionally
through the HTTP surface.

Background (C.1): a deployment with a project "ohm" owning collections "model"
(document) and "features" (bdd); a second project "acme" with collection
"specs". Plus a hidden ("gated") collection "secret" under ohm for the
hidden-from-public scenarios.
"""
from __future__ import annotations

from fastapi.testclient import TestClient

from test_propose_vertical import (  # noqa: F401 — fixtures land via import
    app_with_fake_gitea,
    provision_user_row,
    sign_in_as,
    tmp_env,
)


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


def _su(user_id: int, login: str, role: str = "contributor", *, state: str = "granted"):
    from app import auth

    return auth.SessionUser(
        user_id=user_id, gitea_id=user_id, gitea_login=login,
        display_name=login.capitalize(), email=f"{login}@test", avatar_url="",
        role=role, permission_state=state,
    )


def _project(pid: str, visibility: str = "public", content_repo: str = "meta") -> None:
    from app import db

    db.conn().execute(
        "INSERT OR REPLACE INTO projects (id, name, content_repo, visibility, updated_at) "
        "VALUES (?, ?, ?, ?, datetime('now'))",
        (pid, pid.capitalize(), content_repo, visibility),
    )


def _collection(cid: str, project_id: str, *, ctype: str = "document",
                visibility: str = "public", subfolder: str | None = None) -> None:
    from app import db

    db.conn().execute(
        "INSERT OR REPLACE INTO collections "
        "(id, project_id, type, subfolder, initial_state, visibility, name, created_at, updated_at) "
        "VALUES (?, ?, ?, ?, 'super-draft', ?, ?, datetime('now'), datetime('now'))",
        (cid, project_id, ctype, subfolder if subfolder is not None else cid,
         visibility, cid.capitalize()),
    )


def _grant(scope_type: str, scope_id: str, user_id: int, role: str) -> None:
    from app import db

    db.conn().execute(
        "INSERT OR REPLACE INTO memberships (scope_type, scope_id, user_id, role) "
        "VALUES (?, ?, ?, ?)",
        (scope_type, scope_id, user_id, role),
    )


def _seed_world() -> None:
    """The C.1 background plus a hidden collection and a second project."""
    _project("ohm", "public")
    _collection("ohm", "ohm", subfolder="")          # ohm's structural default
    _collection("model", "ohm", ctype="document")
    _collection("features", "ohm", ctype="bdd")
    _collection("secret", "ohm", visibility="gated")  # hidden from public
    _project("acme", "public")
    _collection("specs", "acme", ctype="specification")
    # the cast
    for uid, login in [(1, "ada"), (2, "ben"), (3, "cleo"), (4, "dan"),
                       (5, "eve"), (6, "fay"), (7, "gil"), (8, "hana")]:
        provision_user_row(user_id=uid, login=login, role="contributor")
    _grant("collection", "model", 1, "contributor")   # ada
    _grant("project", "ohm", 2, "contributor")         # ben
    _grant("global", "*", 3, "contributor")            # cleo
    _grant("collection", "features", 4, "owner")       # dan
    _grant("project", "ohm", 5, "owner")               # eve
    _grant("collection", "model", 6, "contributor")    # fay (+ project owner below)
    _grant("project", "ohm", 6, "owner")               # fay
    _grant("project", "ohm", 7, "contributor")         # gil
    # hana (8): no grant.


# ---------------------------------------------------------------------------
# C.1 — role usage: inheritance and the most-permissive union
# ---------------------------------------------------------------------------


def test_c1_1_collection_contributor_proposes_only_in_that_collection(app_with_fake_gitea):
    from app import auth
    app, _ = app_with_fake_gitea
    with TestClient(app):
        _seed_world()
        ada = _su(1, "ada")
        # may submit a new entry in ohm/model
        assert auth.can_contribute_in_collection(ada, "model") is True
        # ohm/features is read-only and propose is not offered
        assert auth.can_read_collection(ada, "features") is True
        assert auth.can_contribute_in_collection(ada, "features") is False


def test_c1_2_project_contributor_proposes_in_every_collection(app_with_fake_gitea):
    from app import auth
    app, _ = app_with_fake_gitea
    with TestClient(app):
        _seed_world()
        ben = _su(2, "ben")
        assert auth.can_contribute_in_collection(ben, "model") is True
        assert auth.can_contribute_in_collection(ben, "features") is True
        # a collection added later is writable with no new grant
        _collection("roadmap", "ohm", ctype="document")
        assert auth.can_contribute_in_collection(ben, "roadmap") is True


def test_c1_3_global_contributor_proposes_everywhere(app_with_fake_gitea):
    from app import auth
    app, _ = app_with_fake_gitea
    with TestClient(app):
        _seed_world()
        cleo = _su(3, "cleo")
        assert auth.can_contribute_in_collection(cleo, "model") is True
        assert auth.can_contribute_in_collection(cleo, "specs") is True  # acme


def test_c1_4_collection_owner_administers_one_collection_only(app_with_fake_gitea):
    from app import auth
    app, _ = app_with_fake_gitea
    with TestClient(app):
        _seed_world()
        dan = _su(4, "dan")
        # graduate / mark-reviewed / manage membership in ohm/features
        assert auth.is_collection_superuser(dan, "features") is True
        # but not change ohm project settings
        assert auth.is_project_superuser(dan, "ohm") is False
        assert auth.can_create_collection(dan, "ohm") is False
        # and not act on entries in ohm/model
        assert auth.is_collection_superuser(dan, "model") is False
        assert auth.can_contribute_in_collection(dan, "model") is False


def test_c1_5_project_owner_administers_all_collections_and_creates_more(app_with_fake_gitea):
    from app import auth
    app, _ = app_with_fake_gitea
    with TestClient(app):
        _seed_world()
        eve = _su(5, "eve")
        assert auth.is_collection_superuser(eve, "model") is True
        assert auth.is_collection_superuser(eve, "features") is True
        assert auth.is_project_superuser(eve, "ohm") is True       # edit project settings
        assert auth.can_create_collection(eve, "ohm") is True      # create a new collection


def test_c1_6_most_permissive_union_higher_grant_wins(app_with_fake_gitea):
    from app import auth
    app, _ = app_with_fake_gitea
    with TestClient(app):
        _seed_world()
        fay = _su(6, "fay")
        # collection RFC Contributor at model + project Owner at ohm → acts as Owner in model
        assert auth.effective_scope_role(fay, "model") == "owner"
        assert auth.is_collection_superuser(fay, "model") is True


def test_c1_7_no_negative_override(app_with_fake_gitea):
    from app import auth, db
    app, _ = app_with_fake_gitea
    with TestClient(app):
        _seed_world()
        gil = _su(7, "gil")
        # gil can propose in ohm/model via the project grant…
        assert auth.can_contribute_in_collection(gil, "model") is True
        # …and there is no collection-scope row to remove at model while keeping
        # the project grant (a child cannot subtract a parent grant).
        row = db.conn().execute(
            "SELECT 1 FROM memberships WHERE user_id = 7 AND scope_type = 'collection' AND scope_id = 'model'"
        ).fetchone()
        assert row is None


def test_c1_8_granted_account_no_role_sees_only_public(app_with_fake_gitea):
    from app import auth
    app, _ = app_with_fake_gitea
    with TestClient(app):
        _seed_world()
        hana = _su(8, "hana")
        # may read public collections
        assert auth.can_read_collection(hana, "model") is True
        # but is not offered the propose action anywhere (no scope role; the
        # grandfathered baseline covers only the N=1 `default` collection)
        assert auth.can_contribute_in_collection(hana, "model") is False
        assert auth.can_contribute_in_collection(hana, "features") is False
        assert auth.can_contribute_in_collection(hana, "specs") is False
        # gated (hidden) collections do not appear for her
        assert auth.can_read_collection(hana, "secret") is False


# ---------------------------------------------------------------------------
# Collection-grain visibility — the operator's S3 requirements
# ---------------------------------------------------------------------------


def test_hidden_collection_invisible_to_public_visible_to_scope_holder(app_with_fake_gitea):
    """A gated collection is omitted from the directory and 404s on read for the
    public, yet is listed + readable for a scope-role contributor."""
    app, _ = app_with_fake_gitea
    with TestClient(app) as client:
        _seed_world()
        # anonymous: the gated 'secret' collection is not listed, and 404s.
        listed = {c["id"] for c in client.get("/api/projects/ohm/collections").json()["items"]}
        assert "secret" not in listed
        assert "model" in listed  # public ones still listed
        assert client.get("/api/projects/ohm/collections/secret").status_code == 404
        assert client.get("/api/projects/ohm/collections/secret/rfcs").status_code == 404

        # ben (project contributor) sees and reads it.
        sign_in_as(client, user_id=2, gitea_login="ben", display_name="Ben", role="contributor")
        listed2 = {c["id"] for c in client.get("/api/projects/ohm/collections").json()["items"]}
        assert "secret" in listed2
        assert client.get("/api/projects/ohm/collections/secret").status_code == 200
        assert client.get("/api/projects/ohm/collections/secret/rfcs").status_code == 200

        # hana (granted, no role) is back to the public view.
        sign_in_as(client, user_id=8, gitea_login="hana", display_name="Hana", role="contributor")
        listed3 = {c["id"] for c in client.get("/api/projects/ohm/collections").json()["items"]}
        assert "secret" not in listed3
        assert client.get("/api/projects/ohm/collections/secret").status_code == 404


def test_collection_visibility_strictness_validated_at_create(app_with_fake_gitea):
    """A collection may be created only as strict or stricter than its project;
    a looser request is refused (422). On a gated project, a 'public' collection
    is rejected."""
    app, _ = app_with_fake_gitea
    with TestClient(app) as client:
        _project("locked", "gated")
        _collection("locked", "locked", subfolder="", visibility="gated")
        # eve is a deployment owner here to clear the create-authority gate;
        # the strictness check fires regardless.
        provision_user_row(user_id=9, login="root", role="owner")
        sign_in_as(client, user_id=9, gitea_login="root", display_name="Root", role="owner")
        r = client.post("/api/projects/locked/collections", json={
            "collection_id": "wideopen", "type": "document", "visibility": "public",
        })
        assert r.status_code == 422, r.text
        assert "looser" in r.json()["detail"]


def test_create_collection_allowed_for_project_owner_not_plain_contributor(app_with_fake_gitea):
    """§B.1: a project-scope Owner may create a collection; a plain granted
    contributor with no project/global grant may not (403)."""
    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        _seed_world()
        # gil is only a project *contributor* on ohm — per §B.1 a project-scope
        # contributor CAN create collections (the project-level create
        # affordance). A collection-scope grant cannot.
        sign_in_as(client, user_id=7, gitea_login="gil", display_name="Gil", role="contributor")
        r_ok = client.post("/api/projects/ohm/collections", json={
            "collection_id": "fromgil", "type": "document", "visibility": "public",
        })
        assert r_ok.status_code in (200, 502), r_ok.text  # past the authz gate

        # ada holds only a *collection*-scope grant (at model) — no create right.
        sign_in_as(client, user_id=1, gitea_login="ada", display_name="Ada", role="contributor")
        r_no = client.post("/api/projects/ohm/collections", json={
            "collection_id": "fromada", "type": "document", "visibility": "public",
        })
        assert r_no.status_code == 403, r_no.text
