"""Dev Mode and the outbox.

easel sends no email today — the gateway's send endpoint is owner/hosting-peer
only with its own recipient allowlist, so easel cannot use it as-is. What exists
here is the decision, the record, and the switch: every message easel WOULD send
is written down with the reason it did not go.
"""

from app.services import outbound
from tests.conftest import as_user
from tests.test_domain import add_screen_option, make_project


def _outbox(client):
    return client.get("/api/outbound").json()["messages"]


def test_dev_mode_is_on_by_default(client, kit):
    """The safe default for a switch whose OFF position mails clients is the one
    that does not."""
    make_project(client, kit)
    assert client.get("/api/outbound/settings").json()["dev_mode"] is True


def test_presenting_writes_an_email_and_holds_it(client, kit):
    iid = make_project(client, kit)
    add_screen_option(client, iid)
    client.post(f"/api/projects/{iid}/send")

    box = _outbox(client)
    assert [m["kind"] for m in box] == ["presented"]
    assert box[0]["recipient"] == "cleo"
    assert box[0]["sent_at"] is None
    assert box[0]["held_reason"] == "dev_mode"
    assert "Concepts are ready" in box[0]["subject"]


def test_turning_dev_mode_off_sends_through_the_account_service(client, kit, bw_calls):
    """Live, a message goes out through the kit's send_mail: the account
    service holds the address and mails it. The outbox stamps it sent."""
    iid = make_project(client, kit)
    add_screen_option(client, iid)
    client.post("/api/outbound/settings", json={"dev_mode": False})
    client.post(f"/api/projects/{iid}/send")

    box = _outbox(client)
    assert box and all(m["sent_at"] for m in box)
    sends = [p for path, p in bw_calls if path == "/app/send-mail"]
    assert len(sends) == len(box)
    assert sends[0]["username"] == box[0]["recipient"]
    assert "Open it here" in sends[0]["text"] and box[0]["url"] in sends[0]["text"]
    assert "client_secret" in sends[0]  # the service verifies the app, not the user


def test_a_failed_send_is_held_and_retried_for_a_day(client, kit, monkeypatch):
    """The account service down, or a user it will not mail yet: the message
    is held as send_failed, and the loop tries again until it goes."""
    import app.bw_auth as bw_auth_module
    from app.db import get_session_factory
    from app.services import outbound

    iid = make_project(client, kit)
    add_screen_option(client, iid)
    client.post("/api/outbound/settings", json={"dev_mode": False})

    def refuse(*_a, **_k):
        raise bw_auth_module.BWAuthError("send-mail failed: NOT_YOUR_USER")
    monkeypatch.setattr(bw_auth_module, "send_mail", refuse)
    client.post(f"/api/projects/{iid}/send")
    assert {m["held_reason"] for m in _outbox(client)} == {"send_failed"}

    monkeypatch.setattr(bw_auth_module, "send_mail", lambda *_a, **_k: True)
    with get_session_factory()() as db:
        assert outbound.retry_held(db) == len(_outbox(client))
        db.commit()
    assert all(m["sent_at"] for m in _outbox(client))


def test_a_reply_to_a_client_earns_an_email(client, kit):
    iid = make_project(client, kit)
    _sid, oid = add_screen_option(client, iid)
    as_user(client, "cleo")
    tid = client.post(f"/api/options/{oid}/pins", json={
        "x_percent": 5.0, "y_percent": 5.0,
        "body_md": "Can this be bigger?"}).json()["pin"]["thread_id"]

    as_user(client, "mara")
    client.post(f"/api/threads/{tid}/comments", json={"body_md": "Done — bumped it."})
    replied = [m for m in _outbox(client) if m["kind"] == "replied"]
    assert len(replied) == 1
    assert replied[0]["recipient"] == "cleo"
    assert "We replied to your note" in replied[0]["subject"]


def test_the_client_replying_to_themselves_mails_nobody(client, kit):
    """Only a manager's reply mails, and only to the client side. Us replying to
    each other is not news to anyone."""
    iid = make_project(client, kit)
    _sid, oid = add_screen_option(client, iid)
    as_user(client, "cleo")
    tid = client.post(f"/api/options/{oid}/pins", json={
        "x_percent": 5.0, "y_percent": 5.0, "body_md": "One."}).json()["pin"]["thread_id"]
    client.post(f"/api/threads/{tid}/comments", json={"body_md": "And another thing."})

    as_user(client, "mara")
    assert [m for m in _outbox(client) if m["kind"] == "replied"] == []


def test_only_two_moments_ever_mail(client, kit):
    """A tool that mails on every event trains people to ignore it, and the one
    message that mattered goes with the rest."""
    iid = make_project(client, kit)
    sid, oid = add_screen_option(client, iid)
    client.post(f"/api/projects/{iid}/send")

    as_user(client, "cleo")
    # A pin, an approval, a selection, a screen comment: all bell-only.
    client.post(f"/api/options/{oid}/pins",
                json={"x_percent": 5.0, "y_percent": 5.0, "body_md": "A note."})
    client.post(f"/api/screens/{sid}/select", json={"option_id": oid})
    client.post(f"/api/screens/{sid}/comments", json={"body_md": "About the page."})

    as_user(client, "mara")
    assert {m["kind"] for m in _outbox(client)} == {"presented"}


