# This file was auto-generated by Fern from our API Definition.

from typing import Any, Dict, Tuple, cast
from unittest.mock import AsyncMock, MagicMock, patch

import httpx
import pytest

from auth0.management.core.http_client import (
    AsyncHttpClient,
    HttpClient,
    _build_url,
    _should_retry,
    get_request_body,
    remove_none_from_dict,
)
from auth0.management.core.request_options import RequestOptions


# Stub clients for testing HttpClient and AsyncHttpClient
class _DummySyncClient:
    """A minimal stub for httpx.Client that records request arguments."""

    def __init__(self) -> None:
        self.last_request_kwargs: Dict[str, Any] = {}

    def request(self, **kwargs: Any) -> "_DummyResponse":
        self.last_request_kwargs = kwargs
        return _DummyResponse()


class _DummyAsyncClient:
    """A minimal stub for httpx.AsyncClient that records request arguments."""

    def __init__(self) -> None:
        self.last_request_kwargs: Dict[str, Any] = {}

    async def request(self, **kwargs: Any) -> "_DummyResponse":
        self.last_request_kwargs = kwargs
        return _DummyResponse()


class _DummyResponse:
    """A minimal stub for httpx.Response."""

    status_code = 200
    headers: Dict[str, str] = {}


def get_request_options() -> RequestOptions:
    return {"additional_body_parameters": {"see you": "later"}}


def get_request_options_with_none() -> RequestOptions:
    return {"additional_body_parameters": {"see you": "later", "optional": None}}


def test_get_json_request_body() -> None:
    json_body, data_body = get_request_body(json={"hello": "world"}, data=None, request_options=None, omit=None)
    assert json_body == {"hello": "world"}
    assert data_body is None

    json_body_extras, data_body_extras = get_request_body(
        json={"goodbye": "world"}, data=None, request_options=get_request_options(), omit=None
    )

    assert json_body_extras == {"goodbye": "world", "see you": "later"}
    assert data_body_extras is None


def test_get_files_request_body() -> None:
    json_body, data_body = get_request_body(json=None, data={"hello": "world"}, request_options=None, omit=None)
    assert data_body == {"hello": "world"}
    assert json_body is None

    json_body_extras, data_body_extras = get_request_body(
        json=None, data={"goodbye": "world"}, request_options=get_request_options(), omit=None
    )

    assert data_body_extras == {"goodbye": "world", "see you": "later"}
    assert json_body_extras is None


def test_get_none_request_body() -> None:
    json_body, data_body = get_request_body(json=None, data=None, request_options=None, omit=None)
    assert data_body is None
    assert json_body is None

    json_body_extras, data_body_extras = get_request_body(
        json=None, data=None, request_options=get_request_options(), omit=None
    )

    assert json_body_extras == {"see you": "later"}
    assert data_body_extras is None


def test_get_empty_json_request_body() -> None:
    """Test that implicit empty bodies (json=None) are collapsed to None."""
    unrelated_request_options: RequestOptions = {"max_retries": 3}
    json_body, data_body = get_request_body(json=None, data=None, request_options=unrelated_request_options, omit=None)
    assert json_body is None
    assert data_body is None


def test_explicit_empty_json_body_is_preserved() -> None:
    """Test that explicit empty bodies (json={}) are preserved and sent as {}.

    This is important for endpoints where the request body is required but all
    fields are optional. The server expects valid JSON ({}) not an empty body.
    """
    unrelated_request_options: RequestOptions = {"max_retries": 3}

    # Explicit json={} should be preserved
    json_body, data_body = get_request_body(json={}, data=None, request_options=unrelated_request_options, omit=None)
    assert json_body == {}
    assert data_body is None

    # Explicit data={} should also be preserved
    json_body2, data_body2 = get_request_body(json=None, data={}, request_options=unrelated_request_options, omit=None)
    assert json_body2 is None
    assert data_body2 == {}


def test_json_body_preserves_none_values() -> None:
    """Test that JSON bodies preserve None values (they become JSON null)."""
    json_body, data_body = get_request_body(
        json={"hello": "world", "optional": None}, data=None, request_options=None, omit=None
    )
    # JSON bodies should preserve None values
    assert json_body == {"hello": "world", "optional": None}
    assert data_body is None


