"""Discord account linking: signed-state integrity, OAuth callback (Discord HTTP
mocked), unlink, and auth gating. Reuses the throwaway accounts DB from conftest."""
import pytest
from fastapi.testclient import TestClient

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

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


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


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


# --- signed state ------------------------------------------------------------

def test_state_round_trips_and_rejects_tampering():
    s = dl._sign_state("user-123")
    assert dl._verify_state(s) == "user-123"
    payload, mac = s.split(".", 1)
    assert dl._verify_state(f"{payload}.{'0' * len(mac)}") is None   # bad mac
    assert dl._verify_state(payload) is None                          # no mac
    assert dl._verify_state("garbage") is None


def test_state_expires(monkeypatch):
    monkeypatch.setattr(dl, "_STATE_TTL", -1)      # already expired
    assert dl._verify_state(dl._sign_state("u")) is None


def test_state_is_secret_dependent(monkeypatch):
    s = dl._sign_state("u")
    monkeypatch.setattr(dl, "SECRET", dl.SECRET + "-different")
    assert dl._verify_state(s) is None             # signed under the old secret


# --- routes ------------------------------------------------------------------

def test_config_reports_enabled_state(monkeypatch):
    c = TestClient(appmod.app)
    monkeypatch.setattr(dl, "CLIENT_ID", "")
    monkeypatch.setattr(dl, "CLIENT_SECRET", "")
    r = c.get(f"{V2}/discord/config")   # no auth required
    assert r.status_code == 200 and r.json() == {"enabled": False}
    monkeypatch.setattr(dl, "CLIENT_ID", "app123")
    monkeypatch.setattr(dl, "CLIENT_SECRET", "secret456")
    assert c.get(f"{V2}/discord/config").json() == {"enabled": True}


def test_link_requires_auth():
    c = TestClient(appmod.app)
    assert c.get(f"{V2}/discord/link/start", follow_redirects=False).status_code == 401
    assert c.post(f"{V2}/discord/unlink").status_code == 401


def test_start_redirects_to_discord(monkeypatch):
    monkeypatch.setattr(dl, "CLIENT_ID", "app123")
    monkeypatch.setattr(dl, "CLIENT_SECRET", "secret456")
    c = TestClient(appmod.app)
    _login(c, "start@example.com")
    r = c.get(f"{V2}/discord/link/start", follow_redirects=False)
    assert r.status_code == 303
    loc = r.headers["location"]
    assert loc.startswith("https://discord.com/api/oauth2/authorize")
    assert "client_id=app123" in loc and "scope=identify" in loc and "state=" in loc


def test_start_when_unconfigured_bounces_back(monkeypatch):
    monkeypatch.setattr(dl, "CLIENT_ID", "")
    monkeypatch.setattr(dl, "CLIENT_SECRET", "")
    c = TestClient(appmod.app)
    _login(c, "noconf@example.com")
    r = c.get(f"{V2}/discord/link/start", follow_redirects=False)
    assert r.status_code == 303 and "discord=unavailable" in r.headers["location"]


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


class _MockClient:
    """Stands in for httpx.AsyncClient: token exchange then /users/@me."""
    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-999", "username": "someone"})


def test_callback_links_the_account(monkeypatch):
    monkeypatch.setattr(dl, "CLIENT_ID", "app123")
    monkeypatch.setattr(dl, "CLIENT_SECRET", "secret456")
    monkeypatch.setattr(dl.httpx, "AsyncClient", _MockClient)
    c = TestClient(appmod.app)
    _login(c, "callback@example.com")
    uid = c.get(f"{V2}/users/me").json()["id"]
    state = dl._sign_state(uid)
    r = c.get(f"{V2}/discord/link/callback?code=abc&state={state}", follow_redirects=False)
    assert r.status_code == 303 and "discord=linked" in r.headers["location"]
    # /users/me now reports the linked id, and unlink clears it.
    assert c.get(f"{V2}/users/me").json()["discord_id"] == "discord-999"
    assert c.post(f"{V2}/discord/unlink").status_code == 204
    assert c.get(f"{V2}/users/me").json()["discord_id"] is None


def test_callback_rejects_a_forged_state(monkeypatch):
    monkeypatch.setattr(dl, "CLIENT_ID", "app123")
    monkeypatch.setattr(dl, "CLIENT_SECRET", "secret456")
    monkeypatch.setattr(dl.httpx, "AsyncClient", _MockClient)
    c = TestClient(appmod.app)
    _login(c, "forged@example.com")
    # State signed for a different user id must not link this session.
    r = c.get(f"{V2}/discord/link/callback?code=abc&state={dl._sign_state('someone-else')}",
              follow_redirects=False)
    assert r.status_code == 303 and "discord=error" in r.headers["location"]
    assert c.get(f"{V2}/users/me").json()["discord_id"] is None


def test_callback_without_code_is_cancelled(monkeypatch):
    monkeypatch.setattr(dl, "CLIENT_ID", "app123")
    monkeypatch.setattr(dl, "CLIENT_SECRET", "secret456")
    c = TestClient(appmod.app)
    _login(c, "cancel@example.com")
    r = c.get(f"{V2}/discord/link/callback?error=access_denied", follow_redirects=False)
    assert r.status_code == 303 and "discord=cancelled" in r.headers["location"]
