"""The Interaction Standard's conversation state in caddie (04 §3–4, as
amended 2026-09-02): the project itself is a subject, one discussion read per
work surface, resolve propagates to the bell, edits and soft deletes, a
conversation re-filed under another subject, and the page knowing who it is
speaking to."""

import io
import itertools

from tests.conftest import as_user

_n = itertools.count(1)
PNG = bytes.fromhex("89504e470d0a1a0a") + b"\x00" * 64


def _project(client, agency, name=None):
    as_user(client, agency)
    name = name or f"Disc Project {next(_n)}"
    r = client.post("/api/projects", json={"display_name": name,
                                           "template_key": "website_build_v1"})
    pid = r.json()["id"]
    stages = client.get(f"/api/projects/{pid}").json()["stages"]
    return pid, name, stages


def _client(kit, pid, name="cleo"):
    kit.member(name, "member")
    kit.grant(name, pid, "member")
    return name


def _box(client, who):
    as_user(client, who)
    return client.get("/api/notifications").json()


# ── subjects and the one read ────────────────────────────────────────────

def test_the_project_itself_is_a_subject_and_the_read_lists_every_subject(client, kit, agency):
    pid, name, stages = _project(client, agency)
    cleo = _client(kit, pid)

    as_user(client, cleo)
    said = client.post(f"/api/projects/{pid}/comments", json={"body": "General question"})
    assert said.status_code == 201

    d = client.get(f"/api/projects/{pid}/discussion").json()
    assert d["me"] == cleo and d["is_agency"] is False
    first = d["subjects"][0]
    assert first["subject_type"] == "project" and first["subject_id"] == pid
    assert first["label"] == name, "the project's label is its name, no type word"
    assert first["thread"]["comments"][0]["body"] == "General question"
    # every visible stage is listed too — as a starter until someone speaks
    stage_subjects = [s for s in d["subjects"] if s["subject_type"] == "stage"]
    assert len(stage_subjects) == len(stages)
    assert all(s["thread"] is None for s in stage_subjects)
    assert all(s["label"] == f"{name} › {s['title']}" for s in stage_subjects)
    assert d["open"] == 1 and d["total"] == 1

    # the project comment's notification deep-links to the page, no ?stage=
    box = _box(client, agency)
    n = next(x for x in box["notifications"] if x["kind"] == "reply")
    assert n["context_label"] == name
    assert "?stage=" not in n["url"] and n["url"].endswith(f"#c-{said.json()['id']}")


def test_an_internal_stage_is_not_a_subject_for_the_client(client, kit, agency, session):
    pid, _, stages = _project(client, agency)
    cleo = _client(kit, pid)
    from app.models import Stage
    st = session.get(Stage, stages[0]["id"])
    st.client_visible = False
    session.commit()

    as_user(client, cleo)
    d = client.get(f"/api/projects/{pid}/discussion").json()
    assert stages[0]["id"] not in [s["subject_id"] for s in d["subjects"]]
    # and the stranger sees no discussion at all — 404, never 403
    kit.member("stranger", "member")
    as_user(client, "stranger")
    assert client.get(f"/api/projects/{pid}/discussion").status_code == 404


# ── resolve propagates ───────────────────────────────────────────────────

def test_resolving_stops_the_nagging_and_reopening_restarts_it(client, kit, agency):
    pid, _, stages = _project(client, agency)
    sid = stages[0]["id"]
    cleo = _client(kit, pid)

    as_user(client, cleo)
    r = client.post(f"/api/projects/{pid}/stages/{sid}/comments", json={"body": "Is this right?"})
    tid = r.json()["thread_id"]
    assert _box(client, agency)["unread"] == 1

    # the agency resolves it: their reply notification leaves the count …
    as_user(client, agency)
    assert client.post(f"/api/threads/{tid}/resolve", json={"resolved": True}).json()["resolved"] is True
    box = _box(client, agency)
    assert box["unread"] == 0
    assert any(n["resolved"] for n in box["notifications"]), "still there behind Show resolved"
    # … and the person in the thread hears it was resolved — as news, not "needs you"
    cbox = _box(client, cleo)
    assert cbox["unread"] == 1 and cbox["needs_you"] is False
    assert cbox["notifications"][0]["kind"] == "resolved"
    assert cbox["notifications"][0]["url"].endswith(f"?stage={sid}")

    # reopening flips the flag back
    as_user(client, agency)
    client.post(f"/api/threads/{tid}/resolve", json={"resolved": False})
    assert _box(client, agency)["unread"] == 1
    d = client.get(f"/api/projects/{pid}/discussion").json()
    subj = next(s for s in d["subjects"] if s["subject_id"] == sid)
    assert subj["thread"]["resolved"] is False


