"""API tests via FastAPI TestClient. The DB layer is monkeypatched (no MySQL needed)."""
import os

from fastapi.testclient import TestClient

import app as app_module

client = TestClient(app_module.app)


def test_config():
    r = client.get("/api/config")
    assert r.status_code == 200
    body = r.json()
    assert body["subscriptionUrl"] == "https://example.test/pay"
    assert body["redirectToPay"] is True


def test_me_hit(monkeypatch):
    monkeypatch.setattr(app_module.db, "get_profile", lambda u: {"iUserID": 1, "name": "Neel"})
    r = client.get("/api/me", params={"u": "x" * 64})
    assert r.status_code == 200
    assert r.json()["name"] == "Neel"


def test_me_miss(monkeypatch):
    monkeypatch.setattr(app_module.db, "get_profile", lambda u: None)
    r = client.get("/api/me", params={"u": "bad"})
    assert r.status_code == 200
    assert r.json() == {}


def test_me_never_500s(monkeypatch):
    def boom(u):
        raise RuntimeError("db down")
    monkeypatch.setattr(app_module.db, "get_profile", boom)
    r = client.get("/api/me", params={"u": "x" * 64})
    assert r.status_code == 200  # errors are swallowed → generic funnel
    assert r.json() == {}


def test_health_ok(monkeypatch):
    monkeypatch.setattr(app_module.db, "ping", lambda: True)
    r = client.get("/api/health")
    assert r.status_code == 200
    assert r.json()["db"] is True


def test_health_degraded(monkeypatch):
    monkeypatch.setattr(app_module.db, "ping", lambda: False)
    r = client.get("/api/health")
    assert r.status_code == 503


def test_track_batch(monkeypatch):
    calls = {}
    def fake_insert(sid, events, meta):
        calls.update(sid=sid, events=events, meta=meta)
        return len(events)
    monkeypatch.setattr(app_module.db, "insert_clickstream", fake_insert)
    r = client.post("/api/track-batch", json={
        "sessionId": "s1", "meta": {"deviceType": "desktop"},
        "events": [{"step": "hook", "stepNumber": 0, "timeSpentSec": 2}],
    })
    assert r.status_code == 200
    assert r.json()["inserted"] == 1
    assert calls["sid"] == "s1"
    assert calls["meta"]["deviceType"] == "desktop"


def test_session(monkeypatch):
    calls = {}
    def fake_upsert(sid, data):
        calls.update(sid=sid, data=data)
        return True
    monkeypatch.setattr(app_module.db, "upsert_session", fake_upsert)
    r = client.post("/api/session", json={"sessionId": "s2", "data": {"vName": "T", "bReachedOffer": 1}})
    assert r.status_code == 200
    assert r.json()["ok"] is True
    assert calls["data"]["vName"] == "T"


def test_session_validation_rejects_bad_shape():
    # data must be an object; a list is invalid → Pydantic 422
    r = client.post("/api/session", json={"sessionId": "s", "data": ["not", "a", "dict"]})
    assert r.status_code == 422


def test_rate_limit_fails_open_without_xff(monkeypatch):
    monkeypatch.setattr(app_module.db, "insert_clickstream", lambda *a: 0)
    monkeypatch.setattr(app_module, "_RATE_MAX", 2)
    app_module._rate_hits.clear()
    # No X-Forwarded-For → we can't identify the client → never block.
    for _ in range(5):
        r = client.post("/api/track-batch", json={"sessionId": "s", "events": []})
        assert r.status_code == 200


def test_rate_limit_blocks_flood_with_xff(monkeypatch):
    monkeypatch.setattr(app_module.db, "insert_clickstream", lambda *a: 0)
    monkeypatch.setattr(app_module, "_RATE_MAX", 2)
    app_module._rate_hits.clear()
    headers = {"X-Forwarded-For": "9.9.9.9"}
    codes = [
        client.post("/api/track-batch", json={"sessionId": "s", "events": []}, headers=headers).status_code
        for _ in range(4)
    ]
    assert 429 in codes


# ── _rate_ok unit-level: fails open (no client IP), and enforces the cap ──
def test_rate_ok_none_ip_fails_open():
    # No identifiable client IP (no X-Forwarded-For) => never block (fail-open).
    assert app_module._rate_ok(None) is True
    assert app_module._rate_ok("") is True


def test_track_batch_fails_open_without_client_ip(monkeypatch):
    # End-to-end: with no X-Forwarded-For, the limiter must let every request through.
    monkeypatch.setattr(app_module.db, "insert_clickstream", lambda *a: 0)
    monkeypatch.setattr(app_module, "_RATE_MAX", 1)
    app_module._rate_hits.clear()
    codes = [
        client.post("/api/track-batch", json={"sessionId": "s", "events": []}).status_code
        for _ in range(5)
    ]
    assert codes == [200] * 5


