# pyright: reportPrivateUsage=false
"""Unit tests: client.request() and request_raw()."""

import json
from dataclasses import dataclass, field
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from seaart import AsyncClient, SyncClient
from seaart.exceptions import APIError


# ── Fake response ──────────────────────────────────────────────────────────


def _ok_bytes(data: object = None) -> bytes:
    return json.dumps(
        {"status": {"code": 10000, "msg": "ok", "request_id": "r1"}, "data": data}
    ).encode()


@dataclass
class FakeResponse:
    status_code: int = 200
    content: bytes = b""
    text: str = ""
    headers: dict[str, str] = field(default_factory=lambda: {"content-type": "text/html"})  # noqa: PIE807

    def json(self) -> dict[str, Any]:
        return json.loads(self.content)


def _ok_response(data: object = None) -> FakeResponse:
    body = _ok_bytes(data)
    return FakeResponse(content=body, text=body.decode())


def _error_response(status: int = 500) -> FakeResponse:
    return FakeResponse(status_code=status, content=b"server error", text="server error")


def _raw_response(content: bytes = b"<html>OK</html>", text: str = "<html>OK</html>") -> FakeResponse:
    return FakeResponse(content=content, text=text)


# ══════════════════════════════════════════════════════════════════════════
# request()
# ══════════════════════════════════════════════════════════════════════════


class TestAsyncRequest:
    async def test_returns_envelope(self, client: AsyncClient) -> None:
        fake = _ok_response({"id": "abc"})
        with patch.object(client._session, "request", new_callable=AsyncMock, return_value=fake):
            env = await client.request("GET", "some/endpoint")
            assert env.status.code == 10000
            assert env.data == {"id": "abc"}

    async def test_none_data(self, client: AsyncClient) -> None:
        fake = _ok_response()
        with patch.object(client._session, "request", new_callable=AsyncMock, return_value=fake):
            env = await client.request("POST", "some/action")
            assert env.status.code == 10000
            assert env.data is None

    async def test_passes_method_and_url(self, client: AsyncClient) -> None:
        fake = _ok_response()
        with patch.object(client._session, "request", new_callable=AsyncMock, return_value=fake) as mock:
            await client.request("POST", "test/path")
            assert mock.call_args[0][0] == "POST"
            assert "test/path" in mock.call_args[0][1]

    async def test_passes_json_body(self, client: AsyncClient) -> None:
        fake = _ok_response()
        with patch.object(client._session, "request", new_callable=AsyncMock, return_value=fake) as mock:
            await client.request("POST", "endpoint", json={"key": "val"})
            assert mock.call_args.kwargs["json"] == {"key": "val"}

    async def test_passes_params(self, client: AsyncClient) -> None:
        fake = _ok_response()
        with patch.object(client._session, "request", new_callable=AsyncMock, return_value=fake) as mock:
            await client.request("GET", "endpoint", params={"q": "search"})
            assert mock.call_args.kwargs["params"] == {"q": "search"}

    async def test_includes_auth_header(self, client: AsyncClient) -> None:
        fake = _ok_response()
        with patch.object(client._session, "request", new_callable=AsyncMock, return_value=fake) as mock:
            await client.request("GET", "endpoint")
            headers = mock.call_args.kwargs["headers"]
            assert headers["token"] == "test-token"

    async def test_merges_custom_headers(self, client: AsyncClient) -> None:
        fake = _ok_response()
        with patch.object(client._session, "request", new_callable=AsyncMock, return_value=fake) as mock:
            await client.request("GET", "endpoint", headers={"X-Custom": "val"})
            headers = mock.call_args.kwargs["headers"]
            assert headers["X-Custom"] == "val"
            assert headers["token"] == "test-token"

    async def test_http_error_raises(self, client: AsyncClient) -> None:
        fake = _error_response(500)
        with patch.object(client._session, "request", new_callable=AsyncMock, return_value=fake):
            with pytest.raises(APIError) as exc_info:
                await client.request("GET", "bad/endpoint")
            assert exc_info.value.http_status == 500

    async def test_non_json_raises(self, client: AsyncClient) -> None:
        fake = FakeResponse(content=b"not json", text="not json")
        with patch.object(client._session, "request", new_callable=AsyncMock, return_value=fake):
            with pytest.raises(APIError, match="Non-JSON response"):
                await client.request("GET", "endpoint")