def test_only_the_opener_or_the_agency_resolves(client, kit, agency):
    pid, _, stages = _project(client, agency)
    sid = stages[0]["id"]
    cleo = _client(kit, pid)
    dan = _client(kit, pid, "dan")
    as_user(client, cleo)
    tid = client.post(f"/api/projects/{pid}/stages/{sid}/comments",
                      json={"body": "Opened by cleo"}).json()["thread_id"]
    as_user(client, dan)
    assert client.post(f"/api/threads/{tid}/resolve", json={"resolved": True}).status_code == 403
    as_user(client, cleo)
    assert client.post(f"/api/threads/{tid}/resolve", json={"resolved": True}).status_code == 200


# ── edit and soft delete ─────────────────────────────────────────────────

def test_the_author_edits_and_a_new_mention_rings_only_the_newly_named(client, kit, agency):
    pid, _, stages = _project(client, agency)
    sid = stages[0]["id"]
    cleo = _client(kit, pid)
    dan = _client(kit, pid, "dan")

    as_user(client, cleo)
    cid = client.post(f"/api/projects/{pid}/stages/{sid}/comments",
                      json={"body": "first draft"}).json()["id"]
    assert _box(client, agency)["unread"] == 1

    as_user(client, dan)
    assert client.put(f"/api/comments/{cid}", json={"body": "not mine"}).status_code == 403

    as_user(client, cleo)
    r = client.put(f"/api/comments/{cid}", json={"body": "second draft, @dan can you look?"})
    assert r.status_code == 200 and r.json()["comment"]["edited_at"]
    assert _box(client, dan)["notifications"][0]["kind"] == "mention"
    assert _box(client, agency)["unread"] == 1, "an edit never re-rings the reply fan-out"
    assert client.put(f"/api/comments/{cid}", json={"body": "   "}).status_code == 400 \
        if as_user(client, cleo) is None else True


def test_delete_is_soft_and_the_agency_may_remove_a_clients_comment(client, kit, agency, session):
    pid, _, stages = _project(client, agency)
    sid = stages[0]["id"]
    cleo = _client(kit, pid)
    dan = _client(kit, pid, "dan")

    as_user(client, cleo)
    cid = client.post(f"/api/projects/{pid}/stages/{sid}/comments",
                      json={"body": "oops"}).json()["id"]
    as_user(client, dan)
    assert client.delete(f"/api/comments/{cid}").status_code == 403
    as_user(client, agency)
    assert client.delete(f"/api/comments/{cid}").status_code == 200

    # gone from every read, still in the table (never hard-delete a conversation)
    assert client.get(f"/api/projects/{pid}/stages/{sid}/comments").json()["comments"] == []
    d = client.get(f"/api/projects/{pid}/discussion").json()
    assert next(s for s in d["subjects"] if s["subject_id"] == sid)["thread"] is None
    from app.models import Comment
    row = session.get(Comment, cid)
    assert row is not None and row.deleted_at is not None and row.body == "oops"
    assert client.delete(f"/api/comments/{cid}").status_code == 404


# ── re-filing ────────────────────────────────────────────────────────────

