from __future__ import annotations

import os
import warnings
from typing import Any, Callable, Dict, List, Tuple

import pytest

from its_ai import (
    AnalyzeBatchItemResult,
    AnalyzeTextResult,
    ApiError,
    AuthenticationFailed,
    AsyncItsAIClient,
    ItsAIClient,
    ItsAIAsyncClient,
    ItsAIError,
    LowWords,
    ManyWords,
    NotFound,
    OnlyEnglish,
    PermissionDenied,
    RateLimitExceeded,
    ServerError,
    ServiceUnavailable,
    ValidationError,
)


class DummyResponse:
    def __init__(self, status_code: int, json_data: Any = None, text: str = "") -> None:
        self.status_code = status_code
        self._json = json_data
        self.text = text
        self.headers: Dict[str, str] = {}

    def json(self) -> Any:
        if self._json is None:
            raise ValueError("no json")
        return self._json


class DummySession:
    def __init__(self, handler: Callable[[str, Dict[str, Any]], DummyResponse]) -> None:
        self._handler = handler
        self.calls: List[Tuple[str, Dict[str, Any]]] = []

    def post(self, url, headers=None, json=None, timeout=None):  # type: ignore[override]
        payload = json or {}
        self.calls.append((url, payload))
        return self._handler(url, payload)

    def close(self) -> None:
        pass


class DummyAsyncClient:
    def __init__(self, handler: Callable[[str, Dict[str, Any]], DummyResponse]) -> None:
        self._handler = handler
        self.calls: List[Tuple[str, Dict[str, Any]]] = []

    async def post(self, url, headers=None, json=None, timeout=None):  # type: ignore[override]
        payload = json or {}
        self.calls.append((url, payload))
        return self._handler(url, payload)

    async def aclose(self) -> None:
        pass


def test_single_scan_parsing_without_deep_scan() -> None:
    def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
        assert url.endswith("/api/text")
        assert payload["deep_scan"] is False
        body = {"answer": 0.42, "status": "success", "segmentation_tokens": None}
        return DummyResponse(200, body)

    session = DummySession(handler)
    client = ItsAIClient(api_key="sk_test", session=session)
    result = client.analyze_text("example text")
    assert isinstance(result, AnalyzeTextResult)
    assert result.answer == pytest.approx(0.42)
    assert result.status == "success"
    assert result.segmentation_tokens is None
    assert "purpose" not in session.calls[0][1]


def test_single_scan_parsing_with_deep_scan() -> None:
    def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
        assert payload["deep_scan"] is True
        body = {
            "answer": 0.88,
            "status": "success",
            "segmentation_tokens": [0.1, 0.2, 0.3],
        }
        return DummyResponse(200, body)

    session = DummySession(handler)
    client = ItsAIClient(api_key="sk_test", session=session)
    result = client.analyze_text("long enough text for deep scan", deep_scan=True)
    assert result.status == "success"
    assert result.segmentation_tokens == [pytest.approx(0.1), pytest.approx(0.2), pytest.approx(0.3)]


def test_single_scan_missing_segmentation_raises() -> None:
    def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
        body = {"answer": 0.3, "status": "success", "segmentation_tokens": None}
        return DummyResponse(200, body)

    session = DummySession(handler)
    client = ItsAIClient(api_key="sk_test", session=session)
    with pytest.raises(ApiError) as exc_info:
        client.analyze_text("needs segmentation", deep_scan=True)
    assert exc_info.value.code == "missing_segmentation"


def test_single_scan_nested_error_payload() -> None:
    def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
        body = {
            "answer": 0.0,
            "status": "error",
            "error": {"code": "low_words", "message": "Input too short"},
        }
        return DummyResponse(200, body)

    session = DummySession(handler)
    client = ItsAIClient(api_key="sk_test", session=session)
    with pytest.raises(ApiError) as exc_info:
        client.analyze_text("short")
    assert exc_info.value.code == "low_words"
    assert exc_info.value.message == "Input too short"