def test_data_body_preserves_none_values_without_multipart() -> None:
    """Test that data bodies preserve None values when not using multipart.

    The filtering of None values happens in HttpClient.request/stream methods,
    not in get_request_body. This test verifies get_request_body doesn't filter None.
    """
    json_body, data_body = get_request_body(
        json=None, data={"hello": "world", "optional": None}, request_options=None, omit=None
    )
    # get_request_body should preserve None values in data body
    # The filtering happens later in HttpClient.request when multipart is detected
    assert data_body == {"hello": "world", "optional": None}
    assert json_body is None


def test_remove_none_from_dict_filters_none_values() -> None:
    """Test that remove_none_from_dict correctly filters out None values."""
    original = {"hello": "world", "optional": None, "another": "value", "also_none": None}
    filtered = remove_none_from_dict(original)
    assert filtered == {"hello": "world", "another": "value"}
    # Original should not be modified
    assert original == {"hello": "world", "optional": None, "another": "value", "also_none": None}


def test_remove_none_from_dict_empty_dict() -> None:
    """Test that remove_none_from_dict handles empty dict."""
    assert remove_none_from_dict({}) == {}


def test_remove_none_from_dict_all_none() -> None:
    """Test that remove_none_from_dict handles dict with all None values."""
    assert remove_none_from_dict({"a": None, "b": None}) == {}


def test_http_client_does_not_pass_empty_params_list() -> None:
    """Test that HttpClient passes params=None when params are empty.

    This prevents httpx from stripping existing query parameters from the URL,
    which happens when params=[] or params={} is passed.
    """
    dummy_client = _DummySyncClient()
    http_client = HttpClient(
        httpx_client=dummy_client,  # type: ignore[arg-type]
        base_timeout=lambda: None,
        base_headers=lambda: {},
        base_url=lambda: "https://example.com",
    )

    # Use a path with query params (e.g., pagination cursor URL)
    http_client.request(
        path="resource?after=123",
        method="GET",
        params=None,
        request_options=None,
    )

    # We care that httpx receives params=None, not [] or {}
    assert "params" in dummy_client.last_request_kwargs
    assert dummy_client.last_request_kwargs["params"] is None

    # Verify the query string in the URL is preserved
    url = str(dummy_client.last_request_kwargs["url"])
    assert "after=123" in url, f"Expected query param 'after=123' in URL, got: {url}"


def test_http_client_passes_encoded_params_when_present() -> None:
    """Test that HttpClient passes encoded params when params are provided."""
    dummy_client = _DummySyncClient()
    http_client = HttpClient(
        httpx_client=dummy_client,  # type: ignore[arg-type]
        base_timeout=lambda: None,
        base_headers=lambda: {},
        base_url=lambda: "https://example.com/resource",
    )

    http_client.request(
        path="",
        method="GET",
        params={"after": "456"},
        request_options=None,
    )

    params = dummy_client.last_request_kwargs["params"]
    # For a simple dict, encode_query should give a single (key, value) tuple
    assert params == [("after", "456")]


@pytest.mark.asyncio
async def test_async_http_client_does_not_pass_empty_params_list() -> None:
    """Test that AsyncHttpClient passes params=None when params are empty.

    This prevents httpx from stripping existing query parameters from the URL,
    which happens when params=[] or params={} is passed.
    """
    dummy_client = _DummyAsyncClient()
    http_client = AsyncHttpClient(
        httpx_client=dummy_client,  # type: ignore[arg-type]
        base_timeout=lambda: None,
        base_headers=lambda: {},
        base_url=lambda: "https://example.com",
        async_base_headers=None,
    )

    # Use a path with query params (e.g., pagination cursor URL)
    await http_client.request(
        path="resource?after=123",
        method="GET",
        params=None,
        request_options=None,
    )

    # We care that httpx receives params=None, not [] or {}
    assert "params" in dummy_client.last_request_kwargs
    assert dummy_client.last_request_kwargs["params"] is None

    # Verify the query string in the URL is preserved
    url = str(dummy_client.last_request_kwargs["url"])
    assert "after=123" in url, f"Expected query param 'after=123' in URL, got: {url}"


@pytest.mark.asyncio
async def test_async_http_client_passes_encoded_params_when_present() -> None:
    """Test that AsyncHttpClient passes encoded params when params are provided."""
    dummy_client = _DummyAsyncClient()
    http_client = AsyncHttpClient(
        httpx_client=dummy_client,  # type: ignore[arg-type]
        base_timeout=lambda: None,
        base_headers=lambda: {},
        base_url=lambda: "https://example.com/resource",
        async_base_headers=None,
    )

    await http_client.request(
        path="",
        method="GET",
        params={"after": "456"},
        request_options=None,
    )

    params = dummy_client.last_request_kwargs["params"]
    # For a simple dict, encode_query should give a single (key, value) tuple
    assert params == [("after", "456")]


