"""Tests for the asynchronous AsyncAnalyticsClient."""

from __future__ import annotations

import asyncio
import json
from typing import Any

import httpx
import pytest

from kanban_analytics.async_client import (
    AsyncAnalyticsClient,
    AsyncScopedAnalyticsClient,
)
from kanban_analytics.errors import (
    AuthenticationError,
    NetworkError,
    RateLimitError,
    ValidationError,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

ENDPOINT = "https://example.com/api/ingest"


def _mock_transport(
    responses: list[tuple[int, dict[str, str] | None]],
) -> httpx.MockTransport:
    call_index = 0
    calls: list[httpx.Request] = []

    async def _handler(request: httpx.Request) -> httpx.Response:
        nonlocal call_index
        calls.append(request)
        status, headers = responses[min(call_index, len(responses) - 1)]
        call_index += 1
        return httpx.Response(status, headers=headers or {}, json={})

    transport = httpx.MockTransport(_handler)
    transport._calls = calls  # type: ignore[attr-defined]
    return transport


async def _make_client(
    responses: list[tuple[int, dict[str, str] | None]] | None = None,
    **init_kwargs: Any,
) -> tuple[AsyncAnalyticsClient, list[httpx.Request]]:
    if responses is None:
        responses = [(200, None), (202, None)]

    transport = _mock_transport(responses)
    client = AsyncAnalyticsClient()
    client._client = httpx.AsyncClient(transport=transport)
    client._api_key = "test-api-key"
    client._endpoint = ENDPOINT

    from kanban_analytics.context import build_server_context

    client._context = build_server_context({})  # type: ignore[arg-type]

    validate = init_kwargs.pop("validate_on_init", True)
    client._debug = init_kwargs.pop("debug", False)
    client._disabled = init_kwargs.pop("disabled", False)
    client._max_retries = init_kwargs.pop("max_retries", 3)
    client._retry_delay = init_kwargs.pop("retry_delay", 0.01)
    client._batch_mode = init_kwargs.pop("batch_mode", False)
    client._default_properties = init_kwargs.pop("default_properties", {})
    client._timeout = init_kwargs.pop("timeout", 5.0)

    if validate:
        await client._validate_api_key()
        client._api_key_validated = True

    client._initialized = True

    calls: list[httpx.Request] = transport._calls  # type: ignore[attr-defined]
    return client, calls


# ---------------------------------------------------------------------------
# Init
# ---------------------------------------------------------------------------


class TestAsyncInit:
    @pytest.mark.asyncio
    async def test_init_valid_key(self) -> None:
        client, calls = await _make_client([(200, None)])
        assert client._initialized
        assert len(calls) == 1
        assert "/validate-key" in str(calls[0].url)

    @pytest.mark.asyncio
    async def test_init_invalid_key(self) -> None:
        with pytest.raises(AuthenticationError):
            await _make_client([(401, None)])

    @pytest.mark.asyncio
    async def test_init_skip_validation(self) -> None:
        client, calls = await _make_client(
            [(202, None)],
            validate_on_init=False,
        )
        assert client._initialized
        assert len(calls) == 0


# ---------------------------------------------------------------------------
# Track
# ---------------------------------------------------------------------------


class TestAsyncTrack:
    @pytest.mark.asyncio
    async def test_track_without_identity_raises(self) -> None:
        client, _ = await _make_client()
        with pytest.raises(ValidationError, match="user_id"):
            await client.track("test_event", await_response=True)

    @pytest.mark.asyncio
    async def test_fire_and_forget_uses_create_task(self) -> None:
        """Fire-and-forget must not block; it uses asyncio.create_task."""
        client, calls = await _make_client(
            [(200, None), (202, None)],
        )
        # Fire and forget — should return immediately
        await client.track("test", user_id="u1", await_response=False)
        # Allow the background task to complete
        await asyncio.sleep(0.05)
        # Should have sent: validate + track
        assert len(calls) >= 2

    @pytest.mark.asyncio
    async def test_track_await_retries_on_500(self) -> None:
        client, calls = await _make_client(
            [
                (200, None),  # validate
                (500, None),  # attempt 1
                (500, None),  # attempt 2
                (202, None),  # success
            ],
            max_retries=3,
            retry_delay=0.01,
        )
        await client.track("test", user_id="u1", await_response=True)
        assert len(calls) == 4

    @pytest.mark.asyncio
    async def test_track_does_not_retry_on_400(self) -> None:
        client, calls = await _make_client(
            [
                (200, None),
                (400, None),
            ],
            max_retries=3,
            retry_delay=0.01,
        )
        with pytest.raises(ValidationError):
            await client.track("test", user_id="u1", await_response=True)
        assert len(calls) == 2

    @pytest.mark.asyncio
    async def test_track_raises_rate_limit_on_429(self) -> None:
        client, _ = await _make_client(
            [
                (200, None),
                (429, {"Retry-After": "5"}),
            ],
            max_retries=0,
            retry_delay=0.01,
        )
        with pytest.raises(RateLimitError):
            await client.track("test", user_id="u1", await_response=True)

    @pytest.mark.asyncio
    async def test_track_sends_correct_payload(self) -> None:
        client, calls = await _make_client([(200, None), (202, None)])
        await client.track(
            "payment_completed",
            properties={"value": 99},
            user_id="user_1",
            await_response=True,
        )
        body = json.loads(calls[1].content.decode())
        evt = body["batch"][0]
        assert evt["type"] == "track"
        assert evt["event"] == "payment_completed"
        assert evt["userId"] == "user_1"
        assert evt["context"]["sdk"]["name"] == "kanban-analytics"


# ---------------------------------------------------------------------------
# Identify / Reset
# ---------------------------------------------------------------------------


class TestAsyncIdentify:
    @pytest.mark.asyncio
    async def test_identify_sets_user(self) -> None:
        client, calls = await _make_client(
            [(200, None), (202, None), (202, None)],
        )
        await client.identify("u1", traits={"name": "Alice"}, await_response=True)
        await client.track("test", await_response=True)
        body = json.loads(calls[2].content.decode())
        assert body["batch"][0]["userId"] == "u1"

    @pytest.mark.asyncio
    async def test_reset_clears_identity(self) -> None:
        client, _ = await _make_client([(200, None), (202, None)])
        await client.identify("u1", await_response=True)
        client.reset()
        with pytest.raises(ValidationError):
            await client.track("test", await_response=True)


# ---------------------------------------------------------------------------
# with_context
# ---------------------------------------------------------------------------


class TestAsyncWithContext:
    @pytest.mark.asyncio
    async def test_scoped_client_passes_context(self) -> None:
        client, calls = await _make_client(
            [(200, None), (202, None)],
        )
        scoped = client.with_context(user_id="u1")
        await scoped.track("api_called", await_response=True)
        body = json.loads(calls[1].content.decode())
        assert body["batch"][0]["userId"] == "u1"


# ---------------------------------------------------------------------------
# Async context manager
# ---------------------------------------------------------------------------


class TestAsyncContextManager:
    @pytest.mark.asyncio
    async def test_aexit_calls_shutdown(self) -> None:
        transport = _mock_transport([(200, None)])
        client = AsyncAnalyticsClient()
        client._client = httpx.AsyncClient(transport=transport)
        client._api_key = "test"
        client._endpoint = ENDPOINT
        from kanban_analytics.context import build_server_context

        client._context = build_server_context({})  # type: ignore[arg-type]
        await client._validate_api_key()
        client._api_key_validated = True
        client._initialized = True

        async with client:
            pass

        assert client._disabled is True
        assert client._client is None


# ---------------------------------------------------------------------------
# Disabled flag
# ---------------------------------------------------------------------------


class TestAsyncDisabled:
    @pytest.mark.asyncio
    async def test_disabled_is_noop(self) -> None:
        client, calls = await _make_client(
            [(200, None)],
            disabled=True,
        )
        await client.track("test", user_id="u1")
        # Only validate-key, nothing else
        assert len(calls) == 1


# ---------------------------------------------------------------------------
# Concurrent safety
# ---------------------------------------------------------------------------


class TestAsyncConcurrency:
    @pytest.mark.asyncio
    async def test_concurrent_tracks_no_deadlock(self) -> None:
        responses = [(200, None)] + [(202, None)] * 50
        client, calls = await _make_client(responses)

        async def _track(i: int) -> None:
            await client.track(
                f"event_{i}",
                user_id="u1",
                await_response=True,
            )

        await asyncio.gather(*[_track(i) for i in range(20)])
        # 1 validate + 20 track calls
        assert len(calls) == 21