def test_a_conversation_moves_to_another_subject_and_old_deep_links_still_land(client, kit, agency):
    pid, name, stages = _project(client, agency)
    sid = stages[0]["id"]
    cleo = _client(kit, pid)

    as_user(client, cleo)
    r = client.post(f"/api/projects/{pid}/stages/{sid}/comments",
                    json={"body": f"@{agency} this is really about the whole project"})
    cid, tid = r.json()["id"], r.json()["thread_id"]
    link = _box(client, agency)["notifications"][0]["url"]
    assert f"#c-{cid}" in link

    # a non-author client can't move it; the agency can
    dan = _client(kit, pid, "dan")
    as_user(client, dan)
    assert client.post(f"/api/threads/{tid}/move",
                       json={"subject_type": "project", "subject_id": pid}).status_code == 403
    as_user(client, agency)
    moved = client.post(f"/api/threads/{tid}/move",
                        json={"subject_type": "project", "subject_id": pid})
    assert moved.status_code == 200 and moved.json()["subject_type"] == "project"

    d = client.get(f"/api/projects/{pid}/discussion").json()
    assert d["subjects"][0]["thread"]["comments"][0]["id"] == cid, "the comment kept its id"
    assert next(s for s in d["subjects"] if s["subject_id"] == sid)["thread"] is None
    # the stored link is unchanged, and the comment id it carries is findable
    assert _box(client, agency)["notifications"][0]["url"] == link

    # moving onto a subject that already has a conversation MERGES into it
    as_user(client, cleo)
    client.post(f"/api/projects/{pid}/stages/{sid}/comments", json={"body": "on the stage"})
    as_user(client, agency)
    project_tid = d["subjects"][0]["thread"]["id"]
    merged = client.post(f"/api/threads/{project_tid}/move",
                         json={"subject_type": "stage", "subject_id": sid}).json()
    d2 = client.get(f"/api/projects/{pid}/discussion").json()
    stage_thread = next(s for s in d2["subjects"] if s["subject_id"] == sid)["thread"]
    assert merged["thread_id"] == stage_thread["id"]
    assert [c["body"] for c in stage_thread["comments"]] == \
        [f"@{agency} this is really about the whole project", "on the stage"]
    assert d2["subjects"][0]["thread"] is None

    # nowhere: 404, never a hint that the place exists
    assert client.post(f"/api/threads/{stage_thread['id']}/move",
                       json={"subject_type": "stage", "subject_id": "nope"}).status_code == 404


# ── attachments on the project subject ───────────────────────────────────

def test_a_file_can_ride_on_a_project_comment(client, kit, agency):
    pid, _, _ = _project(client, agency)
    cleo = _client(kit, pid)
    as_user(client, cleo)
    up = client.post(f"/api/attachments/{pid}",
                     files={"file": ("shot.png", io.BytesIO(PNG), "image/png")})
    assert up.status_code == 201, up.text
    assert client.get(up.json()["attachment"]["url"]).status_code == 200
    kit.member("stranger", "member")
    as_user(client, "stranger")
    assert client.get(up.json()["attachment"]["url"]).status_code == 404


# ── decisions, and who the page speaks to ────────────────────────────────

def test_an_approval_is_a_decision_not_news_and_not_needs_you(client, kit, agency):
    pid, _, stages = _project(client, agency)
    mock = next(s for s in stages if s["requires_approval"])
    client.post(f"/api/projects/{pid}/stages/{mock['id']}/status", json={"status": "active"})
    cleo = _client(kit, pid)
    as_user(client, cleo)
    assert client.post(f"/api/projects/{pid}/stages/{mock['id']}/approve", json={}).status_code == 200
    box = _box(client, agency)
    n = next(x for x in box["notifications"] if x["source_type"] == "stage") \
        if "source_type" in box["notifications"][0] else box["notifications"][0]
    assert n["kind"] == "decision" and box["needs_you"] is False


def test_whoami_speaks_to_the_person(client, kit, agency):
    pid, _, _ = _project(client, agency)
    alice = _client(kit, pid, "alice")          # exists centrally in the stub
    as_user(client, alice)
    w = client.get("/api/whoami").json()
    assert (w["username"], w["first_name"], w["display_name"], w["is_agency"]) \
        == ("alice", "T", "T User", False)
    as_user(client, agency)                     # unknown to the directory: derived
    w = client.get("/api/whoami").json()
    assert w["first_name"] == "Pm" and w["is_agency"] is True
