"""test_app.py — `pytest -q` (offline). Uses FastAPI's TestClient (no server,
no Azure). Success criteria for the production API + Cosmos modeling + resilience.
"""
from __future__ import annotations

import importlib

import pytest
from fastapi.testclient import TestClient

import app as app_module
from resilience import IdempotencyCache, RateLimited, retry_with_backoff
from store import CosmosLikeStore


@pytest.fixture
def client():
    # Fresh module state per test (store/idempotency are module-level).
    importlib.reload(app_module)
    return TestClient(app_module.app)


# --- API behavior -----------------------------------------------------------
def test_health(client):
    assert client.get("/health").json() == {"status": "ok"}


def test_chat_creates_conversation_and_point_read(client):
    r = client.post("/chat", json={"message": "How long to dispute a transaction?",
                                   "conversation_id": "c1"})
    assert r.status_code == 200
    assert "60 days" in r.json()["answer"] and r.json()["cited"] is True
    # the conversation is retrievable (single-partition point read)
    conv = client.get("/conversations/c1").json()
    assert len(conv["messages"]) == 2          # user + assistant
    assert conv["messages"][0]["role"] == "user"


def test_validation_rejects_empty_message(client):
    r = client.post("/chat", json={"message": "", "conversation_id": "c1"})
    assert r.status_code == 422                 # Pydantic rejects min_length violation


def test_idempotency_returns_cached_and_no_duplicate(client):
    headers = {"X-Idempotency-Key": "abc-123"}
    body = {"message": "fee?", "conversation_id": "c9"}
    r1 = client.post("/chat", json=body, headers=headers)
    r2 = client.post("/chat", json=body, headers=headers)   # retry, same key
    assert r1.json() == r2.json()
    # The retried request must NOT have written a second pair of messages.
    conv = client.get("/conversations/c9").json()
    assert len(conv["messages"]) == 2


def test_streaming_yields_tokens(client):
    r = client.post("/chat/stream", json={"message": "iban?", "conversation_id": "c2"})
    assert r.status_code == 200
    assert "text/event-stream" in r.headers["content-type"]
    body = r.text
    assert "data:" in body and "[DONE]" in body
    assert "23" in body                          # the IBAN length, streamed


def test_unknown_conversation_404(client):
    assert client.get("/conversations/does-not-exist").status_code == 404


# --- Cosmos modeling --------------------------------------------------------
def test_point_read_is_cheaper_than_cross_partition():
    s = CosmosLikeStore()
    for i in range(50):
        s.create_item({"id": f"k{i}", "conversationId": f"conv{i % 5}", "seq": i})
    ru_before = s.ru_consumed
    s.point_read("k0", "conv0")                  # cheap, single partition
    point_cost = s.ru_consumed - ru_before
    ru_before = s.ru_consumed
    s.cross_partition_query(lambda d: d["seq"] > 0)   # scans all partitions
    xpart_cost = s.ru_consumed - ru_before
    assert xpart_cost > point_cost * 5           # cross-partition is much costlier


# --- resilience -------------------------------------------------------------
def test_retry_with_backoff_recovers():
    calls = {"n": 0}

    def flaky():
        calls["n"] += 1
        if calls["n"] < 3:
            raise RateLimited(retry_after=0.0)
        return "ok"

    assert retry_with_backoff(flaky, sleep=lambda _ : None) == "ok"
    assert calls["n"] == 3


def test_retry_gives_up_after_max():
    def always_429():
        raise RateLimited()
    with pytest.raises(RateLimited):
        retry_with_backoff(always_429, max_attempts=2, sleep=lambda _ : None)


def test_idempotency_cache():
    c = IdempotencyCache()
    assert c.get("k") is None
    c.put("k", {"a": 1})
    assert c.get("k") == {"a": 1}