def test_basic_url_joining() -> None:
    """Test basic URL joining with a simple base URL and path."""
    result = _build_url("https://api.example.com", "/users")
    assert result == "https://api.example.com/users"


def test_basic_url_joining_trailing_slash() -> None:
    """Test basic URL joining with a simple base URL and path."""
    result = _build_url("https://api.example.com/", "/users")
    assert result == "https://api.example.com/users"


def test_preserves_base_url_path_prefix() -> None:
    """Test that path prefixes in base URL are preserved.

    This is the critical bug fix - urllib.parse.urljoin() would strip
    the path prefix when the path starts with '/'.
    """
    result = _build_url("https://cloud.example.com/org/tenant/api", "/users")
    assert result == "https://cloud.example.com/org/tenant/api/users"


def test_preserves_base_url_path_prefix_trailing_slash() -> None:
    """Test that path prefixes in base URL are preserved."""
    result = _build_url("https://cloud.example.com/org/tenant/api/", "/users")
    assert result == "https://cloud.example.com/org/tenant/api/users"


# ---------------------------------------------------------------------------
# Connection error retry tests
# ---------------------------------------------------------------------------


def _make_sync_http_client(mock_client: Any) -> HttpClient:
    return HttpClient(
        httpx_client=mock_client,  # type: ignore[arg-type]
        base_timeout=lambda: None,
        base_headers=lambda: {},
        base_url=lambda: "https://example.com",
    )


def _make_async_http_client(mock_client: Any) -> AsyncHttpClient:
    return AsyncHttpClient(
        httpx_client=mock_client,  # type: ignore[arg-type]
        base_timeout=lambda: None,
        base_headers=lambda: {},
        base_url=lambda: "https://example.com",
        async_base_headers=None,
    )


@patch("auth0.management.core.http_client.time.sleep", return_value=None)
def test_sync_retries_on_connect_error(mock_sleep: MagicMock) -> None:
    """Sync: connection error retries on httpx.ConnectError."""
    mock_client = MagicMock()
    mock_client.request.side_effect = [
        httpx.ConnectError("connection failed"),
        _DummyResponse(),
    ]
    http_client = _make_sync_http_client(mock_client)

    response = http_client.request(path="/test", method="GET")

    assert response.status_code == 200
    assert mock_client.request.call_count == 2
    mock_sleep.assert_called_once()


@patch("auth0.management.core.http_client.time.sleep", return_value=None)
def test_sync_retries_on_remote_protocol_error(mock_sleep: MagicMock) -> None:
    """Sync: connection error retries on httpx.RemoteProtocolError."""
    mock_client = MagicMock()
    mock_client.request.side_effect = [
        httpx.RemoteProtocolError("Remote end closed connection without response"),
        _DummyResponse(),
    ]
    http_client = _make_sync_http_client(mock_client)

    response = http_client.request(path="/test", method="GET")

    assert response.status_code == 200
    assert mock_client.request.call_count == 2
    mock_sleep.assert_called_once()


@patch("auth0.management.core.http_client.time.sleep", return_value=None)
def test_sync_connection_error_exhausts_retries(mock_sleep: MagicMock) -> None:
    """Sync: connection error exhausts retries then raises."""
    mock_client = MagicMock()
    mock_client.request.side_effect = httpx.ConnectError("connection failed")
    http_client = _make_sync_http_client(mock_client)

    with pytest.raises(httpx.ConnectError):
        http_client.request(
            path="/test",
            method="GET",
            request_options={"max_retries": 2},
        )

    # 1 initial + 2 retries = 3 total attempts
    assert mock_client.request.call_count == 3
    assert mock_sleep.call_count == 2


@patch("auth0.management.core.http_client.time.sleep", return_value=None)
def test_sync_connection_error_respects_max_retries_zero(mock_sleep: MagicMock) -> None:
    """Sync: connection error respects max_retries=0."""
    mock_client = MagicMock()
    mock_client.request.side_effect = httpx.ConnectError("connection failed")
    http_client = _make_sync_http_client(mock_client)

    with pytest.raises(httpx.ConnectError):
        http_client.request(
            path="/test",
            method="GET",
            request_options={"max_retries": 0},
        )

    # No retries, just the initial attempt
    assert mock_client.request.call_count == 1
    mock_sleep.assert_not_called()


