import pytest
import httpx
import respx

from ragent_oss import (
    RagentOssClient,
    PresignRequest,
    SignRequest,
    DeleteRequest,
    AuthenticationError,
    ValidationError,
    RagentOssError,
    NetworkError,
)


@pytest.fixture
def client(base_url, api_key):
    return RagentOssClient(base_url, api_key)


# ---- health ----


@pytest.mark.asyncio
async def test_health(client, base_url):
    with respx.mock:
        respx.get(f"{base_url}/api/health").mock(
            return_value=httpx.Response(200, json={"status": "ok", "storage": "rustfs"})
        )
        result = await client.health()

    assert result.status == "ok"
    assert result.storage == "rustfs"


@pytest.mark.asyncio
async def test_health_no_auth_header(client, base_url):
    with respx.mock:
        route = respx.get(f"{base_url}/api/health").mock(
            return_value=httpx.Response(200, json={"status": "ok", "storage": "cos"})
        )
        await client.health()

    # Verify no X-API-Key header was sent
    assert "X-API-Key" not in route.calls[0].request.headers


# ---- presign ----


@pytest.mark.asyncio
async def test_presign(client, base_url):
    response_data = {
        "objectKey": "knowledge/202602/abc.pdf",
        "uploadUrl": "https://s3.example.com/presigned",
        "headers": {"Content-Type": "application/pdf"},
    }
    with respx.mock:
        route = respx.post(f"{base_url}/api/presign").mock(
            return_value=httpx.Response(200, json=response_data)
        )
        result = await client.presign(
            PresignRequest("test.pdf", "application/pdf", "knowledge")
        )

    assert result.object_key == "knowledge/202602/abc.pdf"
    assert result.upload_url == "https://s3.example.com/presigned"
    assert result.headers == {"Content-Type": "application/pdf"}

    # Verify request body uses camelCase
    request_body = route.calls[0].request.content
    import json

    body = json.loads(request_body)
    assert body == {
        "filename": "test.pdf",
        "contentType": "application/pdf",
        "category": "knowledge",
    }


@pytest.mark.asyncio
async def test_presign_sends_api_key(client, base_url, api_key):
    with respx.mock:
        route = respx.post(f"{base_url}/api/presign").mock(
            return_value=httpx.Response(
                200,
                json={
                    "objectKey": "k/202602/a.pdf",
                    "uploadUrl": "https://s3/put",
                    "headers": {},
                },
            )
        )
        await client.presign(PresignRequest("f.pdf", "application/pdf", "k"))

    assert route.calls[0].request.headers["X-API-Key"] == api_key


# ---- sign ----


@pytest.mark.asyncio
async def test_sign(client, base_url):
    with respx.mock:
        respx.post(f"{base_url}/api/sign").mock(
            return_value=httpx.Response(200, json={"url": "https://s3/signed"})
        )
        result = await client.sign(SignRequest("knowledge/202602/abc.pdf"))

    assert result.url == "https://s3/signed"


@pytest.mark.asyncio
async def test_sign_with_expires_in(client, base_url):
    with respx.mock:
        route = respx.post(f"{base_url}/api/sign").mock(
            return_value=httpx.Response(200, json={"url": "https://s3/signed"})
        )
        await client.sign(SignRequest("a/b/c.pdf", expires_in=3600))

    import json

    body = json.loads(route.calls[0].request.content)
    assert body == {"objectKey": "a/b/c.pdf", "expiresIn": 3600}


@pytest.mark.asyncio
async def test_sign_omits_expires_in_when_none(client, base_url):
    with respx.mock:
        route = respx.post(f"{base_url}/api/sign").mock(
            return_value=httpx.Response(200, json={"url": "https://s3/signed"})
        )
        await client.sign(SignRequest("a/b/c.pdf"))

    import json

    body = json.loads(route.calls[0].request.content)
    assert body == {"objectKey": "a/b/c.pdf"}
    assert "expiresIn" not in body


# ---- delete ----


@pytest.mark.asyncio
async def test_delete(client, base_url):
    with respx.mock:
        respx.post(f"{base_url}/api/delete").mock(
            return_value=httpx.Response(200, json={"success": True})
        )
        result = await client.delete(DeleteRequest("knowledge/202602/abc.pdf"))

    assert result.success is True


