"""Discord DM alerts: send_dm REST mechanics (mocked), the notify dispatch gate,
the on/off toggle endpoint, and that linking opts in."""
import types

import pytest
from fastapi.testclient import TestClient

from web import app as appmod
from accounts import db
from notifications import discord
from notifications import discord_link as dl
from notifications import notify

V2 = "/thermograph/api/v2"
PW = "supersecret123"


@pytest.fixture(scope="module", autouse=True)
def _tables():
    db.Base.metadata.create_all(db.sync_engine)


# --- send_dm REST mechanics --------------------------------------------------

class _Resp:
    def __init__(self, code, data=None): self.status_code = code; self._data = data or {}
    def json(self): return self._data


def _mock_posts(monkeypatch, script):
    """Route discord._client.post by URL; `script` maps a URL substring -> _Resp."""
    calls = []

    def _post(url, json=None, headers=None, timeout=None):
        calls.append((url, json))
        for frag, resp in script.items():
            if frag in url:
                return resp
        return _Resp(404)
    monkeypatch.setattr(discord._client, "post", _post)
    return calls


def test_send_dm_opens_channel_then_posts(monkeypatch):
    monkeypatch.setattr(discord, "BOT_TOKEN", "bot-abc")
    calls = _mock_posts(monkeypatch, {
        "/users/@me/channels": _Resp(200, {"id": "chan-1"}),
        "/channels/chan-1/messages": _Resp(204),
    })
    ok = discord.send_dm("discord-9", "Tehran hit a Near Record high", "104F, 99th pct", "/day#x")
    assert ok is True
    assert calls[0][0].endswith("/users/@me/channels") and calls[0][1] == {"recipient_id": "discord-9"}
    assert "/channels/chan-1/messages" in calls[1][0]
    embed = calls[1][1]["embeds"][0]
    assert embed["title"].startswith("Tehran") and embed["url"].endswith("/day#x")


def test_send_dm_disabled_or_no_recipient(monkeypatch):
    monkeypatch.setattr(discord, "BOT_TOKEN", "")
    assert discord.send_dm("d", "t", "b") is False
    monkeypatch.setattr(discord, "BOT_TOKEN", "bot")
    assert discord.send_dm("", "t", "b") is False


def test_send_dm_gives_up_if_channel_open_fails(monkeypatch):
    monkeypatch.setattr(discord, "BOT_TOKEN", "bot-abc")
    calls = _mock_posts(monkeypatch, {"/users/@me/channels": _Resp(403)})
    assert discord.send_dm("d", "t", "b") is False
    assert len(calls) == 1        # never tried to post a message


def test_post_subscription_alert_posts_to_channel(monkeypatch):
    monkeypatch.setattr(discord, "BOT_TOKEN", "bot-abc")
    monkeypatch.setattr(discord, "SUBSCRIPTION_CHANNEL_ID", "chan-sub")
    calls = _mock_posts(monkeypatch, {"/channels/chan-sub/messages": _Resp(204)})
    ok = discord.post_subscription_alert("Seattle: unusually hot day", "97th pct", "/day#x")
    assert ok is True
    (url, body), = calls
    assert url.endswith("/channels/chan-sub/messages")
    embed = body["embeds"][0]
    assert embed["title"].startswith("Seattle") and embed["url"].endswith("/day#x")


def test_post_subscription_alert_noop_without_config(monkeypatch):
    # Channel set but no bot token, and bot token set but no channel: both no-op.
    monkeypatch.setattr(discord, "BOT_TOKEN", "")
    monkeypatch.setattr(discord, "SUBSCRIPTION_CHANNEL_ID", "chan-sub")
    calls = _mock_posts(monkeypatch, {})
    assert discord.post_subscription_alert("t", "b") is False
    monkeypatch.setattr(discord, "BOT_TOKEN", "bot-abc")
    monkeypatch.setattr(discord, "SUBSCRIPTION_CHANNEL_ID", "")
    assert discord.post_subscription_alert("t", "b") is False
    assert calls == []            # never touched the network