@pytest.mark.asyncio
@patch("auth0.management.core.http_client.asyncio.sleep", new_callable=AsyncMock)
async def test_async_retries_on_connect_error(mock_sleep: AsyncMock) -> None:
    """Async: connection error retries on httpx.ConnectError."""
    mock_client = MagicMock()
    mock_client.request = AsyncMock(
        side_effect=[
            httpx.ConnectError("connection failed"),
            _DummyResponse(),
        ]
    )
    http_client = _make_async_http_client(mock_client)

    response = await http_client.request(path="/test", method="GET")

    assert response.status_code == 200
    assert mock_client.request.call_count == 2
    mock_sleep.assert_called_once()


@pytest.mark.asyncio
@patch("auth0.management.core.http_client.asyncio.sleep", new_callable=AsyncMock)
async def test_async_retries_on_remote_protocol_error(mock_sleep: AsyncMock) -> None:
    """Async: connection error retries on httpx.RemoteProtocolError."""
    mock_client = MagicMock()
    mock_client.request = AsyncMock(
        side_effect=[
            httpx.RemoteProtocolError("Remote end closed connection without response"),
            _DummyResponse(),
        ]
    )
    http_client = _make_async_http_client(mock_client)

    response = await http_client.request(path="/test", method="GET")

    assert response.status_code == 200
    assert mock_client.request.call_count == 2
    mock_sleep.assert_called_once()


@pytest.mark.asyncio
@patch("auth0.management.core.http_client.asyncio.sleep", new_callable=AsyncMock)
async def test_async_connection_error_exhausts_retries(mock_sleep: AsyncMock) -> None:
    """Async: connection error exhausts retries then raises."""
    mock_client = MagicMock()
    mock_client.request = AsyncMock(side_effect=httpx.ConnectError("connection failed"))
    http_client = _make_async_http_client(mock_client)

    with pytest.raises(httpx.ConnectError):
        await http_client.request(
            path="/test",
            method="GET",
            request_options={"max_retries": 2},
        )

    # 1 initial + 2 retries = 3 total attempts
    assert mock_client.request.call_count == 3
    assert mock_sleep.call_count == 2


# ---------------------------------------------------------------------------
# base_max_retries constructor parameter tests
# ---------------------------------------------------------------------------


def test_sync_http_client_default_base_max_retries() -> None:
    """HttpClient defaults to base_max_retries=2."""
    http_client = HttpClient(
        httpx_client=MagicMock(),  # type: ignore[arg-type]
        base_timeout=lambda: None,
        base_headers=lambda: {},
    )
    assert http_client.base_max_retries == 2


def test_async_http_client_default_base_max_retries() -> None:
    """AsyncHttpClient defaults to base_max_retries=2."""
    http_client = AsyncHttpClient(
        httpx_client=MagicMock(),  # type: ignore[arg-type]
        base_timeout=lambda: None,
        base_headers=lambda: {},
    )
    assert http_client.base_max_retries == 2


def test_sync_http_client_custom_base_max_retries() -> None:
    """HttpClient accepts a custom base_max_retries value."""
    http_client = HttpClient(
        httpx_client=MagicMock(),  # type: ignore[arg-type]
        base_timeout=lambda: None,
        base_headers=lambda: {},
        base_max_retries=5,
    )
    assert http_client.base_max_retries == 5


def test_async_http_client_custom_base_max_retries() -> None:
    """AsyncHttpClient accepts a custom base_max_retries value."""
    http_client = AsyncHttpClient(
        httpx_client=MagicMock(),  # type: ignore[arg-type]
        base_timeout=lambda: None,
        base_headers=lambda: {},
        base_max_retries=5,
    )
    assert http_client.base_max_retries == 5


@patch("auth0.management.core.http_client.time.sleep", return_value=None)
def test_sync_base_max_retries_zero_disables_retries(mock_sleep: MagicMock) -> None:
    """Sync: base_max_retries=0 disables retries when no request_options override."""
    mock_client = MagicMock()
    mock_client.request.side_effect = httpx.ConnectError("connection failed")
    http_client = HttpClient(
        httpx_client=mock_client,  # type: ignore[arg-type]
        base_timeout=lambda: None,
        base_headers=lambda: {},
        base_url=lambda: "https://example.com",
        base_max_retries=0,
    )

    with pytest.raises(httpx.ConnectError):
        http_client.request(path="/test", method="GET")

    # No retries, just the initial attempt
    assert mock_client.request.call_count == 1
    mock_sleep.assert_not_called()


