"""Tests for sync and async clients."""

from datetime import datetime, timezone

import pytest
from pytest_httpx import HTTPXMock

from ticksupply import AsyncClient, Client
from ticksupply.exceptions import AuthenticationError, NotFoundError, RateLimitError


class TestClientConfig:
    """Test client configuration."""

    def test_client_requires_api_key(self) -> None:
        """Client raises ValueError when no API key provided."""
        with pytest.raises(ValueError, match="API key is required"):
            Client()

    def test_client_with_explicit_api_key(self, api_key: str) -> None:
        """Client accepts explicit API key."""
        client = Client(api_key=api_key)
        assert client._config.api_key == api_key
        client.close()

    def test_client_with_custom_config(self, api_key: str, base_url: str) -> None:
        """Client accepts custom configuration."""
        client = Client(
            api_key=api_key,
            base_url=base_url,
            timeout=60.0,
            max_retries=5,
        )
        assert client._config.base_url == base_url
        assert client._config.timeout == 60.0
        assert client._config.max_retries == 5
        client.close()

    def test_client_context_manager(self, api_key: str) -> None:
        """Client works as context manager."""
        with Client(api_key=api_key) as client:
            assert client._http_client._client is None  # Lazy init
        # Client should be closed after exiting


class TestSyncClient:
    """Test synchronous client operations."""

    def test_list_exchanges(self, httpx_mock: HTTPXMock, api_key: str, base_url: str) -> None:
        """Client can list exchanges."""
        httpx_mock.add_response(
            method="GET",
            url=f"{base_url}/exchanges",
            json=[
                {"id": 1, "code": "binance", "display_name": "Binance", "is_public": True},
                {"id": 2, "code": "okx", "display_name": "OKX", "is_public": True},
            ],
        )

        with Client(api_key=api_key, base_url=base_url) as client:
            exchanges = client.exchanges.list()

        assert len(exchanges) == 2
        assert exchanges[0].code == "binance"
        assert exchanges[1].code == "okx"

    def test_list_instruments(self, httpx_mock: HTTPXMock, api_key: str, base_url: str) -> None:
        """Client can list instruments."""
        httpx_mock.add_response(
            method="GET",
            url=f"{base_url}/exchanges/binance/instruments?search=BTC&limit=10",
            json={
                "items": [
                    {
                        "id": 1,
                        "exchange_id": 1,
                        "symbol": "BTCUSDT",
                        "base": "BTC",
                        "quote": "USDT",
                    },
                ],
                "total": 1,
                "limit": 10,
                "next_page_token": None,
            },
        )

        with Client(api_key=api_key, base_url=base_url) as client:
            result = client.exchanges.list_instruments("binance", search="BTC", limit=10)

        assert len(result.items) == 1
        assert result.items[0].symbol == "BTCUSDT"
        assert result.has_next is False

    def test_create_subscription(self, httpx_mock: HTTPXMock, api_key: str, base_url: str) -> None:
        """Client can create a subscription."""
        httpx_mock.add_response(
            method="POST",
            url=f"{base_url}/subscriptions",
            json={
                "id": "sub_123",
                "status": "active",
                "datastream": {
                    "datastream_id": 100,
                    "exchange": "binance",
                    "instrument": "BTCUSDT",
                    "stream_type": "trades",
                    "wire_format": "json",
                },
                "created_at": "2025-01-01T00:00:00Z",
                "spans": [],
            },
        )

        with Client(api_key=api_key, base_url=base_url) as client:
            sub = client.subscriptions.create(datastream_id=100)

        assert sub.id == "sub_123"
        assert sub.status.value == "active"

    def test_create_export(self, httpx_mock: HTTPXMock, api_key: str, base_url: str) -> None:
        """Client can create an export."""
        httpx_mock.add_response(
            method="POST",
            url=f"{base_url}/exports",
            json={
                "id": "exp_123",
                "datastream_id": 100,
                "start_time": 1735689600000000000,
                "end_time": 1735776000000000000,
                "format": "csv",
                "status": "queued",
                "created_at": "2025-01-01T00:00:00Z",
                "finished_at": None,
            },
        )

        with Client(api_key=api_key, base_url=base_url) as client:
            start = datetime(2025, 1, 1, tzinfo=timezone.utc)
            end = datetime(2025, 1, 2, tzinfo=timezone.utc)
            job = client.exports.create(100, start, end)

        assert job.id == "exp_123"
        assert job.status.value == "queued"

    def test_error_handling_not_found(
        self, httpx_mock: HTTPXMock, api_key: str, base_url: str
    ) -> None:
        """Client raises NotFoundError for 404."""
        httpx_mock.add_response(
            method="GET",
            url=f"{base_url}/subscriptions/invalid-uuid",
            status_code=404,
            json={
                "error": {
                    "code": "not_found",
                    "message": "Subscription not found",
                    "trace_id": "trace-123",
                }
            },
        )

        with Client(api_key=api_key, base_url=base_url) as client:
            with pytest.raises(NotFoundError) as exc_info:
                client.subscriptions.get("invalid-uuid")

        assert exc_info.value.code == "not_found"
        assert exc_info.value.http_status == 404

    def test_error_handling_auth(self, httpx_mock: HTTPXMock, api_key: str, base_url: str) -> None:
        """Client raises AuthenticationError for 401."""
        httpx_mock.add_response(
            method="GET",
            url=f"{base_url}/exchanges",
            status_code=401,
            json={
                "error": {
                    "code": "unauthenticated",
                    "message": "Invalid API key",
                }
            },
        )

        with Client(api_key=api_key, base_url=base_url) as client:
            with pytest.raises(AuthenticationError):
                client.exchanges.list()

    def test_error_handling_rate_limit(
        self, httpx_mock: HTTPXMock, api_key: str, base_url: str
    ) -> None:
        """Client raises RateLimitError for 429."""
        httpx_mock.add_response(
            method="GET",
            url=f"{base_url}/exchanges",
            status_code=429,
            headers={"Retry-After": "60"},
            json={
                "error": {
                    "code": "rate_limited",
                    "message": "Too many requests",
                }
            },
        )

        # Disable retries for this test
        with Client(api_key=api_key, base_url=base_url, max_retries=0) as client:
            with pytest.raises(RateLimitError) as exc_info:
                client.exchanges.list()

        assert exc_info.value.retry_after == 60


