"""
BiasClear Client SDK — Tests

Two-layer testing strategy:
1. Unit tests using respx to mock HTTP responses — tests SDK parsing, error handling
2. Integration test that verifies SDK → real API stack through TestClient

This covers:
- Response model parsing (ScanResult, CorrectionResult, PatternsResult, AuditResult)
- Error mapping (401 → AuthError, 429 → RateLimitError, 422 → BiasClearError)
- Context manager lifecycle
- Integration with the live BiasClear engine
"""

from __future__ import annotations

import pytest
import httpx
import respx

from biasclear_client import (
    Client,
    AuthError,
    RateLimitError,
    BiasClearError,
    ScanResult,
    CorrectionResult,
    PatternsResult,
    AuditResult,
)
from biasclear_client.models import Flag, ChainIntegrity


# ── Mock response payloads ───────────────────────────────────────────

HEALTH_RESPONSE = {
    "status": "operational",
    "biasclear": "1.0.0",
    "audit_entries": 42,
}

SCAN_RESPONSE = {
    "text": "Everyone agrees this is settled law.",
    "truth_score": 12,
    "knowledge_type": "sense",
    "bias_detected": True,
    "bias_types": ["CONSENSUS_AS_EVIDENCE"],
    "pit_tier": "tier_1_ideological",
    "pit_detail": "",
    "severity": "high",
    "confidence": 0.92,
    "explanation": "Detected structural distortions.",
    "flags": [
        {
            "category": "structural",
            "pattern_id": "LEGAL_CONSENSUS_AS_EVIDENCE",
            "matched_text": "Everyone agrees",
            "pit_tier": 1,
            "severity": "high",
        }
    ],
    "impact_projection": None,
    "scan_mode": "local",
    "source": "local",
    "core_version": "1.0.0",
}

CORRECT_RESPONSE = {
    "corrected": True,
    "original_text": "Everyone agrees this is settled law.",
    "corrected_text": "The court held that the statute applies.",
    "truth_score_before": 12,
    "truth_score_after": 92,
    "diff_spans": [{"type": "delete", "text": "Everyone agrees"}],
    "changes_made": ["Removed consensus framing"],
    "iterations": 1,
    "reason": "Bias corrected.",
}

PATTERNS_RESPONSE = {
    "domain": "legal",
    "core_version": "1.0.0",
    "frozen_patterns": [
        {"id": "LEGAL_CONSENSUS", "name": "Consensus Framing",
         "description": "...", "pit_tier": 1, "severity": "high"},
    ],
    "learned_patterns": [],
    "frozen_count": 20,
    "learned_count": 0,
    "learning_ring": {
        "total_patterns": 0,
        "staging": 0,
        "active": 0,
        "deactivated": 0,
        "activation_threshold": 10,
        "fp_limit": 3,
    },
}

AUDIT_RESPONSE = {
    "entries": [
        {
            "id": "abc-123",
            "hash": "sha256:...",
            "prev_hash": "sha256:...",
            "event_type": "scan_local",
            "data": {"text": "test"},
            "timestamp": "2026-02-17T12:00:00Z",
            "core_version": "1.0.0",
        }
    ],
    "count": 1,
    "total_entries": 100,
    "chain_integrity": {
        "verified": True,
        "entries_checked": 100,
        "broken_links": [],
    },
    "core_version": "1.0.0",
}


# ── Fixtures ─────────────────────────────────────────────────────────

@pytest.fixture
def bc():
    """Client with mocked HTTP layer via respx."""
    with respx.mock(base_url="http://test") as mock:
        client = Client(base_url="http://test")
        yield client, mock
        client.close()


# ============================================================
# health()
# ============================================================

class TestHealth:

    def test_health_returns_dict(self, bc):
        client, mock = bc
        mock.get("/health").respond(json=HEALTH_RESPONSE)
        result = client.health()
        assert isinstance(result, dict)
        assert result["status"] == "operational"

    def test_health_has_required_fields(self, bc):
        client, mock = bc
        mock.get("/health").respond(json=HEALTH_RESPONSE)
        result = client.health()
        assert result["biasclear"] == "1.0.0"
        assert result["audit_entries"] == 42


# ============================================================
# scan()
# ============================================================

class TestScan:

    def test_scan_returns_scan_result(self, bc):
        client, mock = bc
        mock.post("/scan").respond(json=SCAN_RESPONSE)
        result = client.scan("Everyone agrees this is settled law.", domain="legal")
        assert isinstance(result, ScanResult)

    def test_scan_fields_parsed(self, bc):
        client, mock = bc
        mock.post("/scan").respond(json=SCAN_RESPONSE)
        result = client.scan("test")
        assert result.bias_detected is True
        assert result.truth_score == 12
        assert result.severity == "high"
        assert result.scan_mode == "local"
        assert result.core_version == "1.0.0"

    def test_scan_flags_are_typed(self, bc):
        client, mock = bc
        mock.post("/scan").respond(json=SCAN_RESPONSE)
        result = client.scan("test")
        assert len(result.flags) == 1
        flag = result.flags[0]
        assert isinstance(flag, Flag)
        assert flag.pattern_id == "LEGAL_CONSENSUS_AS_EVIDENCE"
        assert flag.severity == "high"

    def test_scan_sends_correct_payload(self, bc):
        client, mock = bc
        route = mock.post("/scan").respond(json=SCAN_RESPONSE)
        client.scan("some text", domain="financial", mode="full")
        req = route.calls[0].request
        import json
        body = json.loads(req.content)
        assert body["text"] == "some text"
        assert body["domain"] == "financial"
        assert body["mode"] == "full"


