"""SLICE-5 — bulk metadata edit endpoint (PUC-2, §6.4/§6.5).

Through the real API: contributor+ gating (INV-4), set/add/remove ops,
validation at the write boundary, one commit for N sidecars (D7), and
partial-rejection reporting. Reuses the fake-Gitea harness.
"""
from __future__ import annotations

import asyncio
import json

import yaml
from fastapi.testclient import TestClient

from app import cache, db, gitea as gitea_mod
from app.config import load_config

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

PID = "default"
CID = "default"
BASE = f"/api/projects/{PID}/collections/{CID}"


def _refresh():
    cfg = load_config()
    asyncio.run(cache.refresh_meta_repo(cfg, gitea_mod.Gitea(cfg)))


def _set_fields(schema):
    db.conn().execute(
        "UPDATE collections SET config_json = ? WHERE id = 'default'",
        (json.dumps({"fields": schema}),))


def _seed_legacy(fake, slug, *, state="active", **front):
    fm = {"slug": slug, "title": slug.title(), "state": state, **front}
    body = yaml.safe_dump(fm, sort_keys=False).strip()
    fake.files[("wiggleverse", "meta", "main", f"rfcs/{slug}.md")] = {
        "content": f"---\n{body}\n---\n\nBody.\n", "sha": slug}


def _login_owner(client):
    provision_user_row(user_id=1, login="ben", role="owner")
    sign_in_as(client, user_id=1, gitea_login="ben", display_name="Ben", role="owner")


def _has_sidecar(fake, slug):
    return ("wiggleverse", "meta", "main", f"rfcs/{slug}.meta.yaml") in fake.files


def _sidecar(fake, slug):
    return yaml.safe_load(
        fake.files[("wiggleverse", "meta", "main", f"rfcs/{slug}.meta.yaml")]["content"])


# ---- Task 1: happy path, one commit ----

def test_bulk_set_applies_to_all_and_one_commit(app_with_fake_gitea):
    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        _set_fields({"priority": {"type": "enum", "values": ["P0", "P1", "P2"]}})
        _seed_legacy(fake, "a", priority="P2")
        _seed_legacy(fake, "b", priority="P1")
        _refresh()
        _login_owner(client)
        commits_before = fake.change_files_calls
        r = client.post(f"{BASE}/meta/bulk",
                        json={"slugs": ["a", "b"], "op": "set",
                              "field": "priority", "value": "P0"})
        assert r.status_code == 200, r.text
        body = r.json()
        assert set(body["applied"]) == {"a", "b"}
        assert body["rejected"] == []
        assert body["committed"] is True
        # exactly one ChangeFiles commit covered both entries (D7)
        assert fake.change_files_calls - commits_before == 1
    assert _sidecar(fake, "a")["priority"] == "P0"
    assert _sidecar(fake, "b")["priority"] == "P0"


# ---- Task 2: add/remove tags ----

def test_bulk_add_tag(app_with_fake_gitea):
    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        _set_fields({"tags": {"type": "tags"}})
        _seed_legacy(fake, "a", tags=["x"])
        _seed_legacy(fake, "b", tags=["x", "y"])
        _refresh()
        _login_owner(client)
        r = client.post(f"{BASE}/meta/bulk",
                        json={"slugs": ["a", "b"], "op": "add",
                              "field": "tags", "value": "y"})
        assert r.status_code == 200, r.text
        assert set(r.json()["applied"]) == {"a", "b"}
    # "a" gained y; "b" already had y (no-op write skipped → no sidecar written)
    assert _sidecar(fake, "a")["tags"] == ["x", "y"]
    assert not _has_sidecar(fake, "b")


def test_bulk_remove_tag(app_with_fake_gitea):
    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        _set_fields({"tags": {"type": "tags"}})
        _seed_legacy(fake, "a", tags=["x", "y"])
        _refresh()
        _login_owner(client)
        r = client.post(f"{BASE}/meta/bulk",
                        json={"slugs": ["a"], "op": "remove",
                              "field": "tags", "value": "x"})
        assert r.status_code == 200, r.text
        assert r.json()["applied"] == ["a"]
    assert _sidecar(fake, "a")["tags"] == ["y"]


