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

import pytest
from fastapi import Depends, FastAPI
from fastapi.exceptions import FastAPIDeprecationWarning
from fastapi.testclient import TestClient

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


@pytest.fixture(
    scope="module",
    params=[
        "pydantic-v2",
        "pydantic-v1",
    ],
)
def basemodel_class(request: pytest.FixtureRequest) -> type[Any]:
    if request.param == "pydantic-v2":
        from pydantic import BaseModel

        return BaseModel
    else:
        from pydantic.v1 import BaseModel

        return BaseModel


@pytest.fixture(scope="module")
def app(basemodel_class: type[Any]) -> FastAPI:
    class ItemIn(basemodel_class):
        name: str
        value: int

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

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

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

    app = FastAPI()

    with warnings.catch_warnings(record=True):
        warnings.filterwarnings(
            "ignore",
            message=r"pydantic\.v1 is deprecated and will soon stop being supported by FastAPI\..*",
            category=FastAPIDeprecationWarning,
        )

        @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)

    return app


@pytest.fixture(scope="module")
def client(app: FastAPI) -> 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()
