import json
import sys
from collections.abc import Iterator
from typing import Annotated, Any

import pytest
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel

if "--codspeed" not in sys.argv:
    pytest.skip(
        "Benchmark tests are skipped by default; run with --codspeed.",
        allow_module_level=True,
    )

LARGE_ITEMS: list[dict[str, Any]] = [
    {
        "id": i,
        "name": f"item-{i}",
        "values": list(range(25)),
        "meta": {
            "active": True,
            "group": i % 10,
            "tag": f"t{i % 5}",
        },
    }
    for i in range(300)
]

LARGE_METADATA: dict[str, Any] = {
    "source": "benchmark",
    "version": 1,
    "flags": {"a": True, "b": False, "c": True},
    "notes": ["x" * 50, "y" * 50, "z" * 50],
}

LARGE_PAYLOAD: dict[str, Any] = {"items": LARGE_ITEMS, "metadata": LARGE_METADATA}


def dep_a():
    return 40


def dep_b(a: Annotated[int, Depends(dep_a)]):
    return a + 2


class ItemIn(BaseModel):
    name: str
    value: int


class ItemOut(BaseModel):
    name: str
    value: int
    dep: int


class LargeIn(BaseModel):
    items: list[dict[str, Any]]
    metadata: dict[str, Any]


class LargeOut(BaseModel):
    items: list[dict[str, Any]]
    metadata: dict[str, Any]


app = FastAPI()


@app.post("/sync/validated", response_model=ItemOut)
def sync_validated(item: ItemIn, dep: Annotated[int, Depends(dep_b)]):
    return ItemOut(name=item.name, value=item.value, dep=dep)


@app.get("/sync/dict-no-response-model")
def sync_dict_no_response_model():
    return {"name": "foo", "value": 123}


@app.get("/sync/dict-with-response-model", response_model=ItemOut)
def sync_dict_with_response_model(
    dep: Annotated[int, Depends(dep_b)],
):
    return {"name": "foo", "value": 123, "dep": dep}


@app.get("/sync/model-no-response-model")
def sync_model_no_response_model(dep: Annotated[int, Depends(dep_b)]):
    return ItemOut(name="foo", value=123, dep=dep)


@app.get("/sync/model-with-response-model", response_model=ItemOut)
def sync_model_with_response_model(dep: Annotated[int, Depends(dep_b)]):
    return ItemOut(name="foo", value=123, dep=dep)


@app.post("/async/validated", response_model=ItemOut)
async def async_validated(
    item: ItemIn,
    dep: Annotated[int, Depends(dep_b)],
):
    return ItemOut(name=item.name, value=item.value, dep=dep)


@app.post("/sync/large-receive")
def sync_large_receive(payload: LargeIn):
    return {"received": len(payload.items)}


@app.post("/async/large-receive")
async def async_large_receive(payload: LargeIn):
    return {"received": len(payload.items)}


@app.get("/sync/large-dict-no-response-model")
def sync_large_dict_no_response_model():
    return LARGE_PAYLOAD


@app.get("/sync/large-dict-with-response-model", response_model=LargeOut)
def sync_large_dict_with_response_model():
    return LARGE_PAYLOAD


@app.get("/sync/large-model-no-response-model")
def sync_large_model_no_response_model():
    return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA)


@app.get("/sync/large-model-with-response-model", response_model=LargeOut)
def sync_large_model_with_response_model():
    return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA)


@app.get("/async/large-dict-no-response-model")
async def async_large_dict_no_response_model():
    return LARGE_PAYLOAD


@app.get("/async/large-dict-with-response-model", response_model=LargeOut)
async def async_large_dict_with_response_model():
    return LARGE_PAYLOAD


@app.get("/async/large-model-no-response-model")
async def async_large_model_no_response_model():
    return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA)


@app.get("/async/large-model-with-response-model", response_model=LargeOut)
async def async_large_model_with_response_model():
    return LargeOut(items=LARGE_ITEMS, metadata=LARGE_METADATA)


@app.get("/async/dict-no-response-model")
async def async_dict_no_response_model():
    return {"name": "foo", "value": 123}


@app.get("/async/dict-with-response-model", response_model=ItemOut)
async def async_dict_with_response_model(
    dep: Annotated[int, Depends(dep_b)],
):
    return {"name": "foo", "value": 123, "dep": dep}


@app.get("/async/model-no-response-model")
async def async_model_no_response_model(
    dep: Annotated[int, Depends(dep_b)],
):
    return ItemOut(name="foo", value=123, dep=dep)


@app.get("/async/model-with-response-model", response_model=ItemOut)
async def async_model_with_response_model(
    dep: Annotated[int, Depends(dep_b)],
):
    return ItemOut(name="foo", value=123, dep=dep)


@pytest.fixture(scope="module")
def client() -> Iterator[TestClient]:
    with TestClient(app) as client:
        yield client


def _bench_get(benchmark, client: TestClient, path: str) -> tuple[int, bytes]:
    warmup = client.get(path)
    assert warmup.status_code == 200

    def do_request() -> tuple[int, bytes]:
        response = client.get(path)
        return response.status_code, response.content

    return benchmark(do_request)


def _bench_post_json(
    benchmark, client: TestClient, path: str, json: dict[str, Any]
) -> tuple[int, bytes]:
    warmup = client.post(path, json=json)
    assert warmup.status_code == 200

    def do_request() -> tuple[int, bytes]:
        response = client.post(path, json=json)
        return response.status_code, response.content

    return benchmark(do_request)


