"""Tests for the RendShot Python SDK."""

from __future__ import annotations

import pytest
import httpx
import respx

from rendshot import (
    RendshotClient,
    AsyncRendshotClient,
    RendshotError,
    RenderImageOptions,
    ScreenshotUrlOptions,
    ImageResult,
    ImageMetadata,
    UsageResult,
)

BASE = "https://api.rendshot.ai"

IMAGE_RESP = {
    "imageId": "img_1",
    "url": "https://cdn/img.png",
    "width": 800,
    "height": 600,
    "format": "png",
    "size": 1234,
    "createdAt": "2026-03-20",
}

USAGE_RESP = {"month": "2026-03", "count": 42, "limit": 1000, "plan": "pro"}

META_RESP = {**IMAGE_RESP, "imageId": "img_3", "expiresAt": "2026-03-27"}


# ---------------------------------------------------------------------------
# Constructor
# ---------------------------------------------------------------------------


class TestConstructor:
    @respx.mock
    def test_default_base_url(self):
        route = respx.get(f"{BASE}/v1/usage").mock(
            return_value=httpx.Response(200, json=USAGE_RESP)
        )
        client = RendshotClient(api_key="rs_live_test")
        client.get_usage()
        assert route.called

    @respx.mock
    def test_strips_trailing_slash(self):
        route = respx.get("https://custom.api.com/v1/usage").mock(
            return_value=httpx.Response(200, json=USAGE_RESP)
        )
        client = RendshotClient(api_key="rs_live_test", base_url="https://custom.api.com/")
        client.get_usage()
        assert route.called

    @respx.mock
    def test_context_manager(self):
        respx.get(f"{BASE}/v1/usage").mock(
            return_value=httpx.Response(200, json=USAGE_RESP)
        )
        with RendshotClient(api_key="rs_live_test") as client:
            result = client.get_usage()
        assert result.plan == "pro"


# ---------------------------------------------------------------------------
# Headers
# ---------------------------------------------------------------------------


class TestHeaders:
    @respx.mock
    def test_sends_auth_and_user_agent_on_post(self):
        route = respx.post(f"{BASE}/v1/image").mock(
            return_value=httpx.Response(200, json=IMAGE_RESP)
        )
        client = RendshotClient(api_key="rs_live_abc")
        client.render_image(RenderImageOptions(html="<h1>hi</h1>"))

        req = route.calls[0].request
        assert req.headers["authorization"] == "Bearer rs_live_abc"
        assert "rendshot-python/" in req.headers["user-agent"]

    @respx.mock
    def test_sends_auth_and_user_agent_on_get(self):
        route = respx.get(f"{BASE}/v1/usage").mock(
            return_value=httpx.Response(200, json=USAGE_RESP)
        )
        client = RendshotClient(api_key="rs_live_abc")
        client.get_usage()

        req = route.calls[0].request
        assert req.headers["authorization"] == "Bearer rs_live_abc"
        assert "rendshot-python/" in req.headers["user-agent"]


# ---------------------------------------------------------------------------
# render_image
# ---------------------------------------------------------------------------


class TestRenderImage:
    @respx.mock
    def test_posts_to_v1_image(self):
        route = respx.post(f"{BASE}/v1/image").mock(
            return_value=httpx.Response(200, json=IMAGE_RESP)
        )
        client = RendshotClient(api_key="rs_live_test")
        result = client.render_image(
            RenderImageOptions(html="<h1>Hello</h1>", width=800, height=600, format="png")
        )

        assert route.called
        import json
        body = json.loads(route.calls[0].request.content)
        assert body["html"] == "<h1>Hello</h1>"
        assert body["width"] == 800
        assert isinstance(result, ImageResult)
        assert result.image_id == "img_1"
        assert result.url == "https://cdn/img.png"

    @respx.mock
    def test_accepts_dict(self):
        respx.post(f"{BASE}/v1/image").mock(
            return_value=httpx.Response(200, json=IMAGE_RESP)
        )
        client = RendshotClient(api_key="rs_live_test")
        result = client.render_image({"html": "<h1>Hello</h1>"})
        assert result.image_id == "img_1"

    @respx.mock
    def test_fonts_and_timeout_reach_wire(self):
        route = respx.post(f"{BASE}/v1/image").mock(
            return_value=httpx.Response(200, json=IMAGE_RESP)
        )
        client = RendshotClient(api_key="rs_live_test")
        client.render_image(RenderImageOptions(html="<h1>x</h1>", fonts=["Inter", "Roboto"], timeout=5000))

        import json
        body = json.loads(route.calls[0].request.content)
        assert body["fonts"] == ["Inter", "Roboto"]
        assert body["timeout"] == 5000

    @respx.mock
    def test_device_scale_maps_to_camel_case(self):
        route = respx.post(f"{BASE}/v1/image").mock(
            return_value=httpx.Response(200, json=IMAGE_RESP)
        )
        client = RendshotClient(api_key="rs_live_test")
        client.render_image(RenderImageOptions(html="<h1>x</h1>", device_scale=2))

        import json
        body = json.loads(route.calls[0].request.content)
        assert body["deviceScale"] == 2
        assert "device_scale" not in body