def test_batch_success_parsing() -> None:
    def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
        assert url.endswith("/api/batch")
        assert payload["deep_scan"] is True
        body = {
            "answer": [
                {
                    "text": payload["texts"][0],
                    "answer": 0.7,
                    "segmentation_tokens": None,
                    "deep_scan": False,
                },
                {
                    "text": payload["texts"][1],
                    "answer": 0.2,
                    "segmentation_tokens": [0.4, 0.6],
                    "deep_scan": True,
                },
            ],
            "error": None,
        }
        return DummyResponse(200, body)

    session = DummySession(handler)
    client = ItsAIClient(api_key="sk_test", session=session)
    results = client.analyze_batch(["first", "second"], deep_scan=True)
    assert len(results) == 2
    assert all(isinstance(item, AnalyzeBatchItemResult) for item in results)
    assert results[0].segmentation_tokens is None
    assert results[1].segmentation_tokens == [pytest.approx(0.4), pytest.approx(0.6)]


def test_batch_error_propagation() -> None:
    def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
        body = {
            "answer": [],
            "error": {"code": "batch_failed", "message": "cannot process", "details": {"reason": "bad"}},
        }
        return DummyResponse(200, body)

    session = DummySession(handler)
    client = ItsAIClient(api_key="sk_test", session=session)
    with pytest.raises(ApiError) as exc_info:
        client.analyze_batch(["first"], deep_scan=False)
    assert exc_info.value.code == "batch_failed"
    assert exc_info.value.details == {"reason": "bad"}


def test_validation_errors() -> None:
    with pytest.raises(ValidationError):
        ItsAIClient(api_key="")

    session = DummySession(lambda url, payload: DummyResponse(200, {"answer": 0.5, "status": "success", "segmentation_tokens": None}))
    client = ItsAIClient(api_key="sk_test", session=session)
    with pytest.raises(ValidationError):
        client.analyze_text("")
    with pytest.raises(ValidationError):
        client.analyze_batch([])


def test_error_mapping_http() -> None:
    class ErrSession(DummySession):
        def __init__(self) -> None:
            body = {"error": {"code": "authentication_failed", "message": "bad key"}}
            super().__init__(lambda url, payload: DummyResponse(401, body))

    client = ItsAIClient(api_key="sk_test", session=ErrSession())
    with pytest.raises(AuthenticationFailed):
        client.analyze_text("hello")


@pytest.mark.anyio
@pytest.mark.parametrize("anyio_backend", ["asyncio"])
async def test_async_single_scan_parsing(anyio_backend) -> None:
    def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
        assert url.endswith("/api/text")
        assert payload["deep_scan"] is False
        return DummyResponse(200, {"answer": 0.21, "status": "success", "segmentation_tokens": None})

    async_client = DummyAsyncClient(handler)
    client = AsyncItsAIClient(api_key="sk_test", client=async_client)
    result = await client.analyze_text("example text")
    assert isinstance(result, AnalyzeTextResult)
    assert result.answer == pytest.approx(0.21)
    assert "purpose" not in async_client.calls[0][1]
    await client.aclose()


@pytest.mark.anyio
@pytest.mark.parametrize("anyio_backend", ["asyncio"])
async def test_async_single_scan_missing_segmentation(anyio_backend) -> None:
    def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
        return DummyResponse(200, {"answer": 0.3, "status": "success", "segmentation_tokens": None})

    async_client = DummyAsyncClient(handler)
    client = AsyncItsAIClient(api_key="sk_test", client=async_client)
    with pytest.raises(ApiError) as exc_info:
        await client.analyze_text("needs segmentation", deep_scan=True)
    assert exc_info.value.code == "missing_segmentation"
    await client.aclose()


@pytest.mark.anyio
@pytest.mark.parametrize("anyio_backend", ["asyncio"])
async def test_async_batch_error_propagation(anyio_backend) -> None:
    def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
        body = {
            "answer": [],
            "error": {
                "code": "batch_failed",
                "message": "cannot process",
                "details": {"reason": "bad"},
            },
        }
        return DummyResponse(200, body)

    async_client = DummyAsyncClient(handler)
    client = AsyncItsAIClient(api_key="sk_test", client=async_client)
    with pytest.raises(ApiError) as exc_info:
        await client.analyze_batch(["first"], deep_scan=False)
    assert exc_info.value.code == "batch_failed"
    assert exc_info.value.details == {"reason": "bad"}
    await client.aclose()