class TestAsyncClient:
    """Test asynchronous client operations."""

    @pytest.mark.asyncio
    async def test_async_client_context_manager(self, api_key: str) -> None:
        """AsyncClient works as async context manager."""
        async with AsyncClient(api_key=api_key) as client:
            assert client._http_client._client is None  # Lazy init

    @pytest.mark.asyncio
    async def test_async_list_exchanges(
        self, httpx_mock: HTTPXMock, api_key: str, base_url: str
    ) -> None:
        """AsyncClient can list exchanges."""
        httpx_mock.add_response(
            method="GET",
            url=f"{base_url}/exchanges",
            json=[
                {"id": 1, "code": "binance", "display_name": "Binance", "is_public": True},
            ],
        )

        async with AsyncClient(api_key=api_key, base_url=base_url) as client:
            exchanges = await client.exchanges.list()

        assert len(exchanges) == 1
        assert exchanges[0].code == "binance"

    @pytest.mark.asyncio
    async def test_async_create_subscription(
        self, httpx_mock: HTTPXMock, api_key: str, base_url: str
    ) -> None:
        """AsyncClient can create a subscription."""
        httpx_mock.add_response(
            method="POST",
            url=f"{base_url}/subscriptions",
            json={
                "id": "sub_456",
                "status": "active",
                "datastream": {
                    "datastream_id": 200,
                    "exchange": "okx",
                    "instrument": "ETHUSDT",
                    "stream_type": "trades",
                    "wire_format": "json",
                },
                "created_at": "2025-01-01T00:00:00Z",
                "spans": [],
            },
        )

        async with AsyncClient(api_key=api_key, base_url=base_url) as client:
            sub = await client.subscriptions.create(datastream_id=200)

        assert sub.id == "sub_456"

    @pytest.mark.asyncio
    async def test_async_error_handling(
        self, httpx_mock: HTTPXMock, api_key: str, base_url: str
    ) -> None:
        """AsyncClient handles errors correctly."""
        httpx_mock.add_response(
            method="GET",
            url=f"{base_url}/subscriptions/bad-uuid",
            status_code=404,
            json={
                "error": {
                    "code": "not_found",
                    "message": "Not found",
                }
            },
        )

        async with AsyncClient(api_key=api_key, base_url=base_url) as client:
            with pytest.raises(NotFoundError):
                await client.subscriptions.get("bad-uuid")


class TestResourceProperties:
    """Test that client exposes resources as properties."""

    def test_sync_client_resources(self, api_key: str) -> None:
        """Sync client exposes all resource properties."""
        with Client(api_key=api_key) as client:
            assert hasattr(client, "exchanges")
            assert hasattr(client, "subscriptions")
            assert hasattr(client, "exports")
            assert hasattr(client, "availability")

    @pytest.mark.asyncio
    async def test_async_client_resources(self, api_key: str) -> None:
        """Async client exposes all resource properties."""
        async with AsyncClient(api_key=api_key) as client:
            assert hasattr(client, "exchanges")
            assert hasattr(client, "subscriptions")
            assert hasattr(client, "exports")
            assert hasattr(client, "availability")
