import asyncio
import json

import pytest
from sqlalchemy import func, select

from orders_api.models import Order

pytestmark = pytest.mark.anyio


async def test_create_uses_menu_prices_and_returns_location_and_etag(create_order, asha):
    r = await create_order(asha)
    assert r.status_code == 201
    body = r.json()
    assert body["totalMinor"] == 2 * 18000 + 9000                 # prices come from the menu, not the client
    assert body["status"] == "pending" and body["customerId"] == 1
    assert r.headers["location"] == f"/v1/orders/{body['id']}"
    assert r.headers["etag"] == f'"{body["id"]}.1"'


@pytest.mark.parametrize("body, problem", [
    ({"items": [{"sku": "unicorn-frappe", "quantity": 1}]}, "unknown-product"),
    ({"items": [{"sku": "pumpkin-spice", "quantity": 1}]}, "unknown-product"),        # inactive
    ({"items": []}, "validation-error"),
    ({"items": [{"sku": "latte", "quantity": 0}]}, "validation-error"),
    ({"items": [{"sku": "latte", "quantity": 1}, {"sku": "latte", "quantity": 1}]}, "validation-error"),
    ({"items": [{"sku": "latte", "quantity": 1}], "totalMinor": 1}, "validation-error"),   # can't set the price
    ({"items": [{"sku": "latte", "quantity": 1}], "status": "paid"}, "validation-error"),  # or the status
])
async def test_invalid_orders_are_rejected(create_order, asha, body, problem):
    r = await create_order(asha, body)
    assert r.status_code == 422
    assert r.json()["type"].endswith(problem)


async def test_keyset_pagination_walks_every_order_once(create_order, client, asha):
    for _ in range(7):
        await create_order(asha)
    seen, cursor, pages = [], None, 0
    while True:
        params = {"limit": 3} | ({"cursor": cursor} if cursor else {})
        page = (await client.get("/v1/orders", params=params, headers=asha)).json()
        seen += [order["id"] for order in page["items"]]
        pages += 1
        cursor = page["nextCursor"]
        if cursor is None:
            break
    assert seen == [7, 6, 5, 4, 3, 2, 1] and pages == 3


async def test_bad_cursor_is_a_400_problem(client, asha):
    r = await client.get("/v1/orders", params={"cursor": "not-a-cursor"}, headers=asha)
    assert r.status_code == 400 and r.json()["type"].endswith("invalid-cursor")


async def test_status_filter(create_order, client, asha, admin):
    first = (await create_order(asha)).json()
    await create_order(asha)
    await client.post(f"/v1/orders/{first['id']}/status", json={"status": "paid"},
                      headers=admin | {"If-Match": f'"{first["id"]}.1"'})
    paid = (await client.get("/v1/orders", params={"status": "paid"}, headers=asha)).json()["items"]
    assert [o["id"] for o in paid] == [first["id"]]


async def test_etag_304_and_optimistic_locking(create_order, client, asha):
    order = (await create_order(asha)).json()
    url = f"/v1/orders/{order['id']}"
    etag = (await client.get(url, headers=asha)).headers["etag"]
    assert (await client.get(url, headers=asha | {"If-None-Match": etag})).status_code == 304

    assert (await client.patch(url, json={"note": "oat milk"}, headers=asha)).status_code == 428
    ok = await client.patch(url, json={"note": "oat milk"}, headers=asha | {"If-Match": etag})
    assert ok.status_code == 200 and ok.json()["note"] == "oat milk" and ok.headers["etag"] != etag
    stale = await client.patch(url, json={"note": "soy milk"}, headers=asha | {"If-Match": etag})
    assert stale.status_code == 412
    assert stale.json()["currentEtag"] == ok.headers["etag"]


async def test_patch_changes_only_the_fields_sent(create_order, client, asha):
    order = (await create_order(asha, {"items": [{"sku": "mocha", "quantity": 1}], "note": "extra hot"})).json()
    url = f"/v1/orders/{order['id']}"
    r = await client.patch(url, json={}, headers=asha | {"If-Match": '"1.1"'})
    assert r.json()["note"] == "extra hot"
    r = await client.patch(url, json={"note": None}, headers=asha | {"If-Match": r.headers["etag"]})
    assert r.json()["note"] is None


async def test_status_machine(create_order, client, asha, admin):
    order = (await create_order(asha)).json()
    url, etag = f"/v1/orders/{order['id']}", f'"{order["id"]}.1"'
    for status in ("paid", "preparing", "ready"):
        r = await client.post(f"{url}/status", json={"status": status}, headers=admin | {"If-Match": etag})
        assert r.status_code == 200, r.text
        etag = r.headers["etag"]
    skip = await client.post(f"{url}/status", json={"status": "pending"}, headers=admin | {"If-Match": etag})
    assert skip.status_code == 409 and skip.json()["allowed"] == ["completed"]


async def test_customer_can_cancel_only_pending_orders(create_order, client, asha, admin):
    first = (await create_order(asha)).json()
    assert (await client.post(f"/v1/orders/{first['id']}/cancel", headers=asha)).json()["status"] == "cancelled"
    second = (await create_order(asha)).json()
    await client.post(f"/v1/orders/{second['id']}/status", json={"status": "paid"},
                      headers=admin | {"If-Match": f'"{second["id"]}.1"'})
    r = await client.post(f"/v1/orders/{second['id']}/cancel", headers=asha)
    assert r.status_code == 409


async def test_sse_streams_status_until_the_order_is_finished(create_order, client, asha, admin, state):
    order = (await create_order(asha)).json()
    url = f"/v1/orders/{order['id']}"

    async def watch() -> list[str]:
        statuses = []
        async with client.stream("GET", f"{url}/events", headers=asha) as r:
            assert r.headers["content-type"].startswith("text/event-stream")
            async for line in r.aiter_lines():
                if line.startswith("data: "):
                    statuses.append(json.loads(line.removeprefix("data: "))["status"])
        return statuses

    watcher = asyncio.create_task(watch())
    for _ in range(100):                                        # wait until the stream has subscribed
        if state["events"].subscriber_count(order["id"]):
            break
        await asyncio.sleep(0.01)
    etag = f'"{order["id"]}.1"'
    for status in ("paid", "preparing", "ready", "completed"):
        r = await client.post(f"{url}/status", json={"status": status}, headers=admin | {"If-Match": etag})
        etag = r.headers["etag"]
    assert await asyncio.wait_for(watcher, timeout=5) == ["pending", "paid", "preparing", "ready", "completed"]
    assert state["events"].subscriber_count(order["id"]) == 0          # unsubscribed when the stream ended


async def test_orders_are_committed_to_the_database(create_order, asha, state):
    for _ in range(3):
        await create_order(asha)
    async with state["sessions"]() as session:
        assert await session.scalar(select(func.count()).select_from(Order)) == 3