@pytest.mark.anyio
@pytest.mark.parametrize("anyio_backend", ["asyncio"])
async def test_async_batch_success_parsing(anyio_backend) -> None:
    def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
        body = {
            "answer": [
                {
                    "text": payload["texts"][0],
                    "answer": 0.7,
                    "segmentation_tokens": None,
                    "deep_scan": False,
                },
                {
                    "text": payload["texts"][1],
                    "answer": 0.2,
                    "segmentation_tokens": [0.4, 0.6],
                    "deep_scan": True,
                },
            ],
            "error": None,
        }
        return DummyResponse(200, body)

    async_client = DummyAsyncClient(handler)
    client = AsyncItsAIClient(api_key="sk_test", client=async_client)
    results = await client.analyze_batch(["first", "second"], deep_scan=True)
    assert len(results) == 2
    assert all(isinstance(item, AnalyzeBatchItemResult) for item in results)
    assert results[0].segmentation_tokens is None
    assert results[1].segmentation_tokens == [pytest.approx(0.4), pytest.approx(0.6)]
    await client.aclose()


@pytest.mark.anyio
@pytest.mark.parametrize("anyio_backend", ["asyncio"])
async def test_async_validation_errors(anyio_backend) -> None:
    async_client = DummyAsyncClient(lambda url, payload: DummyResponse(200, {}))
    client = AsyncItsAIClient(api_key="sk_test", client=async_client)
    with pytest.raises(ValidationError):
        await client.analyze_text("")
    with pytest.raises(ValidationError):
        await client.analyze_batch([])
    await client.aclose()


# =============================================================================
# Validation Error Tests
# =============================================================================


