from __future__ import annotations

import json
from dataclasses import dataclass
from typing import Any, Mapping
from urllib.parse import urlencode

from .typing import ASGIApp, Message


@dataclass(slots=True)
class TestResponse:
    status_code: int
    headers: dict[str, str]
    body: bytes

    @property
    def text(self) -> str:
        return self.body.decode("utf-8")

    def json(self) -> Any:
        return json.loads(self.text)


class AsyncTestClient:
    def __init__(self, app: ASGIApp) -> None:
        self.app = app

    async def request(
        self,
        method: str,
        path: str,
        *,
        headers: Mapping[str, str] | None = None,
        query_params: Mapping[str, Any] | None = None,
        data: bytes | str | None = None,
        json_data: Any = None,
    ) -> TestResponse:
        payload = b""
        request_headers = {k.lower(): v for k, v in (headers or {}).items()}

        if json_data is not None:
            payload = json.dumps(json_data, ensure_ascii=False).encode("utf-8")
            request_headers.setdefault("content-type", "application/json")
        elif isinstance(data, str):
            payload = data.encode("utf-8")
        elif isinstance(data, bytes):
            payload = data

        query_string = b""
        if query_params:
            query_string = urlencode(query_params, doseq=True).encode("utf-8")

        scope = {
            "type": "http",
            "asgi": {"version": "3.0"},
            "http_version": "1.1",
            "method": method.upper(),
            "path": path,
            "query_string": query_string,
            "headers": [(k.encode("latin-1"), v.encode("latin-1")) for k, v in request_headers.items()],
            "client": ("testclient", 0),
            "server": ("testserver", 80),
            "scheme": "http",
        }

        sent_messages: list[Message] = []
        request_sent = False

        async def receive() -> Message:
            nonlocal request_sent
            if request_sent:
                return {"type": "http.disconnect"}
            request_sent = True
            return {"type": "http.request", "body": payload, "more_body": False}

        async def send(message: Message) -> None:
            sent_messages.append(message)

        await self.app(scope, receive, send)

        status_code = 500
        response_headers: dict[str, str] = {}
        body_chunks: list[bytes] = []

        for message in sent_messages:
            message_type = message.get("type")
            if message_type == "http.response.start":
                status_code = int(message.get("status", 500))
                headers_list = message.get("headers", [])
                response_headers = {
                    k.decode("latin-1").lower(): v.decode("latin-1")
                    for k, v in headers_list
                }
            elif message_type == "http.response.body":
                body_chunks.append(message.get("body", b""))

        return TestResponse(
            status_code=status_code,
            headers=response_headers,
            body=b"".join(body_chunks),
        )

    async def get(self, path: str, **kwargs: Any) -> TestResponse:
        return await self.request("GET", path, **kwargs)

    async def post(self, path: str, **kwargs: Any) -> TestResponse:
        return await self.request("POST", path, **kwargs)

    async def put(self, path: str, **kwargs: Any) -> TestResponse:
        return await self.request("PUT", path, **kwargs)

    async def patch(self, path: str, **kwargs: Any) -> TestResponse:
        return await self.request("PATCH", path, **kwargs)

    async def delete(self, path: str, **kwargs: Any) -> TestResponse:
        return await self.request("DELETE", path, **kwargs)

