"""Tests for the HTTP transport layer."""

from __future__ import annotations

from unittest.mock import AsyncMock, Mock, patch

import pytest
import httpx

from undetecta._transport import (
    TransportConfig,
    build_headers,
    build_url,
    calculate_delay,
    request_with_retry,
)
from undetecta.errors import HttpError, is_retryable_status, TimeoutError


@pytest.mark.asyncio
async def test_build_url():
    """Test URL building."""
    assert build_url("https://api.test.com", "/v1/scrape") == "https://api.test.com/v1/scrape"
    assert build_url("https://api.test.com/", "v1/scrape") == "https://api.test.com/v1/scrape"
    assert build_url("https://api.test.com/", "/v1/scrape") == "https://api.test.com/v1/scrape"
    assert build_url("https://api.test.com", "v1/scrape") == "https://api.test.com/v1/scrape"


def test_build_headers():
    """Test header building."""
    headers = build_headers("test-api-key")
    assert headers["Content-Type"] == "application/json"
    assert headers["x-api-key"] == "test-api-key"
    assert "User-Agent" not in headers

    headers_with_ua = build_headers("test-api-key", "custom-agent/1.0")
    assert headers_with_ua["User-Agent"] == "custom-agent/1.0"

    headers_with_extra = build_headers("test-api-key", additional_headers={"X-Custom": "value"})
    assert headers_with_extra["X-Custom"] == "value"


def test_calculate_delay():
    """Test exponential backoff with jitter."""
    # First attempt: ~1000ms + jitter (returned in seconds)
    delay = calculate_delay(0)
    assert 1.0 <= delay < 1.4  # 1000ms + up to 30% jitter = 1.0-1.3 seconds

    # Second attempt: ~2000ms + jitter
    delay = calculate_delay(1)
    assert 2.0 <= delay < 2.7  # 2000ms + up to 30% jitter = 2.0-2.6 seconds

    # Should cap at MAX_DELAY_MS
    delay = calculate_delay(20)
    assert 10.0 <= delay < 13.5  # 10000ms + up to 30% jitter = 10.0-13.0 seconds


@pytest.mark.asyncio
async def test_request_with_retry_success():
    """Test successful request without retries."""
    mock_client = Mock(spec=httpx.AsyncClient)
    mock_response = Mock()
    mock_response.is_success = True
    mock_response.status_code = 200
    mock_response.json.return_value = {"success": True, "data": {"result": "ok"}}
    mock_response.headers = {"content-type": "application/json"}

    mock_client.request = AsyncMock(return_value=mock_response)

    config = TransportConfig(
        base_url="https://api.test.com",
        api_key="test-key",
        timeout=60000,
        max_retries=3,
    )

    result = await request_with_retry(
        client=mock_client,
        method="POST",
        url="https://api.test.com/v1/test",
        config=config,
        json_data={"test": "data"},
    )

    assert result == {"result": "ok"}
    mock_client.request.assert_called_once()


@pytest.mark.asyncio
async def test_request_with_retry_unwrapped_response():
    """Test request with unwrapped response format."""
    mock_client = Mock(spec=httpx.AsyncClient)
    mock_response = Mock()
    mock_response.is_success = True
    mock_response.status_code = 200
    mock_response.json.return_value = {"status": "ok", "timestamp": "2024-01-01T00:00:00Z"}
    mock_response.headers = {"content-type": "application/json"}

    mock_client.request = AsyncMock(return_value=mock_response)

    config = TransportConfig(
        base_url="https://api.test.com",
        api_key="test-key",
        timeout=60000,
        max_retries=3,
    )

    result = await request_with_retry(
        client=mock_client,
        method="POST",
        url="https://api.test.com/v1/health",
        config=config,
    )

    assert result == {"status": "ok", "timestamp": "2024-01-01T00:00:00Z"}


@pytest.mark.asyncio
async def test_request_with_retry_http_error_no_retry():
    """Test non-retryable HTTP error."""
    mock_client = Mock(spec=httpx.AsyncClient)
    mock_response = Mock()
    mock_response.is_success = False
    mock_response.status_code = 404
    mock_response.reason_phrase = "Not Found"
    mock_response.headers = {"content-type": "application/json"}
    mock_response.json.return_value = {"error": {"code": "NOT_FOUND", "message": "Not found"}}

    mock_client.request = AsyncMock(return_value=mock_response)

    config = TransportConfig(
        base_url="https://api.test.com",
        api_key="test-key",
        timeout=60000,
        max_retries=3,
    )

    with pytest.raises(HttpError) as exc_info:
        await request_with_retry(
            client=mock_client,
            method="POST",
            url="https://api.test.com/v1/test",
            config=config,
        )

    assert exc_info.value.status_code == 404
    mock_client.request.assert_called_once()  # No retries for 404


