"""Tests for ExtractionService with mocked AIClient."""

from __future__ import annotations

import json
from unittest.mock import AsyncMock, MagicMock

import pytest
from ai_core.models import AIResponse, CostEstimate, Provider
from extraction.models import (
    ExtractionConfig,
    ExtractionField,
    ExtractionSchema,
    FieldType,
)
from extraction.service import ExtractionService

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


def _sample_schema() -> ExtractionSchema:
    return ExtractionSchema(
        name="invoice",
        description="Extract invoice data",
        fields=[
            ExtractionField(name="vendor", field_type=FieldType.TEXT, required=True),
            ExtractionField(name="amount", field_type=FieldType.CURRENCY, required=True),
            ExtractionField(name="date", field_type=FieldType.DATE, required=False),
        ],
    )


def _mock_ai_client(response_text: str) -> AIClient:  # noqa: F821
    """Create a mock AIClient that returns the given text."""
    client = MagicMock()
    client.chat = AsyncMock(
        return_value=AIResponse(
            text=response_text,
            tokens_used=CostEstimate(input_tokens=100, output_tokens=50, cost_usd=0.0005),
            provider_used=Provider.ANTHROPIC,
            model="claude-haiku-3-20250414",
            latency_ms=120.0,
        )
    )
    return client


# ---------------------------------------------------------------------------
# Extract
# ---------------------------------------------------------------------------


class TestExtract:
    async def test_successful_extraction(self):
        data = {"vendor": "Acme Corp", "amount": "USD 1500.00", "date": "2024-01-15"}
        client = _mock_ai_client(json.dumps(data))
        service = ExtractionService(client)

        result = await service.extract("Invoice from Acme Corp...", _sample_schema())

        assert result.schema_name == "invoice"
        assert result.data["vendor"] == "Acme Corp"
        assert result.data["amount"] == "USD 1500.00"
        assert result.model_used == "claude-haiku-3-20250414"
        assert result.tokens_used == 150
        assert result.cost_usd == 0.0005
        assert result.processing_time_ms >= 0
        assert result.warnings == []

    async def test_extraction_with_markdown_fences(self):
        data = {"vendor": "Test Inc", "amount": "EUR 200.00"}
        response_text = f"```json\n{json.dumps(data)}\n```"
        client = _mock_ai_client(response_text)
        service = ExtractionService(client)

        result = await service.extract("Some text", _sample_schema())

        assert result.data["vendor"] == "Test Inc"

    async def test_extraction_missing_required_field(self):
        data = {"vendor": "Acme"}  # missing 'amount'
        client = _mock_ai_client(json.dumps(data))
        service = ExtractionService(client)

        result = await service.extract("text", _sample_schema())

        assert any("amount" in w for w in result.warnings)

    async def test_extraction_extra_keys_warning(self):
        data = {"vendor": "Acme", "amount": "USD 100", "unexpected_key": "surprise"}
        client = _mock_ai_client(json.dumps(data))
        service = ExtractionService(client)

        result = await service.extract("text", _sample_schema())

        assert any("unexpected" in w.lower() for w in result.warnings)

    async def test_extraction_invalid_json(self):
        client = _mock_ai_client("This is not JSON at all")
        service = ExtractionService(client)

        with pytest.raises(Exception, match="invalid JSON"):
            await service.extract("text", _sample_schema())

    async def test_extraction_non_object_json(self):
        client = _mock_ai_client("[1, 2, 3]")
        service = ExtractionService(client)

        with pytest.raises(Exception, match="Expected JSON object"):
            await service.extract("text", _sample_schema())

    async def test_custom_model(self):
        data = {"vendor": "X", "amount": "USD 1"}
        client = _mock_ai_client(json.dumps(data))
        service = ExtractionService(client)

        await service.extract("text", _sample_schema(), model="gpt-4o-mini")

        call_kwargs = client.chat.call_args
        assert call_kwargs.kwargs.get("model") == "gpt-4o-mini"

    async def test_custom_config(self):
        data = {"vendor": "X", "amount": "USD 1"}
        client = _mock_ai_client(json.dumps(data))
        config = ExtractionConfig(temperature=0.5, max_tokens=2048)
        service = ExtractionService(client, config=config)

        await service.extract("text", _sample_schema())

        call_kwargs = client.chat.call_args
        assert call_kwargs.kwargs.get("temperature") == 0.5
        assert call_kwargs.kwargs.get("max_tokens") == 2048


# ---------------------------------------------------------------------------
# Batch
# ---------------------------------------------------------------------------


class TestExtractBatch:
    async def test_batch_extraction(self):
        data = {"vendor": "Batch", "amount": "USD 50"}
        client = _mock_ai_client(json.dumps(data))
        service = ExtractionService(client)

        results = await service.extract_batch(
            ["text 1", "text 2", "text 3"],
            _sample_schema(),
        )

        assert len(results) == 3
        assert all(r.schema_name == "invoice" for r in results)
        assert client.chat.call_count == 3

    async def test_batch_respects_concurrency(self):
        data = {"vendor": "X", "amount": "USD 1"}
        client = _mock_ai_client(json.dumps(data))
        service = ExtractionService(client)

        results = await service.extract_batch(
            ["a", "b"],
            _sample_schema(),
            concurrency=1,
        )

        assert len(results) == 2


# ---------------------------------------------------------------------------
# Classify
# ---------------------------------------------------------------------------


class TestClassify:
    async def test_successful_classification(self):
        response_data = {"category": "positive", "confidence": 0.95}
        client = _mock_ai_client(json.dumps(response_data))
        service = ExtractionService(client)

        result = await service.classify(
            "This product is amazing!",
            ["positive", "negative", "neutral"],
        )

        assert result["category"] == "positive"
        assert result["confidence"] == 0.95

    async def test_classification_uses_zero_temperature(self):
        response_data = {"category": "a", "confidence": 0.8}
        client = _mock_ai_client(json.dumps(response_data))
        service = ExtractionService(client)

        await service.classify("text", ["a", "b"])

        call_kwargs = client.chat.call_args
        assert call_kwargs.kwargs.get("temperature") == 0.0

    async def test_classification_invalid_json(self):
        client = _mock_ai_client("not json")
        service = ExtractionService(client)

        with pytest.raises(Exception, match="invalid JSON"):
            await service.classify("text", ["a", "b"])

    async def test_classification_non_object(self):
        client = _mock_ai_client('"just a string"')
        service = ExtractionService(client)

        with pytest.raises(Exception, match="Expected JSON object"):
            await service.classify("text", ["a", "b"])