class TestValidationErrors:
    """Tests for input validation across sync and async clients."""

    def test_empty_api_key_raises(self) -> None:
        with pytest.raises(ValidationError) as exc_info:
            ItsAIClient(api_key="")
        assert "api_key is required" in exc_info.value.message

    def test_none_api_key_without_env_raises(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.delenv("ITS_AI_API_KEY", raising=False)
        with pytest.raises(ValidationError) as exc_info:
            ItsAIClient(api_key=None)
        assert "api_key is required" in exc_info.value.message

    def test_empty_text_raises(self) -> None:
        session = DummySession(lambda url, payload: DummyResponse(200, {}))
        client = ItsAIClient(api_key="sk_test", session=session)
        with pytest.raises(ValidationError) as exc_info:
            client.analyze_text("")
        assert "non-empty string" in exc_info.value.message

    def test_whitespace_only_text_raises(self) -> None:
        session = DummySession(lambda url, payload: DummyResponse(200, {}))
        client = ItsAIClient(api_key="sk_test", session=session)
        with pytest.raises(ValidationError) as exc_info:
            client.analyze_text("   \t\n  ")
        assert "non-empty string" in exc_info.value.message

    def test_non_string_text_raises(self) -> None:
        session = DummySession(lambda url, payload: DummyResponse(200, {}))
        client = ItsAIClient(api_key="sk_test", session=session)
        with pytest.raises(ValidationError) as exc_info:
            client.analyze_text(123)  # type: ignore[arg-type]
        assert "must be type string" in exc_info.value.message

    def test_non_bool_deep_scan_raises(self) -> None:
        session = DummySession(lambda url, payload: DummyResponse(200, {}))
        client = ItsAIClient(api_key="sk_test", session=session)
        with pytest.raises(ValidationError) as exc_info:
            client.analyze_text("valid text", deep_scan="yes")  # type: ignore[arg-type]
        assert "deep_scan argument must be bool" in exc_info.value.message

    def test_empty_batch_list_raises(self) -> None:
        session = DummySession(lambda url, payload: DummyResponse(200, {}))
        client = ItsAIClient(api_key="sk_test", session=session)
        with pytest.raises(ValidationError) as exc_info:
            client.analyze_batch([])
        assert "non-empty list" in exc_info.value.message

    def test_batch_with_empty_string_raises(self) -> None:
        session = DummySession(lambda url, payload: DummyResponse(200, {}))
        client = ItsAIClient(api_key="sk_test", session=session)
        with pytest.raises(ValidationError) as exc_info:
            client.analyze_batch(["valid", ""])
        assert "each item in texts must be a non-empty string" in exc_info.value.message

    def test_batch_with_non_string_raises(self) -> None:
        session = DummySession(lambda url, payload: DummyResponse(200, {}))
        client = ItsAIClient(api_key="sk_test", session=session)
        with pytest.raises(ValidationError) as exc_info:
            client.analyze_batch(["valid", 123])  # type: ignore[list-item]
        assert "each item in texts must be a non-empty string" in exc_info.value.message


# =============================================================================
# Error Mapping Tests
# =============================================================================


class TestErrorMapping:
    """Tests for HTTP status code to exception mapping."""

    @pytest.mark.parametrize(
        "status_code,error_code,expected_exc",
        [
            (401, "authentication_failed", AuthenticationFailed),
            (403, "permission_denied", PermissionDenied),
            (404, "not_found", NotFound),
            (429, "rate_limit", RateLimitExceeded),
            (500, "server_error", ServerError),
            (503, "service_unavailable", ServiceUnavailable),
        ],
    )
    def test_http_error_mapping(
        self, status_code: int, error_code: str, expected_exc: type
    ) -> None:
        def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
            body = {"error": {"code": error_code, "message": "test error"}}
            return DummyResponse(status_code, body)

        session = DummySession(handler)
        client = ItsAIClient(api_key="sk_test", session=session)
        with pytest.raises(expected_exc):
            client.analyze_text("test text")

    @pytest.mark.parametrize(
        "error_code,expected_exc",
        [
            ("low_words", LowWords),
            ("many_words", ManyWords),
            ("only_english", OnlyEnglish),
        ],
    )
    def test_api_error_code_mapping(self, error_code: str, expected_exc: type) -> None:
        def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
            body = {"error": {"code": error_code, "message": f"{error_code} error"}}
            return DummyResponse(400, body)

        session = DummySession(handler)
        client = ItsAIClient(api_key="sk_test", session=session)
        with pytest.raises(expected_exc):
            client.analyze_text("test text")


# =============================================================================
# Client Feature Tests
# =============================================================================


class TestClientFeatures:
    """Tests for client features like repr, context manager, deprecation."""

    def test_client_repr_masks_api_key(self) -> None:
        client = ItsAIClient(api_key="sk_test_secret_key_12345678")
        repr_str = repr(client)
        assert "ItsAIClient(" in repr_str
        assert "sk_test_secret_key_12345678" not in repr_str
        assert "***5678" in repr_str
        assert "base_url=" in repr_str
        assert "timeout=" in repr_str

    def test_async_client_repr_masks_api_key(self) -> None:
        client = AsyncItsAIClient(api_key="sk_async_key_99998888")
        repr_str = repr(client)
        assert "AsyncItsAIClient(" in repr_str
        assert "sk_async_key_99998888" not in repr_str
        assert "***8888" in repr_str

    def test_deprecated_alias_emits_warning(self) -> None:
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            client = ItsAIAsyncClient(api_key="sk_test")
            assert len(w) == 1
            assert issubclass(w[0].category, DeprecationWarning)
            assert "deprecated" in str(w[0].message).lower()
            assert "AsyncItsAIClient" in str(w[0].message)

    def test_context_manager_sync(self) -> None:
        session = DummySession(lambda url, payload: DummyResponse(200, {
            "answer": 0.5, "status": "success", "segmentation_tokens": None
        }))
        with ItsAIClient(api_key="sk_test", session=session) as client:
            result = client.analyze_text("test")
            assert result.answer == pytest.approx(0.5)

    def test_api_key_from_env(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv("ITS_AI_API_KEY", "sk_from_env_12345678")
        client = ItsAIClient()
        assert "***5678" in repr(client)

    def test_custom_base_url(self) -> None:
        client = ItsAIClient(api_key="sk_test", base_url="https://custom.api.example.com/")
        assert client.base_url == "https://custom.api.example.com"  # trailing slash stripped

    def test_custom_timeout_and_attempts(self) -> None:
        client = ItsAIClient(api_key="sk_test", timeout=30.0, max_attempts=5)
        assert client.timeout == 30.0
        assert client.max_attempts == 5

    def test_max_attempts_minimum_is_one(self) -> None:
        client = ItsAIClient(api_key="sk_test", max_attempts=0)
        assert client.max_attempts == 1
        client2 = ItsAIClient(api_key="sk_test", max_attempts=-5)
        assert client2.max_attempts == 1


# =============================================================================
# Batch Chunking Tests
# =============================================================================


class TestBatchChunking:
    """Tests for batch request chunking functionality."""

    def test_batch_chunking_splits_requests(self) -> None:
        call_count = [0]

        def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
            call_count[0] += 1
            texts = payload["texts"]
            body = {
                "answer": [
                    {"text": t, "answer": 0.5, "segmentation_tokens": None, "deep_scan": False}
                    for t in texts
                ],
                "error": None,
            }
            return DummyResponse(200, body)

        session = DummySession(handler)
        client = ItsAIClient(api_key="sk_test", session=session)
        texts = [f"text_{i}" for i in range(10)]
        results = client.analyze_batch(texts, max_batch_size=3)

        assert len(results) == 10
        assert call_count[0] == 4  # 3 + 3 + 3 + 1 = 10 items in 4 requests

    def test_batch_no_chunking_when_under_limit(self) -> None:
        call_count = [0]

        def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
            call_count[0] += 1
            texts = payload["texts"]
            body = {
                "answer": [
                    {"text": t, "answer": 0.5, "segmentation_tokens": None, "deep_scan": False}
                    for t in texts
                ],
                "error": None,
            }
            return DummyResponse(200, body)

        session = DummySession(handler)
        client = ItsAIClient(api_key="sk_test", session=session)
        results = client.analyze_batch(["a", "b", "c"], max_batch_size=10)

        assert len(results) == 3
        assert call_count[0] == 1


# =============================================================================
# E2E Tests (require ITS_AI_E2E=1 and ITS_AI_API_KEY)
# =============================================================================


@pytest.mark.skipif(os.getenv("ITS_AI_E2E") != "1", reason="e2e disabled")
class TestE2E:
    """End-to-end tests against a real API server."""

    # Sample long text that meets minimum length requirements
    SAMPLE_TEXT = (
            "This is a sufficiently long English text for testing the ITS-AI API. "
            "We want to ensure the input meets the minimum length requirement of two hundred characters. "
            "Therefore, we will continue adding more sentences to reach the threshold and avoid low_words errors. "
            "This text should be more than enough to pass validation."
        )

    SAMPLE_TEXT_2 = (
        "Artificial intelligence has transformed the way we interact with technology. "
        "From voice assistants to recommendation systems, AI powers countless applications we use daily. "
        "Machine learning algorithms can now process vast amounts of data and identify patterns that humans might miss. "
        "This revolution in computing continues to accelerate."
    )

    @pytest.fixture
    def api_key(self) -> str:
        key = os.getenv("ITS_AI_API_KEY")
        if not key:
            pytest.skip("ITS_AI_API_KEY not set")
        return key

    @pytest.fixture
    def base_url(self) -> str:
        return os.getenv("ITS_AI_BASE_URL", "https://api.its-ai.org")

    def test_e2e_single_text_analysis(self, api_key: str, base_url: str) -> None:
        with ItsAIClient(api_key=api_key, base_url=base_url) as client:
            result = client.analyze_text(self.SAMPLE_TEXT, deep_scan=False)
            assert isinstance(result, AnalyzeTextResult)
            assert result.status == "success"
            assert 0.0 <= result.answer <= 1.0
            assert not result.segmentation_tokens

    def test_e2e_single_text_with_deep_scan(self, api_key: str, base_url: str) -> None:
        with ItsAIClient(api_key=api_key, base_url=base_url) as client:
            result = client.analyze_text(self.SAMPLE_TEXT, deep_scan=True)
            assert isinstance(result, AnalyzeTextResult)
            assert result.status == "success"
            assert 0.0 <= result.answer <= 1.0
            # deep_scan should return segmentation tokens
            assert result.segmentation_tokens is not None
            assert isinstance(result.segmentation_tokens, list)
            assert len(result.segmentation_tokens) > 0

    def test_e2e_batch_analysis(self, api_key: str, base_url: str) -> None:
        with ItsAIClient(api_key=api_key, base_url=base_url) as client:
            texts = [self.SAMPLE_TEXT, self.SAMPLE_TEXT_2]
            results = client.analyze_batch(texts, deep_scan=False)
            assert len(results) == 2
            for i, result in enumerate(results):
                assert isinstance(result, AnalyzeBatchItemResult)
                assert result.text == texts[i]
                assert 0.0 <= result.answer <= 1.0

    def test_e2e_batch_with_deep_scan(self, api_key: str, base_url: str) -> None:
        with ItsAIClient(api_key=api_key, base_url=base_url) as client:
            texts = [self.SAMPLE_TEXT, self.SAMPLE_TEXT_2]
            results = client.analyze_batch(texts, deep_scan=True)
            assert len(results) == 2
            for result in results:
                assert isinstance(result, AnalyzeBatchItemResult)
                assert 0.0 <= result.answer <= 1.0

    def test_e2e_low_words_error(self, api_key: str, base_url: str) -> None:
        with ItsAIClient(api_key=api_key, base_url=base_url) as client:
            with pytest.raises(ItsAIError) as exc_info:
                client.analyze_text("Too short text")
            # Should get some form of validation/low_words error
            assert exc_info.value.status_code is not None or exc_info.value.type is not None

    @pytest.mark.anyio
    @pytest.mark.parametrize("anyio_backend", ["asyncio"])
    async def test_e2e_async_single_text(
        self, api_key: str, base_url: str, anyio_backend: str
    ) -> None:
        async with AsyncItsAIClient(api_key=api_key, base_url=base_url) as client:
            result = await client.analyze_text(self.SAMPLE_TEXT, deep_scan=False)
            assert isinstance(result, AnalyzeTextResult)
            assert result.status == "success"
            assert 0.0 <= result.answer <= 1.0

    @pytest.mark.anyio
    @pytest.mark.parametrize("anyio_backend", ["asyncio"])
    async def test_e2e_async_batch(
        self, api_key: str, base_url: str, anyio_backend: str
    ) -> None:
        async with AsyncItsAIClient(api_key=api_key, base_url=base_url) as client:
            texts = [self.SAMPLE_TEXT, self.SAMPLE_TEXT_2]
            results = await client.analyze_batch(texts, deep_scan=False)
            assert len(results) == 2
            for result in results:
                assert isinstance(result, AnalyzeBatchItemResult)
                assert 0.0 <= result.answer <= 1.0

    @pytest.mark.anyio
    @pytest.mark.parametrize("anyio_backend", ["asyncio"])
    async def test_e2e_rps_throughput(
        self, api_key: str, base_url: str, anyio_backend: str
    ) -> None:
        """Test throughput: 10 concurrent requests to verify RPS handling."""
        import asyncio
        import time

        async with AsyncItsAIClient(api_key=api_key, base_url=base_url) as client:
            # Create 10 concurrent requests
            tasks = [
                client.analyze_text(self.SAMPLE_TEXT, deep_scan=False)
                for _ in range(10)
            ]

            start_time = time.perf_counter()
            results = await asyncio.gather(*tasks, return_exceptions=True)
            elapsed = time.perf_counter() - start_time

            # Count successful results
            successes = [r for r in results if isinstance(r, AnalyzeTextResult)]
            errors = [r for r in results if isinstance(r, Exception)]

            print(f"\nRPS Test: {len(successes)}/10 succeeded in {elapsed:.2f}s")
            if errors:
                print(f"Errors: {[type(e).__name__ for e in errors]}")

            # At least 8 out of 10 should succeed (allows for rate limiting)
            assert len(successes) >= 8, f"Too many failures: {len(errors)} errors"


# =============================================================================
# Retry Logic Tests (mocked)
# =============================================================================


class TestRetryLogic:
    """Tests for retry behavior on transient errors."""

    def test_retry_on_429_rate_limit(self) -> None:
        """Verify client retries on 429 status code."""
        attempt_count = [0]

        def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
            attempt_count[0] += 1
            if attempt_count[0] < 3:
                resp = DummyResponse(429, {"error": {"code": "rate_limit", "message": "slow down"}})
                resp.headers["Retry-After"] = "0.1"
                return resp
            return DummyResponse(200, {"answer": 0.5, "status": "success", "segmentation_tokens": None})

        session = DummySession(handler)
        client = ItsAIClient(api_key="sk_test", session=session, max_attempts=3)
        result = client.analyze_text("test text")

        assert attempt_count[0] == 3, "Should have retried twice"
        assert result.answer == 0.5

    def test_retry_on_500_server_error(self) -> None:
        """Verify client retries on 500 status code."""
        attempt_count = [0]

        def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
            attempt_count[0] += 1
            if attempt_count[0] < 2:
                return DummyResponse(500, {"error": {"code": "server_error", "message": "oops"}})
            return DummyResponse(200, {"answer": 0.7, "status": "success", "segmentation_tokens": None})

        session = DummySession(handler)
        client = ItsAIClient(api_key="sk_test", session=session, max_attempts=3)
        result = client.analyze_text("test text")

        assert attempt_count[0] == 2
        assert result.answer == 0.7

    def test_retry_on_503_service_unavailable(self) -> None:
        """Verify client retries on 503 status code."""
        attempt_count = [0]

        def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
            attempt_count[0] += 1
            if attempt_count[0] < 2:
                return DummyResponse(503, {"error": {"code": "service_unavailable", "message": "maintenance"}})
            return DummyResponse(200, {"answer": 0.3, "status": "success", "segmentation_tokens": None})

        session = DummySession(handler)
        client = ItsAIClient(api_key="sk_test", session=session, max_attempts=3)
        result = client.analyze_text("test text")

        assert attempt_count[0] == 2
        assert result.answer == 0.3

    def test_no_retry_on_400_client_error(self) -> None:
        """Verify client does NOT retry on 400 client errors."""
        attempt_count = [0]

        def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
            attempt_count[0] += 1
            return DummyResponse(400, {"error": {"code": "validation", "message": "bad request"}})

        session = DummySession(handler)
        client = ItsAIClient(api_key="sk_test", session=session, max_attempts=3)

        with pytest.raises(ValidationError):
            client.analyze_text("test text")

        assert attempt_count[0] == 1, "Should NOT retry on 400"

    def test_max_attempts_exhausted_raises(self) -> None:
        """Verify error is raised when all retry attempts fail."""
        attempt_count = [0]

        def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
            attempt_count[0] += 1
            return DummyResponse(500, {"error": {"code": "server_error", "message": "always fails"}})

        session = DummySession(handler)
        client = ItsAIClient(api_key="sk_test", session=session, max_attempts=3)

        with pytest.raises(ServerError):
            client.analyze_text("test text")

        assert attempt_count[0] == 3, "Should exhaust all attempts"


# =============================================================================
# Header Verification Tests
# =============================================================================


class TestHeaders:
    """Tests for correct HTTP headers being sent."""

    def test_authorization_header_format(self) -> None:
        """Verify Authorization header uses correct format."""
        captured_headers: Dict[str, str] = {}

        class HeaderCapturingSession:
            def post(self, url: str, headers: Dict[str, str] = None, **kwargs) -> DummyResponse:
                captured_headers.update(headers or {})
                return DummyResponse(200, {"answer": 0.5, "status": "success", "segmentation_tokens": None})
            def close(self) -> None:
                pass

        client = ItsAIClient(api_key="sk_test_key_123", session=HeaderCapturingSession())
        client.analyze_text("test")

        assert "Authorization" in captured_headers
        assert captured_headers["Authorization"] == "APIKey sk_test_key_123"

    def test_user_agent_header_present(self) -> None:
        """Verify User-Agent header is set with SDK version."""
        captured_headers: Dict[str, str] = {}

        class HeaderCapturingSession:
            def post(self, url: str, headers: Dict[str, str] = None, **kwargs) -> DummyResponse:
                captured_headers.update(headers or {})
                return DummyResponse(200, {"answer": 0.5, "status": "success", "segmentation_tokens": None})
            def close(self) -> None:
                pass

        client = ItsAIClient(api_key="sk_test", session=HeaderCapturingSession())
        client.analyze_text("test")

        assert "User-Agent" in captured_headers
        assert "its-ai-python-sdk/" in captured_headers["User-Agent"]

    def test_content_type_header(self) -> None:
        """Verify Content-Type is application/json."""
        captured_headers: Dict[str, str] = {}

        class HeaderCapturingSession:
            def post(self, url: str, headers: Dict[str, str] = None, **kwargs) -> DummyResponse:
                captured_headers.update(headers or {})
                return DummyResponse(200, {"answer": 0.5, "status": "success", "segmentation_tokens": None})
            def close(self) -> None:
                pass

        client = ItsAIClient(api_key="sk_test", session=HeaderCapturingSession())
        client.analyze_text("test")

        assert captured_headers.get("Content-Type") == "application/json"
        assert captured_headers.get("Accept") == "application/json"


# =============================================================================
# Concurrent Requests Tests
# =============================================================================


class TestConcurrency:
    """Tests for concurrent request handling."""

    @pytest.mark.anyio
    @pytest.mark.parametrize("anyio_backend", ["asyncio"])
    async def test_async_concurrent_requests(self, anyio_backend: str) -> None:
        """Verify async client handles concurrent requests correctly."""
        import asyncio

        request_count = [0]

        def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
            request_count[0] += 1
            return DummyResponse(200, {
                "answer": 0.5,
                "status": "success",
                "segmentation_tokens": None
            })

        async_client = DummyAsyncClient(handler)
        client = AsyncItsAIClient(api_key="sk_test", client=async_client)

        # Make 5 concurrent requests
        tasks = [client.analyze_text(f"text {i}") for i in range(5)]
        results = await asyncio.gather(*tasks)

        assert len(results) == 5
        assert request_count[0] == 5
        assert all(r.answer == 0.5 for r in results)

        await client.aclose()

    @pytest.mark.anyio
    @pytest.mark.parametrize("anyio_backend", ["asyncio"])
    async def test_async_batch_concurrent(self, anyio_backend: str) -> None:
        """Verify async batch handles concurrent batch requests."""
        import asyncio

        def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
            texts = payload.get("texts", [])
            return DummyResponse(200, {
                "answer": [
                    {"text": t, "answer": 0.5, "segmentation_tokens": None, "deep_scan": False}
                    for t in texts
                ],
                "error": None,
            })

        async_client = DummyAsyncClient(handler)
        client = AsyncItsAIClient(api_key="sk_test", client=async_client)

        # Make 3 concurrent batch requests
        tasks = [
            client.analyze_batch([f"batch{i}_text{j}" for j in range(3)])
            for i in range(3)
        ]
        results = await asyncio.gather(*tasks)

        assert len(results) == 3
        assert all(len(batch) == 3 for batch in results)

        await client.aclose()


# =============================================================================
# Edge Cases and Robustness Tests
# =============================================================================


class TestEdgeCases:
    """Tests for edge cases and robustness."""

    def test_very_long_text(self) -> None:
        """Test handling of very long text input."""
        long_text = "word " * 10000  # 50000+ characters

        def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
            assert len(payload["text"]) > 40000
            return DummyResponse(200, {"answer": 0.5, "status": "success", "segmentation_tokens": None})

        session = DummySession(handler)
        client = ItsAIClient(api_key="sk_test", session=session)
        result = client.analyze_text(long_text)
        assert result.answer == 0.5

    def test_unicode_text(self) -> None:
        """Test handling of Unicode characters in text."""
        unicode_text = "Hello 世界! Привет мир! مرحبا بالعالم! 🌍🌎🌏"

        def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
            assert "世界" in payload["text"]
            return DummyResponse(200, {"answer": 0.5, "status": "success", "segmentation_tokens": None})

        session = DummySession(handler)
        client = ItsAIClient(api_key="sk_test", session=session)
        result = client.analyze_text(unicode_text)
        assert result.answer == 0.5

    def test_special_characters_in_text(self) -> None:
        """Test handling of special characters."""
        special_text = 'Text with "quotes", <tags>, & ampersands, and newlines\n\ttabs'

        def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
            return DummyResponse(200, {"answer": 0.5, "status": "success", "segmentation_tokens": None})

        session = DummySession(handler)
        client = ItsAIClient(api_key="sk_test", session=session)
        result = client.analyze_text(special_text)
        assert result.answer == 0.5

    def test_answer_boundary_values(self) -> None:
        """Test that answer values at boundaries (0.0, 1.0) are handled."""
        for expected_answer in [0.0, 1.0, 0.5]:
            def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
                return DummyResponse(200, {
                    "answer": expected_answer,
                    "status": "success",
                    "segmentation_tokens": None
                })

            session = DummySession(handler)
            client = ItsAIClient(api_key="sk_test", session=session)
            result = client.analyze_text("test")
            assert result.answer == pytest.approx(expected_answer)

    def test_large_batch(self) -> None:
        """Test handling of large batch requests."""
        def handler(url: str, payload: Dict[str, Any]) -> DummyResponse:
            texts = payload["texts"]
            return DummyResponse(200, {
                "answer": [
                    {"text": t, "answer": 0.5, "segmentation_tokens": None, "deep_scan": False}
                    for t in texts
                ],
                "error": None,
            })

        session = DummySession(handler)
        client = ItsAIClient(api_key="sk_test", session=session)

        # Test with 100 items
        texts = [f"Sample text number {i} for batch testing" for i in range(100)]
        results = client.analyze_batch(texts)

        assert len(results) == 100
        assert all(r.answer == 0.5 for r in results)