@pytest.mark.asyncio
async def test_request_with_retry_http_error_with_retry():
    """Test retryable HTTP error."""
    mock_client = Mock(spec=httpx.AsyncClient)

    # First call fails with 503, second succeeds
    mock_response_error = Mock()
    mock_response_error.is_success = False
    mock_response_error.status_code = 503
    mock_response_error.reason_phrase = "Service Unavailable"
    mock_response_error.headers = {"content-type": "application/json"}
    mock_response_error.json.return_value = {
        "error": {"code": "SERVICE_UNAVAILABLE", "message": "Service unavailable"}
    }

    mock_response_success = Mock()
    mock_response_success.is_success = True
    mock_response_success.status_code = 200
    mock_response_success.json.return_value = {"success": True, "data": {"result": "ok"}}
    mock_response_success.headers = {"content-type": "application/json"}

    mock_client.request = AsyncMock(side_effect=[mock_response_error, mock_response_success])

    config = TransportConfig(
        base_url="https://api.test.com",
        api_key="test-key",
        timeout=60000,
        max_retries=3,
    )

    result = await request_with_retry(
        client=mock_client,
        method="POST",
        url="https://api.test.com/v1/test",
        config=config,
    )

    assert result == {"result": "ok"}
    assert mock_client.request.call_count == 2


@pytest.mark.asyncio
async def test_request_with_retry_timeout():
    """Test timeout with retry."""
    mock_client = Mock(spec=httpx.AsyncClient)
    mock_client.request = AsyncMock(side_effect=httpx.TimeoutException("Request timeout"))

    config = TransportConfig(
        base_url="https://api.test.com",
        api_key="test-key",
        timeout=60000,
        max_retries=1,
    )

    with pytest.raises(TimeoutError):
        await request_with_retry(
            client=mock_client,
            method="POST",
            url="https://api.test.com/v1/test",
            config=config,
        )

    assert mock_client.request.call_count == 2  # Initial + 1 retry


@pytest.mark.asyncio
async def test_request_with_retry_network_error():
    """Test network error with retry."""
    mock_client = Mock(spec=httpx.AsyncClient)
    mock_client.request = AsyncMock(side_effect=httpx.NetworkError("Connection failed"))

    config = TransportConfig(
        base_url="https://api.test.com",
        api_key="test-key",
        timeout=60000,
        max_retries=1,
    )

    with pytest.raises(HttpError) as exc_info:
        await request_with_retry(
            client=mock_client,
            method="POST",
            url="https://api.test.com/v1/test",
            config=config,
        )

    assert "Network error" in str(exc_info.value)
    assert mock_client.request.call_count == 2  # Initial + 1 retry


@pytest.mark.asyncio
async def test_request_with_retry_max_retries_exceeded():
    """Test that max retries is respected."""
    mock_client = Mock(spec=httpx.AsyncClient)
    mock_response = Mock()
    mock_response.is_success = False
    mock_response.status_code = 503
    mock_response.reason_phrase = "Service Unavailable"
    mock_response.headers = {"content-type": "application/json"}
    mock_response.json.return_value = {
        "error": {"code": "SERVICE_UNAVAILABLE", "message": "Service unavailable"}
    }

    mock_client.request = AsyncMock(return_value=mock_response)

    config = TransportConfig(
        base_url="https://api.test.com",
        api_key="test-key",
        timeout=60000,
        max_retries=2,
    )

    with pytest.raises(HttpError) as exc_info:
        await request_with_retry(
            client=mock_client,
            method="POST",
            url="https://api.test.com/v1/test",
            config=config,
        )

    assert exc_info.value.status_code == 503
    assert mock_client.request.call_count == 3  # Initial + 2 retries


@pytest.mark.asyncio
async def test_request_with_get_method():
    """Test GET request (no JSON body)."""
    mock_client = Mock(spec=httpx.AsyncClient)
    mock_response = Mock()
    mock_response.is_success = True
    mock_response.status_code = 200
    mock_response.json.return_value = {"success": True, "data": {"status": "ok"}}
    mock_response.headers = {"content-type": "application/json"}

    mock_client.request = AsyncMock(return_value=mock_response)

    config = TransportConfig(
        base_url="https://api.test.com",
        api_key="test-key",
        timeout=60000,
        max_retries=0,
    )

    result = await request_with_retry(
        client=mock_client,
        method="GET",
        url="https://api.test.com/v1/health",
        config=config,
    )

    assert result == {"status": "ok"}
    # Verify json parameter was None for GET request
    call_kwargs = mock_client.request.call_args.kwargs
    assert call_kwargs.get("json") is None


@pytest.mark.asyncio
async def test_request_with_wrapped_error_response():
    """Test request that returns success=False wrapped response."""
    mock_client = Mock(spec=httpx.AsyncClient)
    mock_response = Mock()
    mock_response.is_success = True
    mock_response.status_code = 200
    mock_response.json.return_value = {
        "success": False,
        "error": {"code": "VALIDATION_ERROR", "message": "Invalid input"}
    }
    mock_response.headers = {"content-type": "application/json"}

    mock_client.request = AsyncMock(return_value=mock_response)

    config = TransportConfig(
        base_url="https://api.test.com",
        api_key="test-key",
        timeout=60000,
        max_retries=0,
    )

    with pytest.raises(HttpError) as exc_info:
        await request_with_retry(
            client=mock_client,
            method="POST",
            url="https://api.test.com/v1/test",
            config=config,
        )

    assert exc_info.value.status_code == 200
    assert "Invalid input" in str(exc_info.value)
