"""Tests for the AmbientMeta Python SDK."""

import pytest
import httpx
import respx

from ambientmeta import (
    AmbientMeta,
    AsyncAmbientMeta,
    AmbientMetaError,
    AuthenticationError,
    BatchFeedbackResponse,
    NotFoundError,
    RateLimitError,
    SanitizeResponse,
    ValidationError,
)


BASE_URL = "https://test-api.example.com"


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

SANITIZE_RESPONSE_JSON = {
    "sanitized": "Email [EMAIL_1]",
    "session_id": "ses_123",
    "entities_found": 1,
    "entities": [
        {"placeholder": "[EMAIL_1]", "type": "EMAIL", "confidence": 0.99}
    ],
    "processing_ms": 5.0,
}

REDACT_RESPONSE_JSON = {
    "sanitized": "Email [EMAIL_1]",
    "session_id": None,
    "redacted": True,
    "entities_found": 1,
    "entities": [
        {"placeholder": "[EMAIL_1]", "type": "EMAIL", "confidence": 0.99}
    ],
    "processing_ms": 5.0,
}

BATCH_FEEDBACK_RESPONSE_JSON = {
    "accepted": 2,
    "correction_ids": ["cor_aaa", "cor_bbb"],
    "contradictions_detected": 1,
    "message": "Thank you. 1 contradiction detected.",
}

INSIGHTS_RESPONSE_JSON = {
    "pending_conflicts": [
        {
            "id": "ins_abc",
            "type": "contradiction_cluster",
            "title": "PHONE vs NPI",
            "description": "Ambiguity detected",
            "status": "pending",
            "data": {"entity_type_a": "PHONE"},
            "created_at": "2026-02-13T12:00:00Z",
        }
    ],
    "proposed_improvements": [],
    "discovered_patterns": [],
    "knowledge_depth": {
        "ckd_score": 0.42,
        "maturity": "developing",
        "strongest_entity": {"type": "EMAIL", "confidence": 0.99},
        "weakest_entity": {"type": "NPI", "confidence": 0.71},
        "contradictions_pending": 3,
        "contradictions_resolved": 12,
        "golden_set_size": 234,
        "compiled_rules": 18,
        "on_demand_hit_rate": 0.45,
    },
}


# ---------------------------------------------------------------------------
# Sync client tests
# ---------------------------------------------------------------------------


