"""Tests for the synchronous AnalyticsClient."""

from __future__ import annotations

import json
from typing import Any

import httpx
import pytest

from kanban_analytics.client import AnalyticsClient, ScopedAnalyticsClient
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:
    """Return an httpx.MockTransport that replays *responses* in order.

    Each entry is ``(status_code, optional_headers_dict)``.
    """
    call_index = 0
    calls: list[httpx.Request] = []

    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


def _make_client(
    responses: list[tuple[int, dict[str, str] | None]] | None = None,
    **init_kwargs: Any,
) -> tuple[AnalyticsClient, list[httpx.Request]]:
    """Create and initialise a client with a mock transport.

    First response is consumed by ``validate-key``; subsequent ones by
    track/identify/group calls.
    """
    if responses is None:
        responses = [(200, None), (202, None)]

    transport = _mock_transport(responses)
    client = AnalyticsClient()
    # Inject the mock transport into the httpx.Client
    client._client = httpx.Client(transport=transport)
    client._api_key = "test-api-key"
    client._endpoint = ENDPOINT

    # Build context without network
    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._batch_size = init_kwargs.pop("batch_size", 50)
    client._flush_interval = init_kwargs.pop("flush_interval", 60.0)
    client._default_properties = init_kwargs.pop("default_properties", {})
    client._timeout = init_kwargs.pop("timeout", 5.0)

    if validate:
        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 tests
# ---------------------------------------------------------------------------


class TestInit:
    def test_init_valid_key_succeeds(self) -> None:
        client, calls = _make_client([(200, None)])
        assert client._initialized
        # validate-key was called
        assert len(calls) == 1
        assert "/validate-key" in str(calls[0].url)

    def test_init_invalid_key_raises(self) -> None:
        with pytest.raises(AuthenticationError):
            _make_client([(401, None)])

    def test_init_skips_validation_when_disabled(self) -> None:
        client, calls = _make_client(
            [(202, None)],
            validate_on_init=False,
        )
        assert client._initialized
        assert len(calls) == 0


# ---------------------------------------------------------------------------
# Track tests
# ---------------------------------------------------------------------------


class TestTrack:
    def test_track_without_identity_raises(self) -> None:
        client, _ = _make_client()
        with pytest.raises(ValidationError, match="user_id"):
            client.track("test_event")

    def test_fire_and_forget_returns_none(self) -> None:
        client, _ = _make_client([(200, None), (202, None)])
        result = client.track(
            "test_event",
            user_id="user_1",
            await_response=False,
        )
        assert result is None

    def test_track_await_retries_on_500(self) -> None:
        client, calls = _make_client(
            [
                (200, None),  # validate-key
                (500, None),  # attempt 1
                (500, None),  # attempt 2
                (202, None),  # attempt 3 — success
            ],
            max_retries=3,
            retry_delay=0.01,
        )
        client.track("test_event", user_id="u1", await_response=True)
        # 1 validate + 3 track attempts
        assert len(calls) == 4

    def test_track_does_not_retry_on_400(self) -> None:
        client, calls = _make_client(
            [
                (200, None),  # validate-key
                (400, None),  # 400 — not retryable
            ],
            max_retries=3,
            retry_delay=0.01,
        )
        with pytest.raises(ValidationError):
            client.track("test_event", user_id="u1", await_response=True)
        # 1 validate + 1 track (no retry)
        assert len(calls) == 2

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

    def test_track_sends_correct_payload(self) -> None:
        client, calls = _make_client([(200, None), (202, None)])
        client.track(
            "payment_completed",
            properties={"value": 99},
            user_id="user_1",
            await_response=True,
        )
        # calls[0] is validate-key, calls[1] is the track call
        body = json.loads(calls[1].content.decode())
        assert len(body["batch"]) == 1
        evt = body["batch"][0]
        assert evt["type"] == "track"
        assert evt["event"] == "payment_completed"
        assert evt["userId"] == "user_1"
        assert evt["properties"]["value"] == 99
        assert evt["messageId"]
        assert evt["timestamp"]
        assert evt["context"]["sdk"]["name"] == "kanban-analytics"

    def test_track_uses_api_key_header(self) -> None:
        client, calls = _make_client([(200, None), (202, None)])
        client.track("test", user_id="u1", await_response=True)
        assert calls[1].headers["x-api-key"] == "test-api-key"

    def test_track_merges_default_properties(self) -> None:
        client, calls = _make_client(
            [(200, None), (202, None)],
            default_properties={"app_version": "2.0"},
        )
        client.track(
            "test",
            user_id="u1",
            properties={"custom": True},
            await_response=True,
        )
        body = json.loads(calls[1].content.decode())
        props = body["batch"][0]["properties"]
        assert props["app_version"] == "2.0"
        assert props["custom"] is True


# ---------------------------------------------------------------------------
# Identify / Group / Screen tests
# ---------------------------------------------------------------------------


class TestIdentify:
    def test_identify_sets_user_id_for_subsequent_calls(self) -> None:
        client, calls = _make_client(
            [(200, None), (202, None), (202, None)],
        )
        client.identify("u1", traits={"name": "Alice"}, await_response=True)
        # Now track without explicit user_id
        client.track("test_event", await_response=True)
        body = json.loads(calls[2].content.decode())
        assert body["batch"][0]["userId"] == "u1"

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


class TestGroup:
    def test_group_sends_event(self) -> None:
        client, calls = _make_client([(200, None), (202, None)])
        client.group("org_1", traits={"name": "Acme"}, await_response=True)
        body = json.loads(calls[1].content.decode())
        assert body["batch"][0]["type"] == "group"
        assert body["batch"][0]["groupId"] == "org_1"


class TestScreen:
    def test_screen_sends_event(self) -> None:
        client, calls = _make_client([(200, None), (202, None)])
        client.screen("Dashboard", user_id="u1", await_response=True)
        body = json.loads(calls[1].content.decode())
        assert body["batch"][0]["type"] == "screen"
        assert body["batch"][0]["event"] == "Dashboard"


# ---------------------------------------------------------------------------
# with_context tests
# ---------------------------------------------------------------------------


class TestWithContext:
    def test_attaches_context_to_all_calls(self) -> None:
        client, calls = _make_client(
            [(200, None), (202, None), (202, None)],
        )
        scoped = client.with_context(user_id="u1")
        scoped.track("api_called", await_response=True)
        scoped.track("db_queried", await_response=True)

        body1 = json.loads(calls[1].content.decode())
        assert body1["batch"][0]["userId"] == "u1"

        body2 = json.loads(calls[2].content.decode())
        assert body2["batch"][0]["userId"] == "u1"


# ---------------------------------------------------------------------------
# Context manager tests
# ---------------------------------------------------------------------------


class TestContextManager:
    def test_context_manager_calls_shutdown(self) -> None:
        responses = [(200, None)]
        transport = _mock_transport(responses)
        client = AnalyticsClient()
        client._client = httpx.Client(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]
        client._validate_api_key()
        client._api_key_validated = True
        client._initialized = True

        with client:
            pass

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


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


class TestDisabled:
    def test_disabled_makes_all_methods_noop(self) -> None:
        client, calls = _make_client(
            [(200, None)],
            disabled=True,
        )
        result = client.track("test", user_id="u1")
        assert result is None
        # Only the validate-key call, nothing else
        assert len(calls) == 1


# ---------------------------------------------------------------------------
# Uninitialized tests
# ---------------------------------------------------------------------------


class TestUninitialized:
    def test_track_before_init_raises(self) -> None:
        client = AnalyticsClient()
        with pytest.raises(RuntimeError, match="not initialized"):
            client.track("test", user_id="u1")
