"""Tests for the Presa Python SDK."""

from __future__ import annotations

import json
from io import BytesIO
from unittest.mock import MagicMock, patch
from urllib.error import HTTPError

import pytest

from presa import PDF, ApiError, AuthenticationError, Presa, PresaError, TimeoutError
from presa.client import _build_request_body
from presa.types import CustomFormat, GenerateOptions, Margins, PresaConfig


class TestPresaConstructor:
    def test_raises_when_no_api_key(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.delenv("PRESA_API_KEY", raising=False)
        with pytest.raises(AuthenticationError, match="PRESA_API_KEY"):
            Presa()

    def test_error_message_includes_signup_url(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.delenv("PRESA_API_KEY", raising=False)
        with pytest.raises(AuthenticationError, match="https://presa.dev"):
            Presa()

    def test_error_message_includes_vercel(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.delenv("PRESA_API_KEY", raising=False)
        with pytest.raises(AuthenticationError, match="Vercel Marketplace"):
            Presa()

    def test_reads_api_key_from_env(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv("PRESA_API_KEY", "test-key")
        client = Presa()
        assert client._api_key == "test-key"

    def test_accepts_api_key_via_config(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.delenv("PRESA_API_KEY", raising=False)
        client = Presa(PresaConfig(api_key="config-key"))
        assert client._api_key == "config-key"

    def test_config_api_key_takes_precedence(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv("PRESA_API_KEY", "env-key")
        client = Presa(PresaConfig(api_key="config-key"))
        assert client._api_key == "config-key"

    def test_strips_trailing_slashes_from_base_url(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.delenv("PRESA_API_KEY", raising=False)
        client = Presa(PresaConfig(api_key="key", base_url="http://localhost:8080///"))
        assert client._base_url == "http://localhost:8080"


def _mock_urlopen(data: bytes) -> MagicMock:
    """Create a mock for urllib.request.urlopen that returns given data."""
    mock_response = MagicMock()
    mock_response.read.return_value = data
    mock_response.__enter__ = MagicMock(return_value=mock_response)
    mock_response.__exit__ = MagicMock(return_value=False)
    return mock_response


class TestPresaGenerate:
    @patch("presa.client.urllib.request.urlopen")
    def test_sends_correct_request(self, mock_urlopen: MagicMock) -> None:
        pdf_bytes = b"%PDF-1.4 test"
        mock_urlopen.return_value = _mock_urlopen(pdf_bytes)

        client = Presa(PresaConfig(api_key="test-key", base_url="http://localhost:8080"))
        result = client.generate(GenerateOptions(html="<h1>Hello</h1>"))

        assert mock_urlopen.called
        request = mock_urlopen.call_args[0][0]
        assert request.full_url == "http://localhost:8080/v1/pdf/from-html"
        assert request.get_header("Content-type") == "application/json"
        assert request.get_header("Authorization") == "Bearer test-key"
        assert request.method == "POST"

        body = json.loads(request.data.decode("utf-8"))
        assert body == {"html": "<h1>Hello</h1>"}

        assert result.buffer == pdf_bytes
        assert result.size == len(pdf_bytes)

    @patch("presa.client.urllib.request.urlopen")
    def test_sends_all_options(self, mock_urlopen: MagicMock) -> None:
        mock_urlopen.return_value = _mock_urlopen(b"%PDF")

        client = Presa(PresaConfig(api_key="test-key", base_url="http://localhost:8080"))
        client.generate(
            GenerateOptions(
                html="<h1>Hello</h1>",
                format="A4",
                landscape=True,
                margins=Margins(top="2cm", right="1cm", bottom="2cm", left="1cm"),
                print_background=False,
                scale=1.5,
            )
        )

        request = mock_urlopen.call_args[0][0]
        body = json.loads(request.data.decode("utf-8"))
        assert body == {
            "html": "<h1>Hello</h1>",
            "format": "A4",
            "landscape": True,
            "margins": {"top": "2cm", "right": "1cm", "bottom": "2cm", "left": "1cm"},
            "printBackground": False,
            "scale": 1.5,
        }

    @patch("presa.client.urllib.request.urlopen")
    def test_custom_format(self, mock_urlopen: MagicMock) -> None:
        mock_urlopen.return_value = _mock_urlopen(b"%PDF")

        client = Presa(PresaConfig(api_key="test-key"))
        client.generate(
            GenerateOptions(
                html="<h1>Hello</h1>",
                format=CustomFormat(width="210mm", height="297mm"),
            )
        )

        request = mock_urlopen.call_args[0][0]
        body = json.loads(request.data.decode("utf-8"))
        assert body["format"] == {"width": "210mm", "height": "297mm"}

    @patch("presa.client.urllib.request.urlopen")
    def test_api_error_response(self, mock_urlopen: MagicMock) -> None:
        error_body = json.dumps({
            "error": {
                "code": "QUOTA_EXCEEDED",
                "message": "Monthly quota exceeded",
                "status": 403,
            }
        }).encode("utf-8")
        mock_urlopen.side_effect = HTTPError(
            url="http://localhost/v1/pdf/from-html",
            code=403,
            msg="Forbidden",
            hdrs=MagicMock(),  # type: ignore[arg-type]
            fp=BytesIO(error_body),
        )

        client = Presa(PresaConfig(api_key="test-key"))
        with pytest.raises(ApiError) as exc_info:
            client.generate(GenerateOptions(html="<h1>Hello</h1>"))

        assert exc_info.value.code == "QUOTA_EXCEEDED"
        assert exc_info.value.status == 403
        assert "Monthly quota exceeded" in str(exc_info.value)

    @patch("presa.client.urllib.request.urlopen")
    def test_401_raises_authentication_error(self, mock_urlopen: MagicMock) -> None:
        error_body = json.dumps({
            "error": {
                "code": "UNAUTHORIZED",
                "message": "Invalid API key",
                "status": 401,
            }
        }).encode("utf-8")
        mock_urlopen.side_effect = HTTPError(
            url="http://localhost/v1/pdf/from-html",
            code=401,
            msg="Unauthorized",
            hdrs=MagicMock(),  # type: ignore[arg-type]
            fp=BytesIO(error_body),
        )

        client = Presa(PresaConfig(api_key="bad-key"))
        with pytest.raises(AuthenticationError):
            client.generate(GenerateOptions(html="<h1>Hello</h1>"))

    @patch("presa.client.urllib.request.urlopen")
    def test_omits_undefined_optional_fields(self, mock_urlopen: MagicMock) -> None:
        mock_urlopen.return_value = _mock_urlopen(b"%PDF")

        client = Presa(PresaConfig(api_key="test-key"))
        client.generate(GenerateOptions(html="<h1>Hello</h1>"))

        request = mock_urlopen.call_args[0][0]
        body = json.loads(request.data.decode("utf-8"))
        assert list(body.keys()) == ["html"]


class TestPDFNamespace:
    @patch("presa.client.urllib.request.urlopen")
    def test_generates_pdf_from_env(
        self, mock_urlopen: MagicMock, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("PRESA_API_KEY", "test-key")
        pdf_bytes = b"%PDF-1.4"
        mock_urlopen.return_value = _mock_urlopen(pdf_bytes)

        result = PDF.generate(html="<h1>Hello</h1>")
        assert result.buffer == pdf_bytes
        assert result.size == len(pdf_bytes)

    def test_raises_when_no_env(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.delenv("PRESA_API_KEY", raising=False)
        with pytest.raises(AuthenticationError):
            PDF.generate(html="<h1>Hello</h1>")


class TestBuildRequestBody:
    def test_minimal(self) -> None:
        body = _build_request_body(GenerateOptions(html="<h1>Hello</h1>"))
        assert body == {"html": "<h1>Hello</h1>"}

    def test_margins_omits_none_values(self) -> None:
        body = _build_request_body(
            GenerateOptions(html="<h1>Hello</h1>", margins=Margins(top="1cm"))
        )
        assert body["margins"] == {"top": "1cm"}


class TestErrorClasses:
    def test_presa_error_is_exception(self) -> None:
        err = PresaError("test")
        assert isinstance(err, Exception)

    def test_authentication_error_extends_presa_error(self) -> None:
        err = AuthenticationError()
        assert isinstance(err, PresaError)

    def test_api_error_includes_code_and_status(self) -> None:
        err = ApiError(code="TEST_ERROR", message="Test message", status=422)
        assert err.code == "TEST_ERROR"
        assert err.status == 422
        assert "Test message" in str(err)

    def test_timeout_error_includes_duration(self) -> None:
        err = TimeoutError(5.0)
        assert "5.0s" in str(err)