class TestSyncClient:
    def setup_method(self):
        self.client = AmbientMeta(api_key="test-key", base_url=BASE_URL)

    def teardown_method(self):
        self.client.close()

    @respx.mock
    def test_sanitize(self):
        respx.post(f"{BASE_URL}/v1/sanitize").mock(
            return_value=httpx.Response(200, json=SANITIZE_RESPONSE_JSON)
        )
        result = self.client.sanitize("Email john@acme.com")
        assert result.sanitized == "Email [EMAIL_1]"
        assert result.session_id == "ses_123"
        assert result.entities_found == 1
        assert result.entities[0].type == "EMAIL"
        assert result.entities[0].confidence == 0.99
        assert result.redacted is False
        # Client reference is injected
        assert result._client is self.client

    @respx.mock
    def test_sanitize_with_entities(self):
        respx.post(f"{BASE_URL}/v1/sanitize").mock(
            return_value=httpx.Response(
                200,
                json={
                    "sanitized": "text",
                    "session_id": "ses_456",
                    "entities_found": 0,
                    "entities": [],
                    "processing_ms": 1.0,
                },
            )
        )
        result = self.client.sanitize("text", entities=["EMAIL", "PHONE"])
        assert result.entities_found == 0

    @respx.mock
    def test_sanitize_redact_mode(self):
        respx.post(f"{BASE_URL}/v1/sanitize").mock(
            return_value=httpx.Response(200, json=REDACT_RESPONSE_JSON)
        )
        result = self.client.sanitize("Email john@acme.com", mode="redact")
        assert result.redacted is True
        assert result.session_id is None

    @respx.mock
    def test_rehydrate(self):
        respx.post(f"{BASE_URL}/v1/rehydrate").mock(
            return_value=httpx.Response(
                200,
                json={
                    "text": "Email john@acme.com",
                    "entities_restored": 1,
                    "processing_ms": 2.0,
                },
            )
        )
        result = self.client.rehydrate("Email [EMAIL_1]", "ses_123")
        assert result.text == "Email john@acme.com"
        assert result.entities_restored == 1

    @respx.mock
    def test_create_pattern(self):
        respx.post(f"{BASE_URL}/v1/patterns").mock(
            return_value=httpx.Response(
                200,
                json={
                    "pattern_id": "pat_abc",
                    "name": "EMPLOYEE_ID",
                    "status": "active",
                },
            )
        )
        result = self.client.create_pattern(
            name="EMPLOYEE_ID",
            pattern=r"EMP-\d{6}",
            description="Employee ID",
            examples=["EMP-123456"],
        )
        assert result.pattern_id == "pat_abc"
        assert result.name == "EMPLOYEE_ID"
        assert result.status == "active"

    @respx.mock
    def test_send_feedback(self):
        respx.post(f"{BASE_URL}/v1/feedback").mock(
            return_value=httpx.Response(200, json={"status": "recorded"})
        )
        result = self.client.send_feedback(
            session_id="ses_123",
            feedback_type="missed_entity",
            text_snippet="Dr. Jane",
            expected_type="PERSON",
        )
        assert result.status == "recorded"

    @respx.mock
    def test_auth_error(self):
        respx.post(f"{BASE_URL}/v1/sanitize").mock(
            return_value=httpx.Response(
                401,
                json={
                    "error": {
                        "code": "invalid_api_key",
                        "message": "Invalid API key",
                        "suggestion": "Check your key",
                    }
                },
            )
        )
        with pytest.raises(AuthenticationError) as exc_info:
            self.client.sanitize("text")
        assert exc_info.value.code == "invalid_api_key"

    @respx.mock
    def test_rate_limit_error(self):
        respx.post(f"{BASE_URL}/v1/sanitize").mock(
            return_value=httpx.Response(
                429,
                json={
                    "error": {
                        "code": "rate_limited",
                        "message": "Rate limit exceeded",
                    }
                },
            )
        )
        with pytest.raises(RateLimitError):
            self.client.sanitize("text")

    @respx.mock
    def test_not_found_error(self):
        respx.post(f"{BASE_URL}/v1/rehydrate").mock(
            return_value=httpx.Response(
                404,
                json={
                    "error": {
                        "code": "session_not_found",
                        "message": "Session not found",
                    }
                },
            )
        )
        with pytest.raises(NotFoundError):
            self.client.rehydrate("[PERSON_1]", "ses_expired")

    @respx.mock
    def test_validation_error(self):
        respx.post(f"{BASE_URL}/v1/sanitize").mock(
            return_value=httpx.Response(
                422,
                json={
                    "error": {
                        "code": "validation_error",
                        "message": "Invalid input",
                    }
                },
            )
        )
        with pytest.raises(ValidationError):
            self.client.sanitize("")

    @respx.mock
    def test_generic_error(self):
        respx.post(f"{BASE_URL}/v1/sanitize").mock(
            return_value=httpx.Response(
                500,
                json={
                    "error": {
                        "code": "internal_error",
                        "message": "Unexpected error",
                    }
                },
            )
        )
        with pytest.raises(AmbientMetaError):
            self.client.sanitize("text")

    def test_context_manager(self):
        with AmbientMeta(api_key="key", base_url=BASE_URL) as client:
            assert client is not None

    # --- Correction methods (sync) ---

    @respx.mock
    def test_correct_and_submit(self):
        respx.post(f"{BASE_URL}/v1/sanitize").mock(
            return_value=httpx.Response(200, json=SANITIZE_RESPONSE_JSON)
        )
        respx.post(f"{BASE_URL}/v1/feedback/batch").mock(
            return_value=httpx.Response(200, json=BATCH_FEEDBACK_RESPONSE_JSON)
        )

        result = self.client.sanitize("Email john@acme.com")
        fb = result.correct("[EMAIL_1]", "EMPLOYEE_ID").submit_corrections()

        assert isinstance(fb, BatchFeedbackResponse)
        assert fb.accepted == 2
        assert fb.contradictions_detected == 1
        assert len(fb.correction_ids) == 2
        # Corrections list cleared after submit
        assert len(result._corrections) == 0

    @respx.mock
    def test_chained_corrections(self):
        respx.post(f"{BASE_URL}/v1/sanitize").mock(
            return_value=httpx.Response(200, json=SANITIZE_RESPONSE_JSON)
        )
        respx.post(f"{BASE_URL}/v1/feedback/batch").mock(
            return_value=httpx.Response(200, json=BATCH_FEEDBACK_RESPONSE_JSON)
        )

        result = self.client.sanitize("Email john@acme.com")
        result.correct("[EMAIL_1]", "NPI").report_missed("EMP-123", "EMPLOYEE_ID").report_false_positive("[EMAIL_1]")

        assert len(result._corrections) == 3
        assert result._corrections[0]["feedback_type"] == "misclassification"
        assert result._corrections[1]["feedback_type"] == "missed_entity"
        assert result._corrections[2]["feedback_type"] == "false_positive"

        fb = result.submit_corrections()
        assert fb.accepted == 2
        assert len(result._corrections) == 0

    def test_submit_no_corrections_raises(self):
        result = SanitizeResponse(
            sanitized="text",
            session_id="ses_123",
            entities_found=0,
            entities=[],
            processing_ms=1.0,
            _client=self.client,
        )
        with pytest.raises(ValueError, match="No corrections"):
            result.submit_corrections()

    def test_submit_no_client_raises(self):
        result = SanitizeResponse(
            sanitized="text",
            session_id="ses_123",
            entities_found=0,
            entities=[],
            processing_ms=1.0,
        )
        result.correct("[X_1]", "Y")
        with pytest.raises(RuntimeError, match="not created by a client"):
            result.submit_corrections()

    def test_submit_redacted_session_raises(self):
        result = SanitizeResponse(
            sanitized="text",
            session_id=None,
            entities_found=0,
            entities=[],
            processing_ms=1.0,
            redacted=True,
            _client=self.client,
        )
        result.correct("[X_1]", "Y")
        with pytest.raises(ValueError, match="redacted sessions"):
            result.submit_corrections()

    # --- Insights (sync) ---

    @respx.mock
    def test_get_insights(self):
        respx.get(f"{BASE_URL}/v1/insights").mock(
            return_value=httpx.Response(200, json=INSIGHTS_RESPONSE_JSON)
        )
        result = self.client.get_insights()
        assert len(result.pending_conflicts) == 1
        assert result.pending_conflicts[0].id == "ins_abc"
        assert result.pending_conflicts[0].type == "contradiction_cluster"
        assert result.knowledge_depth.ckd_score == 0.42
        assert result.knowledge_depth.maturity == "developing"
        assert result.knowledge_depth.strongest_entity.type == "EMAIL"
        assert result.knowledge_depth.strongest_entity.confidence == 0.99
        assert result.knowledge_depth.weakest_entity.type == "NPI"
        assert result.knowledge_depth.compiled_rules == 18
        assert result.knowledge_depth.on_demand_hit_rate == 0.45

    @respx.mock
    def test_resolve_insight(self):
        respx.post(f"{BASE_URL}/v1/insights/ins_abc/resolve").mock(
            return_value=httpx.Response(
                200, json={"status": "resolved", "golden_set_entries_created": 2}
            )
        )
        result = self.client.resolve_insight("ins_abc", "approve")
        assert result["status"] == "resolved"
        assert result["golden_set_entries_created"] == 2