class TestSyncRequest:
    def test_returns_envelope(self) -> None:
        client = SyncClient(token="test-token")
        fake = _ok_response({"id": "xyz"})
        with patch.object(client._session, "request", new_callable=MagicMock, return_value=fake):
            env = client.request("GET", "some/endpoint")
            assert env.status.code == 10000
            assert env.data == {"id": "xyz"}

    def test_passes_json_body(self) -> None:
        client = SyncClient(token="test-token")
        fake = _ok_response()
        with patch.object(client._session, "request", new_callable=MagicMock, return_value=fake) as mock:
            client.request("POST", "endpoint", json={"a": 1})
            assert mock.call_args.kwargs["json"] == {"a": 1}

    def test_http_error_raises(self) -> None:
        client = SyncClient(token="test-token")
        fake = _error_response(404)
        with patch.object(client._session, "request", new_callable=MagicMock, return_value=fake):
            with pytest.raises(APIError) as exc_info:
                client.request("GET", "missing")
            assert exc_info.value.http_status == 404

    def test_includes_auth_header(self) -> None:
        client = SyncClient(token="my-token")
        fake = _ok_response()
        with patch.object(client._session, "request", new_callable=MagicMock, return_value=fake) as mock:
            client.request("GET", "endpoint")
            headers = mock.call_args.kwargs["headers"]
            assert headers["token"] == "my-token"


# ══════════════════════════════════════════════════════════════════════════
# request_raw()
# ══════════════════════════════════════════════════════════════════════════


class TestAsyncRequestRaw:
    async def test_returns_raw_response(self, client: AsyncClient) -> None:
        fake = _raw_response()
        with patch.object(client._session, "request", new_callable=AsyncMock, return_value=fake):
            r = await client.request_raw("GET", "some/page")
            assert r.status_code == 200
            assert r.text == "<html>OK</html>"

    async def test_binary_content(self, client: AsyncClient) -> None:
        fake = _raw_response(content=b"\x89PNG\r\n", text="")
        with patch.object(client._session, "request", new_callable=AsyncMock, return_value=fake):
            r = await client.request_raw("GET", "some/image")
            assert r.content == b"\x89PNG\r\n"

    async def test_passes_params(self, client: AsyncClient) -> None:
        fake = _raw_response()
        with patch.object(client._session, "request", new_callable=AsyncMock, return_value=fake) as mock:
            await client.request_raw("POST", "endpoint", json={"key": "val"}, params={"q": "test"})
            assert mock.call_args.kwargs["json"] == {"key": "val"}
            assert mock.call_args.kwargs["params"] == {"q": "test"}

    async def test_passes_headers(self, client: AsyncClient) -> None:
        fake = _raw_response()
        with patch.object(client._session, "request", new_callable=AsyncMock, return_value=fake) as mock:
            await client.request_raw("GET", "page", headers={"X-Custom": "value"})
            call_headers = mock.call_args.kwargs["headers"]
            assert call_headers["X-Custom"] == "value"
            assert "token" in call_headers

    async def test_no_envelope_parsing(self, client: AsyncClient) -> None:
        fake = _raw_response(content=b"not json at all", text="not json at all")
        with patch.object(client._session, "request", new_callable=AsyncMock, return_value=fake):
            r = await client.request_raw("GET", "some/page")
            assert r.text == "not json at all"

    async def test_error_status_not_raised(self, client: AsyncClient) -> None:
        """request_raw does not raise on HTTP errors — caller handles it."""
        fake = FakeResponse(status_code=500, content=b"error", text="error")
        with patch.object(client._session, "request", new_callable=AsyncMock, return_value=fake):
            r = await client.request_raw("GET", "bad/page")
            assert r.status_code == 500


class TestSyncRequestRaw:
    def test_returns_raw_response(self) -> None:
        client = SyncClient(token="test-token")
        fake = _raw_response()
        with patch.object(client._session, "request", new_callable=MagicMock, return_value=fake):
            r = client.request_raw("GET", "some/page")
            assert r.status_code == 200
            assert r.text == "<html>OK</html>"

    def test_binary_content(self) -> None:
        client = SyncClient(token="test-token")
        fake = _raw_response(content=b"\x89PNG\r\n", text="")
        with patch.object(client._session, "request", new_callable=MagicMock, return_value=fake):
            r = client.request_raw("GET", "some/image")
            assert r.content == b"\x89PNG\r\n"

    def test_passes_json_body(self) -> None:
        client = SyncClient(token="test-token")
        fake = _raw_response()
        with patch.object(client._session, "request", new_callable=MagicMock, return_value=fake) as mock:
            client.request_raw("POST", "endpoint", json={"a": 1})
            assert mock.call_args.kwargs["json"] == {"a": 1}

    def test_no_envelope_parsing(self) -> None:
        client = SyncClient(token="test-token")
        fake = _raw_response(content=b"plain text", text="plain text")
        with patch.object(client._session, "request", new_callable=MagicMock, return_value=fake):
            r = client.request_raw("GET", "page")
            assert r.text == "plain text"

    def test_error_status_not_raised(self) -> None:
        client = SyncClient(token="test-token")
        fake = FakeResponse(status_code=403, content=b"forbidden", text="forbidden")
        with patch.object(client._session, "request", new_callable=MagicMock, return_value=fake):
            r = client.request_raw("GET", "page")
            assert r.status_code == 403