@pytest.mark.asyncio
@patch("auth0.management.core.http_client.asyncio.sleep", new_callable=AsyncMock)
async def test_async_base_max_retries_zero_disables_retries(mock_sleep: AsyncMock) -> None:
    """Async: base_max_retries=0 disables retries when no request_options override."""
    mock_client = MagicMock()
    mock_client.request = AsyncMock(side_effect=httpx.ConnectError("connection failed"))
    http_client = AsyncHttpClient(
        httpx_client=mock_client,  # type: ignore[arg-type]
        base_timeout=lambda: None,
        base_headers=lambda: {},
        base_url=lambda: "https://example.com",
        base_max_retries=0,
    )

    with pytest.raises(httpx.ConnectError):
        await http_client.request(path="/test", method="GET")

    # No retries, just the initial attempt
    assert mock_client.request.call_count == 1
    mock_sleep.assert_not_called()


@patch("auth0.management.core.http_client.time.sleep", return_value=None)
def test_sync_request_options_override_base_max_retries(mock_sleep: MagicMock) -> None:
    """Sync: request_options max_retries overrides base_max_retries."""
    mock_client = MagicMock()
    mock_client.request.side_effect = [
        httpx.ConnectError("connection failed"),
        httpx.ConnectError("connection failed"),
        _DummyResponse(),
    ]
    http_client = HttpClient(
        httpx_client=mock_client,  # type: ignore[arg-type]
        base_timeout=lambda: None,
        base_headers=lambda: {},
        base_url=lambda: "https://example.com",
        base_max_retries=0,  # base says no retries
    )

    # But request_options overrides to allow 2 retries
    response = http_client.request(
        path="/test",
        method="GET",
        request_options={"max_retries": 2},
    )

    assert response.status_code == 200
    # 1 initial + 2 retries = 3 total attempts
    assert mock_client.request.call_count == 3


@pytest.mark.asyncio
@patch("auth0.management.core.http_client.asyncio.sleep", new_callable=AsyncMock)
async def test_async_request_options_override_base_max_retries(mock_sleep: AsyncMock) -> None:
    """Async: request_options max_retries overrides base_max_retries."""
    mock_client = MagicMock()
    mock_client.request = AsyncMock(
        side_effect=[
            httpx.ConnectError("connection failed"),
            httpx.ConnectError("connection failed"),
            _DummyResponse(),
        ]
    )
    http_client = AsyncHttpClient(
        httpx_client=mock_client,  # type: ignore[arg-type]
        base_timeout=lambda: None,
        base_headers=lambda: {},
        base_url=lambda: "https://example.com",
        base_max_retries=0,  # base says no retries
    )

    # But request_options overrides to allow 2 retries
    response = await http_client.request(
        path="/test",
        method="GET",
        request_options={"max_retries": 2},
    )

    assert response.status_code == 200
    # 1 initial + 2 retries = 3 total attempts
    assert mock_client.request.call_count == 3


@patch("auth0.management.core.http_client.time.sleep", return_value=None)
def test_sync_base_max_retries_used_as_default(mock_sleep: MagicMock) -> None:
    """Sync: base_max_retries is used when request_options has no max_retries."""
    mock_client = MagicMock()
    mock_client.request.side_effect = [
        httpx.ConnectError("fail"),
        httpx.ConnectError("fail"),
        httpx.ConnectError("fail"),
        _DummyResponse(),
    ]
    http_client = HttpClient(
        httpx_client=mock_client,  # type: ignore[arg-type]
        base_timeout=lambda: None,
        base_headers=lambda: {},
        base_url=lambda: "https://example.com",
        base_max_retries=3,
    )

    response = http_client.request(path="/test", method="GET")

    assert response.status_code == 200
    # 1 initial + 3 retries = 4 total attempts
    assert mock_client.request.call_count == 4


@pytest.mark.asyncio
@patch("auth0.management.core.http_client.asyncio.sleep", new_callable=AsyncMock)
async def test_async_base_max_retries_used_as_default(mock_sleep: AsyncMock) -> None:
    """Async: base_max_retries is used when request_options has no max_retries."""
    mock_client = MagicMock()
    mock_client.request = AsyncMock(
        side_effect=[
            httpx.ConnectError("fail"),
            httpx.ConnectError("fail"),
            httpx.ConnectError("fail"),
            _DummyResponse(),
        ]
    )
    http_client = AsyncHttpClient(
        httpx_client=mock_client,  # type: ignore[arg-type]
        base_timeout=lambda: None,
        base_headers=lambda: {},
        base_url=lambda: "https://example.com",
        base_max_retries=3,
    )

    response = await http_client.request(path="/test", method="GET")

    assert response.status_code == 200
    # 1 initial + 3 retries = 4 total attempts
    assert mock_client.request.call_count == 4


