"""Route-level tests for the accounts feature: auth (fastapi-users), subscription
CRUD, and the notifications endpoints. Uses a throwaway accounts DB (conftest points
THERMOGRAPH_ACCOUNTS_DB at a temp file) so nothing is written into the repo."""
import pytest
from fastapi.testclient import TestClient

from web import app as appmod
from accounts import db

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


@pytest.fixture(scope="module", autouse=True)
def _tables():
    # TestClient(app) (no context manager) skips the lifespan, so create the
    # account tables directly against the temp DB.
    db.Base.metadata.create_all(db.sync_engine)


def _client():
    return TestClient(appmod.app)


def _login(client, email):
    r = client.post(f"{V2}/auth/register", json={"email": email, "password": PW})
    assert r.status_code in (201, 400)          # 400 only if the email already exists
    r = client.post(f"{V2}/auth/login", data={"username": email, "password": PW})
    assert r.status_code == 204                  # sets the session cookie on the client


def test_auth_flow():
    c = _client()
    _login(c, "auth-flow@example.com")
    me = c.get(f"{V2}/users/me")
    assert me.status_code == 200 and me.json()["email"] == "auth-flow@example.com"
    assert c.post(f"{V2}/auth/logout").status_code == 204
    assert c.get(f"{V2}/users/me").status_code == 401


def test_requires_auth():
    c = _client()
    assert c.get(f"{V2}/subscriptions").status_code == 401
    assert c.get(f"{V2}/notifications").status_code == 401


def test_subscription_crud():
    c = _client()
    _login(c, "crud@example.com")
    r = c.post(f"{V2}/subscriptions", json={
        "lat": 47.6, "lon": -122.3, "label": "Seattle", "threshold": 96,
        "metrics": ["tmax", "feels"], "kind": "observed", "two_sided": True,
    })
    assert r.status_code == 201
    sub = r.json()
    assert sub["cell_id"] and sub["threshold"] == 96 and sub["metrics"] == ["tmax", "feels"]

    assert len(c.get(f"{V2}/subscriptions").json()) == 1

    patched = c.patch(f"{V2}/subscriptions/{sub['id']}", json={"threshold": 99, "active": False})
    assert patched.status_code == 200
    assert patched.json()["threshold"] == 99 and patched.json()["active"] is False

    assert c.delete(f"{V2}/subscriptions/{sub['id']}").status_code == 204
    assert c.get(f"{V2}/subscriptions").json() == []


def test_duplicate_and_validation():
    c = _client()
    _login(c, "dup@example.com")
    body = {"lat": 33.4, "lon": -112.0, "threshold": 97, "metrics": ["tmax"], "kind": "observed"}
    assert c.post(f"{V2}/subscriptions", json=body).status_code == 201
    assert c.post(f"{V2}/subscriptions", json=body).status_code == 409           # same cell + kind
    # a forecast subscription on the same cell is allowed (different kind)
    assert c.post(f"{V2}/subscriptions", json={**body, "kind": "forecast"}).status_code == 201
    # threshold must be 95..99
    assert c.post(f"{V2}/subscriptions", json={**body, "threshold": 90}).status_code == 422
    # at least one metric required
    assert c.post(f"{V2}/subscriptions", json={**body, "metrics": []}).status_code == 422


def test_ownership_is_404():
    a, b = _client(), _client()
    _login(a, "owner-a@example.com")
    _login(b, "owner-b@example.com")
    sub_id = a.post(f"{V2}/subscriptions", json={
        "lat": 40.7, "lon": -74.0, "threshold": 95, "metrics": ["tmax"], "kind": "observed",
    }).json()["id"]
    # B cannot see, patch, or delete A's subscription — 404, not 403 (no existence leak)
    assert b.patch(f"{V2}/subscriptions/{sub_id}", json={"threshold": 98}).status_code == 404
    assert b.delete(f"{V2}/subscriptions/{sub_id}").status_code == 404
    assert b.get(f"{V2}/subscriptions").json() == []


def test_notifications_empty_and_read_all():
    c = _client()
    _login(c, "notif@example.com")
    r = c.get(f"{V2}/notifications")
    assert r.status_code == 200
    assert r.json() == {"notifications": [], "unread_count": 0}
    assert c.post(f"{V2}/notifications/read-all").status_code == 204


# --- web push ---------------------------------------------------------------
_SUB = {"endpoint": "https://push.example.com/ep-1", "keys": {"p256dh": "BKEY", "auth": "YXV0aA"}}


def test_push_vapid_key_is_open():
    # The public key is not a secret and the browser needs it before login, so
    # this route is unauthenticated.
    r = _client().get(f"{V2}/push/vapid-key")
    assert r.status_code == 200
    assert isinstance(r.json()["key"], str) and r.json()["key"]


def test_push_requires_auth():
    c = _client()
    assert c.post(f"{V2}/push/subscribe", json=_SUB).status_code == 401
    assert c.post(f"{V2}/push/test").status_code == 401


def test_push_subscribe_upsert_test_unsubscribe(monkeypatch):
    from notifications import push
    sent = []
    monkeypatch.setattr(push, "send", lambda info, payload: sent.append(info["endpoint"]) or "ok")
    c = _client()
    _login(c, "push@example.com")

    assert c.post(f"{V2}/push/subscribe", json=_SUB).status_code == 201
    # Re-subscribe from the same device (same endpoint, new keys) upserts — one device.
    assert c.post(f"{V2}/push/subscribe",
                  json={**_SUB, "keys": {"p256dh": "BNEW", "auth": "bmV3"}}).status_code == 201

    r = c.post(f"{V2}/push/test")
    assert r.status_code == 202
    assert r.json() == {"devices": 1, "sent": 1, "pruned": 0, "failed": 0}
    assert sent == [_SUB["endpoint"]]

    assert c.request("DELETE", f"{V2}/push/subscribe",
                     json={"endpoint": _SUB["endpoint"]}).status_code == 204
    assert c.post(f"{V2}/push/test").json()["devices"] == 0


def test_push_test_prunes_gone_endpoint(monkeypatch):
    from notifications import push
    monkeypatch.setattr(push, "send", lambda info, payload: "gone")
    c = _client()
    _login(c, "push-gone@example.com")
    assert c.post(f"{V2}/push/subscribe", json=_SUB).status_code == 201
    r = c.post(f"{V2}/push/test")
    assert r.json() == {"devices": 1, "sent": 0, "pruned": 1, "failed": 0}
    # the gone endpoint was removed, so a second pass sees no devices
    assert c.post(f"{V2}/push/test").json()["devices"] == 0


def test_push_test_reports_failed_delivery(monkeypatch):
    # A rejected delivery (e.g. VAPID mismatch) must be reported, not silently dropped:
    # the endpoint keeps returning 202 but with sent:0 / failed:N so the client can tell.
    from notifications import push
    monkeypatch.setattr(push, "send", lambda info, payload: "error")
    c = _client()
    _login(c, "push-fail@example.com")
    assert c.post(f"{V2}/push/subscribe", json=_SUB).status_code == 201
    r = c.post(f"{V2}/push/test")
    assert r.status_code == 202
    assert r.json() == {"devices": 1, "sent": 0, "pruned": 0, "failed": 1}