# ---------------------------------------------------------------------------
# screenshot_url
# ---------------------------------------------------------------------------


class TestScreenshotUrl:
    @respx.mock
    def test_posts_to_v1_screenshot(self):
        shot_resp = {**IMAGE_RESP, "imageId": "img_2", "width": 1280, "height": 800}
        route = respx.post(f"{BASE}/v1/screenshot").mock(
            return_value=httpx.Response(200, json=shot_resp)
        )
        client = RendshotClient(api_key="rs_live_test")
        result = client.screenshot_url(
            ScreenshotUrlOptions(url="https://example.com", full_page=True)
        )

        import json
        body = json.loads(route.calls[0].request.content)
        assert body["url"] == "https://example.com"
        assert body["fullPage"] is True
        assert result.image_id == "img_2"

    @respx.mock
    def test_accepts_dict(self):
        respx.post(f"{BASE}/v1/screenshot").mock(
            return_value=httpx.Response(200, json=IMAGE_RESP)
        )
        client = RendshotClient(api_key="rs_live_test")
        result = client.screenshot_url({"url": "https://example.com"})
        assert result.image_id == "img_1"

    @respx.mock
    def test_device_scale_maps_to_camel_case(self):
        route = respx.post(f"{BASE}/v1/screenshot").mock(
            return_value=httpx.Response(200, json=IMAGE_RESP)
        )
        client = RendshotClient(api_key="rs_live_test")
        client.screenshot_url(ScreenshotUrlOptions(url="https://example.com", device_scale=2))

        import json
        body = json.loads(route.calls[0].request.content)
        assert body["deviceScale"] == 2
        assert "device_scale" not in body


# ---------------------------------------------------------------------------
# get_usage
# ---------------------------------------------------------------------------


class TestGetUsage:
    @respx.mock
    def test_returns_typed_result(self):
        respx.get(f"{BASE}/v1/usage").mock(
            return_value=httpx.Response(200, json=USAGE_RESP)
        )
        client = RendshotClient(api_key="rs_live_test")
        result = client.get_usage()

        assert isinstance(result, UsageResult)
        assert result.plan == "pro"
        assert result.count == 42
        assert result.limit == 1000


# ---------------------------------------------------------------------------
# get_image
# ---------------------------------------------------------------------------


class TestGetImage:
    @respx.mock
    def test_returns_metadata(self):
        respx.get(f"{BASE}/v1/image/img_3").mock(
            return_value=httpx.Response(200, json=META_RESP)
        )
        client = RendshotClient(api_key="rs_live_test")
        result = client.get_image("img_3")

        assert isinstance(result, ImageMetadata)
        assert result.expires_at == "2026-03-27"

    @respx.mock
    def test_encodes_image_id(self):
        encoded_resp = {**META_RESP, "imageId": "img/special"}
        route = respx.get(f"{BASE}/v1/image/img%2Fspecial").mock(
            return_value=httpx.Response(200, json=encoded_resp)
        )
        client = RendshotClient(api_key="rs_live_test")
        client.get_image("img/special")
        assert route.called


# ---------------------------------------------------------------------------
# Error handling
# ---------------------------------------------------------------------------


class TestErrors:
    @respx.mock
    def test_api_error_with_code_and_status(self):
        respx.post(f"{BASE}/v1/image").mock(
            return_value=httpx.Response(
                429,
                json={"error": {"code": "RATE_LIMIT_EXCEEDED", "message": "Rate limited", "status": 429}},
            )
        )
        client = RendshotClient(api_key="rs_live_test")
        with pytest.raises(RendshotError) as exc_info:
            client.render_image(RenderImageOptions(html="<p>x</p>"))

        assert exc_info.value.code == "RATE_LIMIT_EXCEEDED"
        assert exc_info.value.status == 429
        assert exc_info.value.message == "Rate limited"
        assert "Rate limited" in str(exc_info.value)

    @respx.mock
    def test_fallback_when_no_error_field(self):
        respx.post(f"{BASE}/v1/image").mock(
            return_value=httpx.Response(500, json={})
        )
        client = RendshotClient(api_key="rs_live_test")
        with pytest.raises(RendshotError) as exc_info:
            client.render_image(RenderImageOptions(html="<p>x</p>"))

        assert exc_info.value.code == "UNKNOWN_ERROR"
        assert exc_info.value.status == 500

    @respx.mock
    def test_non_json_error_response(self):
        respx.post(f"{BASE}/v1/image").mock(
            return_value=httpx.Response(502, text="Bad Gateway")
        )
        client = RendshotClient(api_key="rs_live_test")
        with pytest.raises(RendshotError) as exc_info:
            client.render_image(RenderImageOptions(html="<p>x</p>"))

        assert exc_info.value.code == "UNKNOWN_ERROR"
        assert exc_info.value.status == 502

    @respx.mock
    def test_non_dict_json_error_body(self):
        respx.post(f"{BASE}/v1/image").mock(
            return_value=httpx.Response(500, json=None)
        )
        client = RendshotClient(api_key="rs_live_test")
        with pytest.raises(RendshotError) as exc_info:
            client.render_image(RenderImageOptions(html="<p>x</p>"))
        assert exc_info.value.code == "UNKNOWN_ERROR"
        assert exc_info.value.status == 500

    @respx.mock
    def test_non_json_ok_response(self):
        respx.post(f"{BASE}/v1/image").mock(
            return_value=httpx.Response(200, text="OK")
        )
        client = RendshotClient(api_key="rs_live_test")
        with pytest.raises(ValueError, match="non-JSON"):
            client.render_image(RenderImageOptions(html="<p>x</p>"))