def test_a_suppressed_message_is_still_recorded(client, kit):
    """That is what makes Dev Mode more than a switch: "we turned it off and
    nothing was lost" is checkable rather than hopeful."""
    iid = make_project(client, kit)
    add_screen_option(client, iid)
    client.post(f"/api/projects/{iid}/send")
    box = _outbox(client)
    assert len(box) == 1
    assert box[0]["body"]        # the copy is readable before a transport exists
    assert box[0]["url"].endswith(f"/p/{iid}")


def test_the_switch_and_the_outbox_are_team_only(client, kit):
    iid = make_project(client, kit)
    add_screen_option(client, iid)
    as_user(client, "cleo")
    assert client.get("/api/outbound").status_code == 403
    assert client.get("/api/outbound/settings").status_code == 403
    assert client.post("/api/outbound/settings",
                       json={"dev_mode": False}).status_code == 403


def test_the_service_default_is_on_without_a_row(client, kit):
    from app.db import get_session_factory

    make_project(client, kit)
    with get_session_factory()() as db:
        assert outbound.dev_mode(db) is True


# ------------------------------------------------------------- the digest

def _mention(client, iid, author, text):
    """A page comment carrying a mention: the one event a person can raise
    for someone else from the client side."""
    from tests.conftest import as_user
    as_user(client, author)
    screen = client.get(f"/api/projects/{iid}").json()["screens"][0]
    r = client.post(f"/api/screens/{screen['id']}/comments", json={"body_md": text})
    assert r.status_code == 200, r.text


def test_the_digest_waits_for_the_quiet_then_mails_once(client, kit):
    """New items in the bell: nothing until an hour passes with nothing new,
    then ONE email, and the items it covered are not mailed again."""
    from datetime import datetime, timedelta, timezone
    from sqlalchemy import select
    from app.db import get_session_factory
    from app.models import Notification
    from app.services import digest

    iid = make_project(client, kit)
    add_screen_option(client, iid)
    client.post("/api/outbound/settings", json={"dev_mode": False})
    _mention(client, iid, "cleo", "@rian could the logo be larger?")
    now = datetime.now(timezone.utc)
    with get_session_factory()() as db:
        rows = db.scalars(select(Notification).where(Notification.recipient == "rian")).all()
        assert rows and all(r.delivered_at is None for r in rows)
        # Still arriving: nothing goes.
        assert digest.run_once(db, quiet_minutes=60, now=now) == 0
        # An hour of quiet: one digest per person with something waiting, the
        # rows stamped. rian was mentioned; mara hears as the rest of the team
        # (a client's comment reaches the team).
        assert digest.run_once(db, quiet_minutes=60, now=now + timedelta(minutes=61)) == 2
        db.commit()
        rows = db.scalars(select(Notification).where(Notification.recipient == "rian")).all()
        assert all(r.delivered_at is not None for r in rows)
        # And not again.
        assert digest.run_once(db, quiet_minutes=0, now=now + timedelta(hours=3)) == 0
    from tests.conftest import as_user
    as_user(client, "rian")
    box = [m for m in _outbox(client) if m["kind"] == "digest" and m["recipient"] == "rian"]
    assert len(box) == 1
    assert box[0]["sent_at"]
    assert "Design Easel" in box[0]["subject"]
    assert "cleo mentioned you" in box[0]["body"]
    assert box[0]["url"].endswith(f"/p/{iid}")


def test_something_read_in_time_is_not_mailed(client, kit):
    from datetime import datetime, timedelta, timezone
    from tests.conftest import as_user
    from app.db import get_session_factory
    from app.services import digest

    iid = make_project(client, kit)
    add_screen_option(client, iid)
    # A team member's mention: only rian is told, so only rian's reading counts.
    _mention(client, iid, "mara", "@rian one more thing")
    as_user(client, "rian")
    ids = [n["id"] for n in client.get("/api/notifications").json()["notifications"]]
    assert ids
    client.post("/api/notifications/read", json={"ids": ids})
    with get_session_factory()() as db:
        assert digest.run_once(db, quiet_minutes=0,
                               now=datetime.now(timezone.utc) + timedelta(hours=2)) == 0


def test_the_team_can_run_the_digest_now(client, kit):
    """The route is the loop's tick on demand; quiet 0 mails everyone with
    something waiting. Team only."""
    from tests.conftest import as_user

    iid = make_project(client, kit)
    add_screen_option(client, iid)
    _mention(client, iid, "cleo", "@rian ready when you are")
    as_user(client, "cleo")
    assert client.post("/api/outbound/digest", json={"quiet_minutes": 0}).status_code == 403
    as_user(client, "rian")
    r = client.post("/api/outbound/digest", json={"quiet_minutes": 0})
    assert r.status_code == 200, r.text
    # rian (mentioned) and mara (the team hears every client comment).
    assert r.json() == {"queued": 2, "retried": 0}
    assert sorted(m["recipient"] for m in _outbox(client) if m["kind"] == "digest") == ["mara", "rian"]
    assert {m["held_reason"] for m in _outbox(client)} == {"dev_mode"}  # on by default: held, recorded


def test_a_self_mention_lights_your_own_bell(client, kit):
    """As review allows: a reminder to yourself."""
    from tests.conftest import as_user

    iid = make_project(client, kit)
    add_screen_option(client, iid)
    _mention(client, iid, "rian", "@rian remember to check the footer")
    as_user(client, "rian")
    kinds = [n["kind"] for n in client.get("/api/notifications").json()["notifications"]]
    assert kinds == ["mention"]