# ---- error handling ----


@pytest.mark.asyncio
async def test_authentication_error(client, base_url):
    with respx.mock:
        respx.post(f"{base_url}/api/presign").mock(
            return_value=httpx.Response(401, json={"error": "Unauthorized"})
        )
        with pytest.raises(AuthenticationError) as exc_info:
            await client.presign(PresignRequest("f.pdf", "application/pdf", "k"))

    assert exc_info.value.status_code == 401
    assert exc_info.value.body == {"error": "Unauthorized"}


@pytest.mark.asyncio
async def test_validation_error(client, base_url):
    with respx.mock:
        respx.post(f"{base_url}/api/presign").mock(
            return_value=httpx.Response(422, json={"message": "Required"})
        )
        with pytest.raises(ValidationError) as exc_info:
            await client.presign(PresignRequest("", "", ""))

    assert exc_info.value.status_code == 422


@pytest.mark.asyncio
async def test_other_http_error(client, base_url):
    with respx.mock:
        respx.post(f"{base_url}/api/presign").mock(
            return_value=httpx.Response(500, json={"error": "Internal Server Error"})
        )
        with pytest.raises(RagentOssError) as exc_info:
            await client.presign(PresignRequest("f.pdf", "application/pdf", "k"))

    assert exc_info.value.status_code == 500


@pytest.mark.asyncio
async def test_network_error(client, base_url):
    with respx.mock:
        respx.get(f"{base_url}/api/health").mock(
            side_effect=httpx.ConnectError("Connection refused")
        )
        with pytest.raises(NetworkError):
            await client.health()


# ---- convenience methods ----


@pytest.mark.asyncio
async def test_upload(client, base_url):
    with respx.mock:
        respx.post(f"{base_url}/api/presign").mock(
            return_value=httpx.Response(
                200,
                json={
                    "objectKey": "knowledge/202602/abc.pdf",
                    "uploadUrl": "https://s3.example.com/put",
                    "headers": {"Content-Type": "application/pdf"},
                },
            )
        )
        respx.put("https://s3.example.com/put").mock(
            return_value=httpx.Response(200)
        )

        object_key = await client.upload(
            "test.pdf", b"content", "application/pdf", "knowledge"
        )

    assert object_key == "knowledge/202602/abc.pdf"


@pytest.mark.asyncio
async def test_download(client, base_url):
    with respx.mock:
        respx.post(f"{base_url}/api/sign").mock(
            return_value=httpx.Response(
                200, json={"url": "https://s3.example.com/get"}
            )
        )
        respx.get("https://s3.example.com/get").mock(
            return_value=httpx.Response(200, content=b"file-data")
        )

        content = await client.download("knowledge/202602/abc.pdf")

    assert content == b"file-data"


@pytest.mark.asyncio
async def test_download_to_temp(client, base_url):
    with respx.mock:
        respx.post(f"{base_url}/api/sign").mock(
            return_value=httpx.Response(
                200, json={"url": "https://s3.example.com/get"}
            )
        )
        respx.get("https://s3.example.com/get").mock(
            return_value=httpx.Response(200, content=b"file-data")
        )

        path = await client.download_to_temp("knowledge/202602/abc.pdf")

    assert path.exists()
    assert path.read_bytes() == b"file-data"
    assert path.suffix == ".pdf"
    # Cleanup
    path.unlink()


# ---- base url handling ----


@pytest.mark.asyncio
async def test_strips_trailing_slash(base_url, api_key):
    client = RagentOssClient(f"{base_url}/", api_key)
    with respx.mock:
        respx.get(f"{base_url}/api/health").mock(
            return_value=httpx.Response(200, json={"status": "ok", "storage": "cos"})
        )
        result = await client.health()

    assert result.status == "ok"


# ---- context manager ----


@pytest.mark.asyncio
async def test_context_manager(base_url, api_key):
    async with RagentOssClient(base_url, api_key) as client:
        with respx.mock:
            respx.get(f"{base_url}/api/health").mock(
                return_value=httpx.Response(
                    200, json={"status": "ok", "storage": "cos"}
                )
            )
            result = await client.health()
            assert result.status == "ok"