# ---------------------------------------------------------------------------
# Async client tests
# ---------------------------------------------------------------------------


class TestAsyncClient:
    @respx.mock
    @pytest.mark.asyncio
    async def test_sanitize(self):
        respx.post(f"{BASE_URL}/v1/sanitize").mock(
            return_value=httpx.Response(200, json=SANITIZE_RESPONSE_JSON)
        )
        async with AsyncAmbientMeta(api_key="test-key", base_url=BASE_URL) as client:
            result = await client.sanitize("Email john@acme.com")
            assert result.sanitized == "Email [EMAIL_1]"
            assert result.session_id == "ses_123"
            assert result._client is client

    @respx.mock
    @pytest.mark.asyncio
    async def test_sanitize_redact_mode(self):
        respx.post(f"{BASE_URL}/v1/sanitize").mock(
            return_value=httpx.Response(200, json=REDACT_RESPONSE_JSON)
        )
        async with AsyncAmbientMeta(api_key="test-key", base_url=BASE_URL) as client:
            result = await client.sanitize("Email john@acme.com", mode="redact")
            assert result.redacted is True
            assert result.session_id is None

    @respx.mock
    @pytest.mark.asyncio
    async def test_rehydrate(self):
        respx.post(f"{BASE_URL}/v1/rehydrate").mock(
            return_value=httpx.Response(
                200,
                json={
                    "text": "Email john@acme.com",
                    "entities_restored": 1,
                    "processing_ms": 2.0,
                },
            )
        )
        async with AsyncAmbientMeta(api_key="test-key", base_url=BASE_URL) as client:
            result = await client.rehydrate("Email [EMAIL_1]", "ses_123")
            assert result.text == "Email john@acme.com"

    @respx.mock
    @pytest.mark.asyncio
    async def test_create_pattern(self):
        respx.post(f"{BASE_URL}/v1/patterns").mock(
            return_value=httpx.Response(
                200,
                json={
                    "pattern_id": "pat_abc",
                    "name": "TEST_PAT",
                    "status": "active",
                },
            )
        )
        async with AsyncAmbientMeta(api_key="test-key", base_url=BASE_URL) as client:
            result = await client.create_pattern("TEST_PAT", r"\d+")
            assert result.name == "TEST_PAT"

    @respx.mock
    @pytest.mark.asyncio
    async def test_send_feedback(self):
        respx.post(f"{BASE_URL}/v1/feedback").mock(
            return_value=httpx.Response(200, json={"status": "recorded"})
        )
        async with AsyncAmbientMeta(api_key="test-key", base_url=BASE_URL) as client:
            result = await client.send_feedback("ses_1", "missed_entity", "text")
            assert result.status == "recorded"

    @respx.mock
    @pytest.mark.asyncio
    async def test_auth_error(self):
        respx.post(f"{BASE_URL}/v1/sanitize").mock(
            return_value=httpx.Response(
                401,
                json={"error": {"code": "invalid_api_key", "message": "Bad key"}},
            )
        )
        async with AsyncAmbientMeta(api_key="bad-key", base_url=BASE_URL) as client:
            with pytest.raises(AuthenticationError):
                await client.sanitize("text")

    # --- Correction methods (async) ---

    @respx.mock
    @pytest.mark.asyncio
    async def test_correct_and_asubmit(self):
        respx.post(f"{BASE_URL}/v1/sanitize").mock(
            return_value=httpx.Response(200, json=SANITIZE_RESPONSE_JSON)
        )
        respx.post(f"{BASE_URL}/v1/feedback/batch").mock(
            return_value=httpx.Response(200, json=BATCH_FEEDBACK_RESPONSE_JSON)
        )

        async with AsyncAmbientMeta(api_key="test-key", base_url=BASE_URL) as client:
            result = await client.sanitize("Email john@acme.com")
            result.correct("[EMAIL_1]", "NPI").report_missed("EMP-123", "EMPLOYEE_ID")

            fb = await result.asubmit_corrections()
            assert isinstance(fb, BatchFeedbackResponse)
            assert fb.accepted == 2
            assert len(result._corrections) == 0

    @pytest.mark.asyncio
    async def test_asubmit_no_corrections_raises(self):
        result = SanitizeResponse(
            sanitized="text",
            session_id="ses_123",
            entities_found=0,
            entities=[],
            processing_ms=1.0,
            _client=object(),  # dummy client
        )
        with pytest.raises(ValueError, match="No corrections"):
            await result.asubmit_corrections()

    @pytest.mark.asyncio
    async def test_asubmit_redacted_raises(self):
        result = SanitizeResponse(
            sanitized="text",
            session_id=None,
            entities_found=0,
            entities=[],
            processing_ms=1.0,
            redacted=True,
            _client=object(),
        )
        result.correct("[X_1]", "Y")
        with pytest.raises(ValueError, match="redacted sessions"):
            await result.asubmit_corrections()

    # --- Insights (async) ---

    @respx.mock
    @pytest.mark.asyncio
    async def test_get_insights(self):
        respx.get(f"{BASE_URL}/v1/insights").mock(
            return_value=httpx.Response(200, json=INSIGHTS_RESPONSE_JSON)
        )
        async with AsyncAmbientMeta(api_key="test-key", base_url=BASE_URL) as client:
            result = await client.get_insights()
            assert result.knowledge_depth.ckd_score == 0.42
            assert result.knowledge_depth.strongest_entity.type == "EMAIL"
            assert len(result.pending_conflicts) == 1

    @respx.mock
    @pytest.mark.asyncio
    async def test_resolve_insight(self):
        respx.post(f"{BASE_URL}/v1/insights/ins_abc/resolve").mock(
            return_value=httpx.Response(
                200, json={"status": "resolved", "golden_set_entries_created": 1}
            )
        )
        async with AsyncAmbientMeta(api_key="test-key", base_url=BASE_URL) as client:
            result = await client.resolve_insight(
                "ins_abc", "refine", modifications={"keywords": ["clinic"]}
            )
            assert result["status"] == "resolved"