# ---------------------------------------------------------------------------
# Async client
# ---------------------------------------------------------------------------


class TestAsyncClient:
    @respx.mock
    async def test_render_image(self):
        respx.post(f"{BASE}/v1/image").mock(
            return_value=httpx.Response(200, json=IMAGE_RESP)
        )
        async with AsyncRendshotClient(api_key="rs_live_test") as client:
            result = await client.render_image(RenderImageOptions(html="<h1>hi</h1>"))
        assert result.image_id == "img_1"

    @respx.mock
    async def test_screenshot_url(self):
        respx.post(f"{BASE}/v1/screenshot").mock(
            return_value=httpx.Response(200, json=IMAGE_RESP)
        )
        async with AsyncRendshotClient(api_key="rs_live_test") as client:
            result = await client.screenshot_url(ScreenshotUrlOptions(url="https://example.com"))
        assert result.image_id == "img_1"

    @respx.mock
    async def test_get_usage(self):
        respx.get(f"{BASE}/v1/usage").mock(
            return_value=httpx.Response(200, json=USAGE_RESP)
        )
        async with AsyncRendshotClient(api_key="rs_live_test") as client:
            result = await client.get_usage()
        assert result.plan == "pro"

    @respx.mock
    async def test_get_image(self):
        respx.get(f"{BASE}/v1/image/img_3").mock(
            return_value=httpx.Response(200, json=META_RESP)
        )
        async with AsyncRendshotClient(api_key="rs_live_test") as client:
            result = await client.get_image("img_3")
        assert result.expires_at == "2026-03-27"

    @respx.mock
    async def test_error_handling(self):
        respx.post(f"{BASE}/v1/image").mock(
            return_value=httpx.Response(
                401,
                json={"error": {"code": "UNAUTHORIZED", "message": "Invalid API key", "status": 401}},
            )
        )
        async with AsyncRendshotClient(api_key="bad_key") as client:
            with pytest.raises(RendshotError) as exc_info:
                await client.render_image(RenderImageOptions(html="<p>x</p>"))
        assert exc_info.value.code == "UNAUTHORIZED"

    @respx.mock
    async def test_non_json_error_response(self):
        respx.post(f"{BASE}/v1/image").mock(
            return_value=httpx.Response(502, text="Bad Gateway")
        )
        async with AsyncRendshotClient(api_key="rs_live_test") as client:
            with pytest.raises(RendshotError) as exc_info:
                await client.render_image(RenderImageOptions(html="<p>x</p>"))
        assert exc_info.value.code == "UNKNOWN_ERROR"
        assert exc_info.value.status == 502

    @respx.mock
    async def test_custom_base_url(self):
        route = respx.get("https://custom.api.com/v1/usage").mock(
            return_value=httpx.Response(200, json=USAGE_RESP)
        )
        async with AsyncRendshotClient(api_key="rs_live_test", base_url="https://custom.api.com/") as client:
            await client.get_usage()
        assert route.called


# ---------------------------------------------------------------------------
# Types
# ---------------------------------------------------------------------------


class TestTypes:
    def test_render_options_to_dict_minimal(self):
        opts = RenderImageOptions(html="<h1>hi</h1>")
        d = opts.to_dict()
        assert d == {"html": "<h1>hi</h1>"}

    def test_render_options_to_dict_full(self):
        opts = RenderImageOptions(
            html="<h1>hi</h1>",
            css="h1 { color: red; }",
            width=800,
            height=600,
            format="jpg",
            quality=85,
            device_scale=2,
            fonts=["Inter"],
            timeout=5000,
        )
        d = opts.to_dict()
        assert d["css"] == "h1 { color: red; }"
        assert d["deviceScale"] == 2
        assert d["fonts"] == ["Inter"]
        assert "device_scale" not in d

    def test_screenshot_options_to_dict(self):
        opts = ScreenshotUrlOptions(
            url="https://example.com",
            full_page=True,
            device_scale=3,
        )
        d = opts.to_dict()
        assert d["fullPage"] is True
        assert d["deviceScale"] == 3
        assert "full_page" not in d

    def test_image_result_from_dict(self):
        result = ImageResult.from_dict(IMAGE_RESP)
        assert result.image_id == "img_1"
        assert result.created_at == "2026-03-20"

    def test_image_metadata_from_dict(self):
        meta = ImageMetadata.from_dict(META_RESP)
        assert meta.expires_at == "2026-03-27"
        assert meta.image_id == "img_3"

    def test_usage_result_from_dict(self):
        usage = UsageResult.from_dict(USAGE_RESP)
        assert usage.month == "2026-03"
        assert usage.count == 42