def test_rate_ok_enforces_cap(monkeypatch):
    monkeypatch.setattr(app_module, "_RATE_MAX", 3)
    app_module._rate_hits.clear()
    ip = "5.5.5.5"
    allowed = [app_module._rate_ok(ip) for _ in range(3)]
    blocked = app_module._rate_ok(ip)
    assert allowed == [True, True, True]
    assert blocked is False


# ── /api/me: profile hit, excluded (test/deleted) user, unknown hash ──
def test_me_valid_hash_returns_profile(monkeypatch):
    monkeypatch.setattr(app_module.db, "get_profile",
                        lambda u: {"iUserID": 10, "name": "Ann", "eligible": True})
    r = client.get("/api/me", params={"u": "a" * 64})
    assert r.status_code == 200
    body = r.json()
    assert body["iUserID"] == 10
    assert body["name"] == "Ann"


def test_me_excluded_user_returns_empty(monkeypatch):
    # get_profile applies bDeleted=0/bTestUser=0; an excluded user resolves to None -> {}.
    monkeypatch.setattr(app_module.db, "get_profile", lambda u: None)
    r = client.get("/api/me", params={"u": "d" * 64})
    assert r.status_code == 200
    assert r.json() == {}


def test_me_unknown_hash_returns_empty(monkeypatch):
    monkeypatch.setattr(app_module.db, "get_profile", lambda u: None)
    r = client.get("/api/me", params={"u": "e" * 64})
    assert r.status_code == 200
    assert r.json() == {}


# ── POST fail-open: DB raising still yields {"ok": false}, never a 5xx ──
def test_track_batch_never_5xx_on_db_error(monkeypatch):
    def boom(*a, **k):
        raise RuntimeError("db down")
    monkeypatch.setattr(app_module.db, "insert_clickstream", boom)
    r = client.post("/api/track-batch", json={"sessionId": "s", "events": [{"step": "x"}]})
    assert r.status_code == 200
    assert r.json() == {"ok": False}


def test_session_never_5xx_on_db_error(monkeypatch):
    def boom(*a, **k):
        raise RuntimeError("db down")
    monkeypatch.setattr(app_module.db, "upsert_session", boom)
    r = client.post("/api/session", json={"sessionId": "s", "data": {"vName": "X"}})
    assert r.status_code == 200
    assert r.json() == {"ok": False}


def test_session_returns_ok_false_when_upsert_fails(monkeypatch):
    # upsert_session returns False (not an exception) => {"ok": false}, still 200.
    monkeypatch.setattr(app_module.db, "upsert_session", lambda sid, data: False)
    r = client.post("/api/session", json={"sessionId": "s", "data": {"vName": "X"}})
    assert r.status_code == 200
    assert r.json()["ok"] is False


# ── /api/health: reachable vs down ──
def test_health_ok_shape(monkeypatch):
    monkeypatch.setattr(app_module.db, "ping", lambda: True)
    r = client.get("/api/health")
    assert r.status_code == 200
    body = r.json()
    assert body["status"] == "ok"
    assert body["db"] is True


def test_health_down_returns_503(monkeypatch):
    monkeypatch.setattr(app_module.db, "ping", lambda: False)
    r = client.get("/api/health")
    assert r.status_code == 503
    body = r.json()
    assert body["status"] == "degraded"
    assert body["db"] is False


# ── /api/config: reflects env; typed model ──
def test_config_redirect_flag_off(monkeypatch):
    monkeypatch.setenv("REDIRECT_TO_PAY", "0")
    r = client.get("/api/config")
    assert r.status_code == 200
    assert r.json()["redirectToPay"] is False


# ── _check_config: logs (never raises) when required config is missing ──
def test_check_config_logs_missing(monkeypatch, caplog):
    for k in ("DB_HOST", "DB_USERNAME", "DB_PASSWORD", "DB_DATABASE"):
        monkeypatch.delenv(k, raising=False)
    monkeypatch.delenv("SUBSCRIPTION_URL", raising=False)
    with caplog.at_level("ERROR"):
        app_module._check_config()  # must not raise
    joined = " ".join(r.message for r in caplog.records)
    assert "missing required env vars" in joined
    assert "SUBSCRIPTION_URL" in joined


def test_check_config_quiet_when_present(monkeypatch, caplog):
    for k in ("DB_HOST", "DB_USERNAME", "DB_PASSWORD", "DB_DATABASE"):
        monkeypatch.setenv(k, "x")
    monkeypatch.setenv("SUBSCRIPTION_URL", "https://example.test/pay")
    with caplog.at_level("ERROR"):
        app_module._check_config()
    assert not any("missing required env vars" in r.message for r in caplog.records)


def test_conftest_sets_mount_prefix_empty():
    # Prod contract: MOUNT_PREFIX empty means routes live at /api/*.
    assert os.environ.get("MOUNT_PREFIX", "") == ""