def test_bot_post_retries_once_on_429(monkeypatch):
    monkeypatch.setattr(discord, "BOT_TOKEN", "bot-abc")
    monkeypatch.setattr(discord.time, "sleep", lambda s: None)     # don't actually wait
    seq = iter([_Resp(429, {"retry_after": 0.01}), _Resp(200, {"id": "c"})])
    monkeypatch.setattr(discord._client, "post", lambda *a, **k: next(seq))
    resp = discord._bot_post("/users/@me/channels", {"recipient_id": "d"})
    assert resp.status_code == 200        # the retry succeeded


# --- notify dispatch gate ----------------------------------------------------

def _fake_session(user):
    return types.SimpleNamespace(get=lambda model, uid: user)


def _sub():
    return types.SimpleNamespace(id="s1", user_id="u1", lat=47.6, lon=-122.3)


def test_discord_job_gathers_linked_opted_in_user(monkeypatch):
    monkeypatch.setattr(discord, "dm_enabled", lambda: True)
    user = types.SimpleNamespace(discord_id="d9", discord_dm=True)
    job = notify._discord_job(_fake_session(user), _sub(), "T", "B", "2026-07-19")
    assert job is not None and job[0] == "d9"


def test_discord_job_skips_unlinked_or_opted_out(monkeypatch):
    monkeypatch.setattr(discord, "dm_enabled", lambda: True)
    # linked but opted out
    assert notify._discord_job(
        _fake_session(types.SimpleNamespace(discord_id="d", discord_dm=False)),
        _sub(), "T", "B", "2026-07-19") is None
    # not linked
    assert notify._discord_job(
        _fake_session(types.SimpleNamespace(discord_id=None, discord_dm=True)),
        _sub(), "T", "B", "2026-07-19") is None


def test_discord_job_noop_when_bot_unconfigured(monkeypatch):
    monkeypatch.setattr(discord, "dm_enabled", lambda: False)
    user = types.SimpleNamespace(discord_id="d", discord_dm=True)
    assert notify._discord_job(_fake_session(user), _sub(), "T", "B", "2026-07-19") is None


def test_send_discord_job_calls_send_dm(monkeypatch):
    sent = []
    monkeypatch.setattr(discord, "send_dm", lambda *a, **k: sent.append(a) or True)
    notify._send_discord_job(("d9", "T", "B", "/day#x"))
    assert sent == [("d9", "T", "B", "/day#x")]


# --- the on/off toggle + link opt-in -----------------------------------------

def _login(client, email):
    r = client.post(f"{V2}/auth/register", json={"email": email, "password": PW})
    assert r.status_code in (201, 400)
    assert client.post(f"{V2}/auth/login", data={"username": email, "password": PW}).status_code == 204


class _MockClient:
    def __init__(self, *a, **k): pass
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    async def post(self, url, **k): return _Resp(200, {"access_token": "tok"})
    async def get(self, url, **k): return _Resp(200, {"id": "discord-777"})


def test_linking_opts_in_and_toggle_flips(monkeypatch):
    monkeypatch.setattr(dl, "CLIENT_ID", "app")
    monkeypatch.setattr(dl, "CLIENT_SECRET", "sec")
    monkeypatch.setattr(dl.httpx, "AsyncClient", _MockClient)
    c = TestClient(appmod.app)
    _login(c, "dm-toggle@example.com")
    uid = c.get(f"{V2}/users/me").json()["id"]
    # Link -> opted in by default.
    r = c.get(f"{V2}/discord/link/callback?code=x&state={dl._sign_state(uid)}", follow_redirects=False)
    assert r.status_code == 303
    me = c.get(f"{V2}/users/me").json()
    assert me["discord_id"] == "discord-777" and me["discord_dm"] is True
    # Mute without unlinking.
    assert c.post(f"{V2}/discord/dm", json={"enabled": False}).status_code == 204
    me = c.get(f"{V2}/users/me").json()
    assert me["discord_id"] == "discord-777" and me["discord_dm"] is False
    # Turn back on.
    assert c.post(f"{V2}/discord/dm", json={"enabled": True}).status_code == 204
    assert c.get(f"{V2}/users/me").json()["discord_dm"] is True


def test_dm_toggle_requires_auth():
    c = TestClient(appmod.app)
    assert c.post(f"{V2}/discord/dm", json={"enabled": True}).status_code == 401