def test_bulk_set_scalar_on_tags_rejected(app_with_fake_gitea):
    # A scalar `set` onto a tags field must reject, not char-split into a list.
    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        _set_fields({"tags": {"type": "tags"}})
        _seed_legacy(fake, "a", tags=["x"])
        _refresh()
        _login_owner(client)
        r = client.post(f"{BASE}/meta/bulk",
                        json={"slugs": ["a"], "op": "set",
                              "field": "tags", "value": "checkout"})
        assert r.status_code == 200, r.text
        assert r.json()["applied"] == []
        assert len(r.json()["rejected"]) == 1
        assert r.json()["committed"] is False
        assert not _has_sidecar(fake, "a")


def test_bulk_set_list_on_tags_ok(app_with_fake_gitea):
    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        _set_fields({"tags": {"type": "tags"}})
        _seed_legacy(fake, "a", tags=["x"])
        _refresh()
        _login_owner(client)
        r = client.post(f"{BASE}/meta/bulk",
                        json={"slugs": ["a"], "op": "set",
                              "field": "tags", "value": ["x", "y"]})
        assert r.status_code == 200, r.text
        assert r.json()["applied"] == ["a"]
    assert _sidecar(fake, "a")["tags"] == ["x", "y"]


def test_bulk_add_remove_requires_tags_field(app_with_fake_gitea):
    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        _set_fields({"priority": {"type": "enum", "values": ["P0"]}})
        _seed_legacy(fake, "a", priority="P0")
        _refresh()
        _login_owner(client)
        r = client.post(f"{BASE}/meta/bulk",
                        json={"slugs": ["a"], "op": "add",
                              "field": "priority", "value": "z"})
        assert r.status_code == 422, r.text


# ---- Task 3: partial rejection, authz, validation guards ----

def test_bulk_partial_reject_missing_entry(app_with_fake_gitea):
    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        _set_fields({"priority": {"type": "enum", "values": ["P0", "P1"]}})
        _seed_legacy(fake, "a", priority="P1")
        _refresh()
        _login_owner(client)
        r = client.post(f"{BASE}/meta/bulk",
                        json={"slugs": ["a", "ghost"], "op": "set",
                              "field": "priority", "value": "P0"})
        assert r.status_code == 200, r.text
        body = r.json()
        assert body["applied"] == ["a"]
        assert body["rejected"] == [{"slug": "ghost", "reason": "not found"}]
        assert body["committed"] is True
    assert _sidecar(fake, "a")["priority"] == "P0"


def test_bulk_invalid_value_rejects_all_no_commit(app_with_fake_gitea):
    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        _set_fields({"priority": {"type": "enum", "values": ["P0", "P1"]}})
        _seed_legacy(fake, "a", priority="P1")
        _refresh()
        _login_owner(client)
        r = client.post(f"{BASE}/meta/bulk",
                        json={"slugs": ["a"], "op": "set",
                              "field": "priority", "value": "ZZZ"})
        assert r.status_code == 200, r.text
        assert r.json()["applied"] == []
        assert len(r.json()["rejected"]) == 1
        assert r.json()["committed"] is False
        assert not _has_sidecar(fake, "a")


def test_bulk_forbidden_for_anonymous(app_with_fake_gitea):
    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        _set_fields({"priority": {"type": "enum", "values": ["P0"]}})
        _seed_legacy(fake, "a", priority="P0")
        _refresh()
        r = client.post(f"{BASE}/meta/bulk",
                        json={"slugs": ["a"], "op": "set",
                              "field": "priority", "value": "P0"})
        assert r.status_code == 403, r.text


def test_bulk_unknown_field_op_and_empty(app_with_fake_gitea):
    app, fake = app_with_fake_gitea
    with TestClient(app) as client:
        _set_fields({"priority": {"type": "enum", "values": ["P0"]}})
        _seed_legacy(fake, "a", priority="P0")
        _refresh()
        _login_owner(client)
        assert client.post(f"{BASE}/meta/bulk",
                           json={"slugs": ["a"], "op": "set",
                                 "field": "nope", "value": "P0"}).status_code == 422
        assert client.post(f"{BASE}/meta/bulk",
                           json={"slugs": ["a"], "op": "frobnicate",
                                 "field": "priority", "value": "P0"}).status_code == 422
        assert client.post(f"{BASE}/meta/bulk",
                           json={"slugs": [], "op": "set",
                                 "field": "priority", "value": "P0"}).status_code == 422