def test_sync_receiving_validated_pydantic_model(benchmark, client: TestClient) -> None:
    status_code, body = _bench_post_json(
        benchmark,
        client,
        "/sync/validated",
        json={"name": "foo", "value": 123},
    )
    assert status_code == 200
    assert body == b'{"name":"foo","value":123,"dep":42}'


def test_sync_return_dict_without_response_model(benchmark, client: TestClient) -> None:
    status_code, body = _bench_get(benchmark, client, "/sync/dict-no-response-model")
    assert status_code == 200
    assert body == b'{"name":"foo","value":123}'


def test_sync_return_dict_with_response_model(benchmark, client: TestClient) -> None:
    status_code, body = _bench_get(benchmark, client, "/sync/dict-with-response-model")
    assert status_code == 200
    assert body == b'{"name":"foo","value":123,"dep":42}'


def test_sync_return_model_without_response_model(
    benchmark, client: TestClient
) -> None:
    status_code, body = _bench_get(benchmark, client, "/sync/model-no-response-model")
    assert status_code == 200
    assert body == b'{"name":"foo","value":123,"dep":42}'


def test_sync_return_model_with_response_model(benchmark, client: TestClient) -> None:
    status_code, body = _bench_get(benchmark, client, "/sync/model-with-response-model")
    assert status_code == 200
    assert body == b'{"name":"foo","value":123,"dep":42}'


def test_async_receiving_validated_pydantic_model(
    benchmark, client: TestClient
) -> None:
    status_code, body = _bench_post_json(
        benchmark, client, "/async/validated", json={"name": "foo", "value": 123}
    )
    assert status_code == 200
    assert body == b'{"name":"foo","value":123,"dep":42}'


def test_async_return_dict_without_response_model(
    benchmark, client: TestClient
) -> None:
    status_code, body = _bench_get(benchmark, client, "/async/dict-no-response-model")
    assert status_code == 200
    assert body == b'{"name":"foo","value":123}'


def test_async_return_dict_with_response_model(benchmark, client: TestClient) -> None:
    status_code, body = _bench_get(benchmark, client, "/async/dict-with-response-model")
    assert status_code == 200
    assert body == b'{"name":"foo","value":123,"dep":42}'


def test_async_return_model_without_response_model(
    benchmark, client: TestClient
) -> None:
    status_code, body = _bench_get(benchmark, client, "/async/model-no-response-model")
    assert status_code == 200
    assert body == b'{"name":"foo","value":123,"dep":42}'


def test_async_return_model_with_response_model(benchmark, client: TestClient) -> None:
    status_code, body = _bench_get(
        benchmark, client, "/async/model-with-response-model"
    )
    assert status_code == 200
    assert body == b'{"name":"foo","value":123,"dep":42}'


def test_sync_receiving_large_payload(benchmark, client: TestClient) -> None:
    status_code, body = _bench_post_json(
        benchmark,
        client,
        "/sync/large-receive",
        json=LARGE_PAYLOAD,
    )
    assert status_code == 200
    assert body == b'{"received":300}'


def test_async_receiving_large_payload(benchmark, client: TestClient) -> None:
    status_code, body = _bench_post_json(
        benchmark,
        client,
        "/async/large-receive",
        json=LARGE_PAYLOAD,
    )
    assert status_code == 200
    assert body == b'{"received":300}'


def _expected_large_payload_json_bytes() -> bytes:
    return json.dumps(
        LARGE_PAYLOAD,
        ensure_ascii=False,
        allow_nan=False,
        separators=(",", ":"),
    ).encode("utf-8")


def test_sync_return_large_dict_without_response_model(
    benchmark, client: TestClient
) -> None:
    status_code, body = _bench_get(
        benchmark, client, "/sync/large-dict-no-response-model"
    )
    assert status_code == 200
    assert body == _expected_large_payload_json_bytes()


def test_sync_return_large_dict_with_response_model(
    benchmark, client: TestClient
) -> None:
    status_code, body = _bench_get(
        benchmark, client, "/sync/large-dict-with-response-model"
    )
    assert status_code == 200
    assert body == _expected_large_payload_json_bytes()


def test_sync_return_large_model_without_response_model(
    benchmark, client: TestClient
) -> None:
    status_code, body = _bench_get(
        benchmark, client, "/sync/large-model-no-response-model"
    )
    assert status_code == 200
    assert body == _expected_large_payload_json_bytes()


def test_sync_return_large_model_with_response_model(
    benchmark, client: TestClient
) -> None:
    status_code, body = _bench_get(
        benchmark, client, "/sync/large-model-with-response-model"
    )
    assert status_code == 200
    assert body == _expected_large_payload_json_bytes()


def test_async_return_large_dict_without_response_model(
    benchmark, client: TestClient
) -> None:
    status_code, body = _bench_get(
        benchmark, client, "/async/large-dict-no-response-model"
    )
    assert status_code == 200
    assert body == _expected_large_payload_json_bytes()


def test_async_return_large_dict_with_response_model(
    benchmark, client: TestClient
) -> None:
    status_code, body = _bench_get(
        benchmark, client, "/async/large-dict-with-response-model"
    )
    assert status_code == 200
    assert body == _expected_large_payload_json_bytes()


def test_async_return_large_model_without_response_model(
    benchmark, client: TestClient
) -> None:
    status_code, body = _bench_get(
        benchmark, client, "/async/large-model-no-response-model"
    )
    assert status_code == 200
    assert body == _expected_large_payload_json_bytes()


def test_async_return_large_model_with_response_model(
    benchmark, client: TestClient
) -> None:
    status_code, body = _bench_get(
        benchmark, client, "/async/large-model-with-response-model"
    )
    assert status_code == 200
    assert body == _expected_large_payload_json_bytes()