# ============================================================
# correct()
# ============================================================

class TestCorrect:

    def test_correct_returns_correction_result(self, bc):
        client, mock = bc
        mock.post("/correct").respond(json=CORRECT_RESPONSE)
        result = client.correct("biased text")
        assert isinstance(result, CorrectionResult)

    def test_correct_fields_parsed(self, bc):
        client, mock = bc
        mock.post("/correct").respond(json=CORRECT_RESPONSE)
        result = client.correct("biased text")
        assert result.corrected is True
        assert result.truth_score_before == 12
        assert result.truth_score_after == 92
        assert len(result.diff_spans) == 1
        assert result.diff_spans[0].type == "delete"


# ============================================================
# patterns()
# ============================================================

class TestPatterns:

    def test_patterns_returns_patterns_result(self, bc):
        client, mock = bc
        mock.get("/patterns").respond(json=PATTERNS_RESPONSE)
        result = client.patterns("legal")
        assert isinstance(result, PatternsResult)

    def test_patterns_fields_parsed(self, bc):
        client, mock = bc
        mock.get("/patterns").respond(json=PATTERNS_RESPONSE)
        result = client.patterns("legal")
        assert result.domain == "legal"
        assert result.frozen_count == 20
        assert result.learning_ring.activation_threshold == 10


# ============================================================
# audit()
# ============================================================

class TestAudit:

    def test_audit_returns_audit_result(self, bc):
        client, mock = bc
        mock.get("/audit").respond(json=AUDIT_RESPONSE)
        result = client.audit()
        assert isinstance(result, AuditResult)

    def test_audit_chain_integrity(self, bc):
        client, mock = bc
        mock.get("/audit").respond(json=AUDIT_RESPONSE)
        result = client.audit()
        assert isinstance(result.chain_integrity, ChainIntegrity)
        assert result.chain_integrity.verified is True
        assert result.chain_integrity.entries_checked == 100

    def test_audit_entries_parsed(self, bc):
        client, mock = bc
        mock.get("/audit").respond(json=AUDIT_RESPONSE)
        result = client.audit()
        assert result.count == 1
        assert result.entries[0].event_type == "scan_local"


# ============================================================
# Error Handling
# ============================================================

class TestErrors:

    def test_auth_error_on_401(self, bc):
        client, mock = bc
        mock.post("/scan").respond(
            status_code=401,
            json={"detail": "Invalid API key."},
            headers={"www-authenticate": "Bearer"},
        )
        with pytest.raises(AuthError) as exc_info:
            client.scan("test")
        assert exc_info.value.status_code == 401

    def test_rate_limit_error_on_429(self, bc):
        client, mock = bc
        mock.post("/scan").respond(
            status_code=429,
            json={"detail": "Rate limit exceeded: 100 requests per minute."},
            headers={"retry-after": "42"},
        )
        with pytest.raises(RateLimitError) as exc_info:
            client.scan("test")
        assert exc_info.value.status_code == 429
        assert exc_info.value.retry_after == 42

    def test_validation_error_on_422(self, bc):
        client, mock = bc
        mock.post("/scan").respond(
            status_code=422,
            json={"detail": [{"msg": "field required"}]},
        )
        with pytest.raises(BiasClearError) as exc_info:
            client.scan("")
        assert exc_info.value.status_code == 422

    def test_server_error_on_500(self, bc):
        client, mock = bc
        from biasclear_client import ServerError
        mock.post("/scan").respond(
            status_code=500,
            json={"detail": "Internal server error"},
        )
        with pytest.raises(ServerError) as exc_info:
            client.scan("test")
        assert exc_info.value.status_code == 500

    def test_bad_request_error_on_400(self, bc):
        client, mock = bc
        mock.post("/scan").respond(
            status_code=400,
            json={"detail": "Invalid scan mode"},
        )
        with pytest.raises(BiasClearError) as exc_info:
            client.scan("test", mode="invalid")
        assert exc_info.value.status_code == 400


# ============================================================
# Integration — Full Stack (SDK → API → Engine)
# ============================================================

class TestIntegration:
    """
    Integration tests that hit the real BiasClear engine
    through FastAPI TestClient.
    """

    @pytest.fixture
    def live_client(self):
        from fastapi.testclient import TestClient as FastAPITestClient
        from biasclear.api import app
        with FastAPITestClient(app) as tc:
            yield tc

    def test_scan_integration(self, live_client):
        """SDK response model matches real API output."""
        r = live_client.post("/scan", json={
            "text": "Everyone agrees this is well-settled law.",
            "domain": "legal",
            "mode": "local",
        })
        assert r.status_code == 200
        # Parse through SDK model
        result = ScanResult.from_dict(r.json())
        assert isinstance(result, ScanResult)
        assert result.bias_detected is True
        assert len(result.flags) > 0

    def test_patterns_integration(self, live_client):
        r = live_client.get("/patterns")
        assert r.status_code == 200
        result = PatternsResult.from_dict(r.json())
        assert result.frozen_count > 0

    def test_audit_integration(self, live_client):
        r = live_client.get("/audit")
        assert r.status_code == 200
        result = AuditResult.from_dict(r.json())
        assert isinstance(result.chain_integrity, ChainIntegrity)