# ---------------------------------------------------------------------------
# _should_retry unit tests
# ---------------------------------------------------------------------------


def _make_response(status_code: int) -> httpx.Response:
    return httpx.Response(status_code=status_code, content=b"")


@pytest.mark.parametrize(
    "status_code",
    [408, 409, 429, 500, 501, 502, 503, 504, 599],
)
def test_should_retry_retryable_status_codes(status_code: int) -> None:
    """Legacy mode: retries on 408, 409, 429, and all >= 500."""
    assert _should_retry(_make_response(status_code)) is True


@pytest.mark.parametrize(
    "status_code",
    [200, 201, 301, 400, 401, 403, 404],
)
def test_should_not_retry_non_retryable_status_codes(status_code: int) -> None:
    assert _should_retry(_make_response(status_code)) is False


def test_should_retry_599_upper_boundary() -> None:
    """Legacy mode retries on >= 500, which includes 599."""
    assert _should_retry(_make_response(599)) is True


# ---------------------------------------------------------------------------
# RequestOptions timeout resolution tests (timeout / deprecated timeout_in_seconds)
# ---------------------------------------------------------------------------


def _sync_client_with_base_timeout(base_timeout: Any) -> Tuple[HttpClient, _DummySyncClient]:
    dummy_client = _DummySyncClient()
    http_client = HttpClient(
        httpx_client=dummy_client,  # type: ignore[arg-type]
        base_timeout=lambda: base_timeout,
        base_headers=lambda: {},
        base_url=lambda: "https://example.com",
    )
    return http_client, dummy_client


def test_sync_request_options_timeout_used() -> None:
    """The new `timeout` request option is passed through to httpx (in seconds)."""
    http_client, dummy_client = _sync_client_with_base_timeout(60)
    http_client.request(path="/test", method="GET", request_options={"timeout": 30})
    assert dummy_client.last_request_kwargs["timeout"] == 30


def test_sync_request_options_timeout_in_seconds_still_works() -> None:
    """The deprecated `timeout_in_seconds` request option remains backwards compatible."""
    http_client, dummy_client = _sync_client_with_base_timeout(60)
    http_client.request(path="/test", method="GET", request_options={"timeout_in_seconds": 45})
    assert dummy_client.last_request_kwargs["timeout"] == 45


def test_sync_request_options_timeout_takes_precedence() -> None:
    """When both are set, `timeout` wins over `timeout_in_seconds` (same seconds unit)."""
    http_client, dummy_client = _sync_client_with_base_timeout(60)
    http_client.request(path="/test", method="GET", request_options={"timeout": 30, "timeout_in_seconds": 45})
    assert dummy_client.last_request_kwargs["timeout"] == 30


def test_sync_request_options_timeout_falls_back_to_base() -> None:
    """When neither key is set, the client-level base timeout is used."""
    http_client, dummy_client = _sync_client_with_base_timeout(60)
    http_client.request(path="/test", method="GET", request_options=None)
    assert dummy_client.last_request_kwargs["timeout"] == 60


def test_sync_request_options_timeout_none_falls_back_to_deprecated() -> None:
    """An explicit `timeout=None` (dynamic caller) falls back to the deprecated `timeout_in_seconds`."""
    http_client, dummy_client = _sync_client_with_base_timeout(60)
    request_options = cast(RequestOptions, {"timeout": None, "timeout_in_seconds": 45})
    http_client.request(path="/test", method="GET", request_options=request_options)
    assert dummy_client.last_request_kwargs["timeout"] == 45


@pytest.mark.asyncio
async def test_async_request_options_timeout_takes_precedence() -> None:
    """Async: `timeout` wins over the deprecated `timeout_in_seconds`."""
    dummy_client = _DummyAsyncClient()
    http_client = AsyncHttpClient(
        httpx_client=dummy_client,  # type: ignore[arg-type]
        base_timeout=lambda: 60,
        base_headers=lambda: {},
        base_url=lambda: "https://example.com",
        async_base_headers=None,
    )
    await http_client.request(path="/test", method="GET", request_options={"timeout": 30, "timeout_in_seconds": 45})
    assert dummy_client.last_request_kwargs["timeout"] == 30
