"""The running list's archive (R2 task T9): a fourth state of rian's, a filter that keeps it
reachable, and the recorded-write shape every state change shares.

Why: closed items left the page after ten days with no way back but the generated registers,
and there was no way to put an open item away for good without pretending it was done. The
merge is pure (no database); the route tests run in-process on the accounts suite's SQLite and
pin what `ACCOUNTS.md` "Recorded writes" promises the oversight lane: `POST /api/items/{id}/
<state>` with an optional `text`, the actor from the session, `items.act` required.
"""

from datetime import UTC, date, datetime

import pytest
from sqlalchemy import select

from app.models import AccountLevel, Notification, OwnerItemState
from app.services import accounts
from app.services import items as svc
from tests import _accounts as T
from tests.kit import _env

TODAY = date(2026, 9, 12)


def item(id_: str, kind: str = "issue", **kw) -> dict:
    return {"id": id_, "kind": kind, "title": id_, "detail": "", "created": "2026-09-01", "by": "x", **kw}


class TestMerge:
    def test_archived_is_never_visible_by_default_and_the_filter_finds_it(self):
        states = {
            "a": {"status": "archived", "decision": "parked", "acted_by": "rian", "acted_at": datetime(2026, 9, 1, tzinfo=UTC)},
            "b": {"status": "done", "decision": None, "acted_by": "rian", "acted_at": datetime(2026, 9, 11, tzinfo=UTC)},
            "c": {"status": "done", "decision": None, "acted_by": "rian", "acted_at": datetime(2026, 8, 1, tzinfo=UTC)},
        }
        merged = svc.merge([item("a"), item("b"), item("c"), item("d")], states, today=TODAY)
        by = {i["id"]: i for i in merged}
        assert by["a"]["status"] == "archived" and by["a"]["visible"] is False and by["a"]["closed_on"] == "2026-09-01"
        assert by["b"]["visible"] is True and by["c"]["visible"] is False  # closed 42 days ago: gone from the default view
        assert [i["id"] for i in svc.shown(merged, "recent")] == ["d", "b"]
        assert [i["id"] for i in svc.shown(merged, "archived")] == ["a"]
        assert {i["id"] for i in svc.shown(merged, "all")} == {"a", "b", "c", "d"}
        c = svc.counts(merged)
        assert c["archived"] == 1 and c["closed"] == 1 and c["issue"] == 1
        assert svc.STATES == ("open", "done", "dismissed", "archived") and svc.SHOWS == ("recent", "archived", "all")


@pytest.fixture
def world(monkeypatch):
    T.fresh(monkeypatch)
    T.person("rian", display_name="Rian")
    with _env.TestSessionLocal() as db:
        db.add(AccountLevel(name="admin", permissions=list(accounts.SEED_LEVELS["admin"]["permissions"]), assignable=[]))
        db.commit()
    T.person("adam", level="admin", display_name="Adam")
    monkeypatch.setattr(svc, "load_items", lambda: [item("decide-x", "decide"), item("issue-y", "issue", priority="P2")])
    yield


class TestRoutes:
    def test_archive_and_reopen_are_recorded_writes_from_the_session(self, world):
        c = T.client()
        T.as_user(c, "rian")
        r = c.post("/api/items/issue-y/archive", json={"text": "parked until Cannes", "acted_by": "Someone Else"})
        assert r.status_code == 200, r.text
        body = r.json()
        assert body["status"] == "archived" and body["decision"] == "parked until Cannes" and body["acted_by"] == "Rian"
        with _env.TestSessionLocal() as db:
            row = db.get(OwnerItemState, "issue-y")
            assert row.status == "archived" and row.acted_by_id == 1 and row.acted_at is not None
        # The default read hides it; the filter shows it; a bad filter is refused.
        assert [i["id"] for i in c.get("/api/items").json()["items"]] == ["decide-x"]
        assert [i["id"] for i in c.get("/api/items?show=archived").json()["items"]] == ["issue-y"]
        assert {i["id"] for i in c.get("/api/items?show=all").json()["items"]} == {"decide-x", "issue-y"}
        assert c.get("/api/items").json()["counts"]["archived"] == 1
        assert c.get("/api/items?show=bogus").status_code == 422
        # Reopen brings it back, and the note is cleared for an open item.
        r = c.post("/api/items/issue-y/reopen", json={})
        assert r.json()["status"] == "open" and r.json()["decision"] is None
        assert {i["id"] for i in c.get("/api/items").json()["items"]} == {"issue-y", "decide-x"}

    def test_archiving_a_decision_is_not_deciding_it(self, world):
        c = T.client()
        T.as_user(c, "rian")
        assert c.post("/api/items/decide-x/archive", json={"text": "overtaken"}).status_code == 200
        with _env.TestSessionLocal() as db:
            assert db.scalars(select(Notification)).all() == []  # no decision event: nothing was decided
        assert c.get("/api/items?show=archived").json()["items"][0]["kind"] == "decide"

    def test_a_client_cannot_archive_and_is_not_told_the_list_exists(self, world):
        c = T.client()
        T.as_user(c, "adam")
        r = c.post("/api/items/issue-y/archive", json={"text": "x"})
        assert r.status_code == 403 and r.json()["detail"]["error_code"] == "FORBIDDEN"
        assert c.get("/api/items?show=archived").status_code == 403
        assert c.post("/api/items/no-such/archive", json={}).status_code == 403  # refused before the lookup
