"""Comments + notifications (04 local-only mode, review-modelled): one
conversation stream per item, server-side mentions, turn notifications to the
side whose court the ball landed in, dedupe, never-notify-self, and the bell
contract (unread / needs_you)."""

import pytest

from app.db import get_session_factory
from app.services import interaction as ix
from app.services import items as eng
from app.services import punchlists as pls
from tests.conftest import OWNER, as_user


@pytest.fixture
def session():
    with get_session_factory()() as s:
        yield s
        s.commit()


@pytest.fixture
def world(session, kit):
    """A punchlist with one internal member (tina) and one client (cleo).
    Kit writes use their own connections to the same SQLite file, so this
    fixture commits the ORM session between phases — holding a write
    transaction across a kit call is how "database is locked" happens."""
    kit.bwa.create_level(OWNER, "internal", permissions=[
        "items.act_team", "items.compose", "workflows.author",
        "instances.create", "instances.grant"])
    kit.member("tina", "internal", all_instances=True)
    kit.member("cleo", "member")
    pl = pls.create(session, OWNER, "Interaction world")
    session.commit()                     # release the lock before kit.grant
    kit.grant("cleo", pl.id, "member")
    item = eng.instantiate(session, OWNER, pl.id, template_key="google_ads_access",
                           variables={"who_needs_access": ["a@x.com"]})
    session.commit()
    return pl, item


def _inbox(session, who):
    return ix.inbox(session, who)


def test_flag_message_lands_in_thread_and_notifies_internal(session, world):
    pl, item = world
    eng.act(session, item, actor="cleo", actor_kind="client",
            action="trouble", message="I can't find my customer ID")
    comments = ix.comments_of(session, item)
    assert [c.body for c in comments] == ["I can't find my customer ID"]

    tina = _inbox(session, "tina")
    assert tina["unread"] >= 1 and tina["needs_you"] is True
    kinds = {n["kind"] for n in tina["notifications"]}
    assert "turn" in kinds or "reply" in kinds
    # cleo never hears about her own action
    assert all(n["actor"] != "cleo" for n in _inbox(session, "cleo")["notifications"])


def test_reply_notifies_client_with_deep_link(session, world):
    pl, item = world
    eng.act(session, item, actor="cleo", actor_kind="client",
            action="trouble", message="stuck")
    eng.act(session, item, actor="tina", actor_kind="team",
            action="resolve_flag", message="Top right of any Google Ads page")
    cleo = _inbox(session, "cleo")
    assert cleo["unread"] >= 1
    n = cleo["notifications"][0]
    assert n["path"] == f"/punchlists/{pl.id}?item={item.id}"
    assert n["context_label"] == item.title
    # the conversation is ONE stream: both messages in order
    assert [c.body for c in ix.comments_of(session, item)] == [
        "stuck", "Top right of any Google Ads page"]


def test_mentions_are_parsed_server_side(session, world):
    pl, item = world
    ix.add_comment(session, item, author="tina", author_kind="team",
                   body="@cleo can you check with your IT people? (@nosuchuser ignored)")
    cleo = _inbox(session, "cleo")
    assert any(n["kind"] == "mention" for n in cleo["notifications"])
    assert not any("nosuchuser" in (n["body"] or "") and n["kind"] == "mention"
                   for n in _inbox(session, "tina")["notifications"])


def test_turn_notification_reaches_client_side_only(session, world):
    pl, item = world
    # cleo submits the ID -> ball to team; then tina requests -> ball to client
    eng.act(session, item, actor="cleo", actor_kind="client", action="primary",
            fields={"customer_id": "123-456-7890"})
    eng.act(session, item, actor="tina", actor_kind="team", action="primary")
    cleo = _inbox(session, "cleo")
    turn = [n for n in cleo["notifications"] if n["kind"] == "turn"]
    assert turn and turn[0]["body"] == "Your turn on this one."
    assert cleo["needs_you"] is True


def test_mark_read_clears_the_bell(session, world):
    pl, item = world
    eng.act(session, item, actor="cleo", actor_kind="client",
            action="trouble", message="halp")
    assert _inbox(session, "tina")["unread"] >= 1
    ix.mark_read(session, "tina")
    box = _inbox(session, "tina")
    assert box["unread"] == 0 and box["needs_you"] is False


def test_api_surface(client, kit):
    as_user(client, OWNER)
    r = client.post("/api/punchlists", json={"title": "API ix"})
    pid = r.json()["id"]
    r = client.post(f"/api/punchlists/{pid}/items", json={
        "spec": {"key": "q", "title": "Quick", "start": "a",
                 "steps": [{"id": "a", "owner": "client", "headline": "x",
                            "primary": {"label": "Done", "to": "$done"}}]}})
    iid = r.json()["id"]
    kit.member("cleo", "member")
    kit.grant("cleo", pid, "member")

    as_user(client, "cleo")
    r = client.post(f"/api/items/{iid}/comments", json={"body": "hello @rian"})
    assert r.status_code == 201
    assert r.json()["item"]["messages"][-1]["text"] == "hello @rian"

    as_user(client, OWNER)
    box = client.get("/api/notifications").json()
    assert box["unread"] >= 1
    assert any(n["kind"] == "mention" for n in box["notifications"])
    r = client.post("/api/notifications/read", json={})
    assert r.json()["marked"] >= 1

    # archive drops it from nothing yet but flips state
    r = client.patch(f"/api/punchlists/{pid}", json={"state": "archived"})
    assert r.json()["state"] == "archived"
