"""Tests for the GeoScored Python SDK client."""

import json

import httpx
import pytest

from geoscored import GeoScoredClient, GeoScoredError


def _mock_transport(responses: dict):
    """Create a mock httpx transport that returns preset responses."""

    async def handler(request: httpx.Request) -> httpx.Response:
        path = request.url.path
        method = request.method
        key = f"{method} {path}"
        if key in responses:
            status, body = responses[key]
            return httpx.Response(status, json=body)
        return httpx.Response(404, json={"error": {"type": "not_found", "code": "not_found", "message": "Not found", "request_id": "req_test"}})

    return httpx.MockTransport(handler)


MOCK_SCAN = {
    "id": "scan-123",
    "url": "https://example.com",
    "brand_name": "Example",
    "tier": "full",
    "status": "complete",
    "overall_score": 72.5,
    "grade": "C",
    "grade_descriptor": "AI systems can find your content.",
    "technical_score": 85.0,
    "structural_score": 60.0,
    "entity_score": 72.5,
    "seo_score": 74.0,
    "current_stage": "Done",
    "progress_pct": 100,
    "error_message": None,
    "created_at": "2026-01-15T12:00:00",
    "updated_at": "2026-01-15T12:01:30",
    "callback_url": None,
    "webhook_secret": None,
    "idempotency_key": None,
}

MOCK_CHECK = {
    "check_id": "technical.ai_crawler_access",
    "check_name": "AI Crawler Access",
    "pillar": "technical",
    "score": 90.0,
    "severity": "pass",
    "summary": "All AI crawlers allowed.",
    "details": {},
    "recommendations": [],
}

MOCK_REPORT = {
    "scan_id": "scan-123",
    "url": "https://example.com",
    "overall_score": 72.5,
    "overall_grade": "C",
    "grade_descriptor": "AI systems can find your content.",
    "technical_score": 85.0,
    "structural_score": 60.0,
    "entity_score": 72.5,
    "seo_score": 74.0,
    "total_checks": 1,
    "passing_checks": 1,
    "warning_checks": 0,
    "failing_checks": 0,
    "checks": [MOCK_CHECK],
    "access_level": "full",
    "is_lite": False,
}


@pytest.mark.asyncio
async def test_create_scan():
    """create_scan sends POST and returns Scan model."""
    transport = _mock_transport({
        "POST /api/v1/scans": (201, MOCK_SCAN),
    })
    async with httpx.AsyncClient(transport=transport, base_url="https://test/api/v1") as http:
        client = GeoScoredClient.__new__(GeoScoredClient)
        client._client = http
        scan = await client.create_scan("https://example.com", brand_name="Example")
        assert scan.id == "scan-123"
        assert scan.status == "complete"
        assert scan.overall_score == 72.5


@pytest.mark.asyncio
async def test_get_scan():
    """get_scan sends GET and returns Scan model."""
    transport = _mock_transport({
        "GET /api/v1/scans/scan-123": (200, MOCK_SCAN),
    })
    async with httpx.AsyncClient(transport=transport, base_url="https://test/api/v1") as http:
        client = GeoScoredClient.__new__(GeoScoredClient)
        client._client = http
        scan = await client.get_scan("scan-123")
        assert scan.id == "scan-123"


@pytest.mark.asyncio
async def test_list_scans():
    """list_scans sends GET and returns ScanList model."""
    transport = _mock_transport({
        "GET /api/v1/scans": (200, {
            "data": [MOCK_SCAN],
            "total": 1,
            "limit": 50,
            "offset": 0,
            "has_more": False,
        }),
    })
    async with httpx.AsyncClient(transport=transport, base_url="https://test/api/v1") as http:
        client = GeoScoredClient.__new__(GeoScoredClient)
        client._client = http
        result = await client.list_scans()
        assert result.total == 1
        assert len(result.data) == 1


@pytest.mark.asyncio
async def test_get_report():
    """get_report sends GET and returns Report model."""
    transport = _mock_transport({
        "GET /api/v1/scans/scan-123/report": (200, MOCK_REPORT),
    })
    async with httpx.AsyncClient(transport=transport, base_url="https://test/api/v1") as http:
        client = GeoScoredClient.__new__(GeoScoredClient)
        client._client = http
        report = await client.get_report("scan-123")
        assert report.overall_grade == "C"
        assert len(report.checks) == 1


@pytest.mark.asyncio
async def test_list_checks():
    """list_checks sends GET and returns CheckList model."""
    transport = _mock_transport({
        "GET /api/v1/scans/scan-123/checks": (200, {
            "data": [MOCK_CHECK],
            "total": 1,
            "scan_id": "scan-123",
        }),
    })
    async with httpx.AsyncClient(transport=transport, base_url="https://test/api/v1") as http:
        client = GeoScoredClient.__new__(GeoScoredClient)
        client._client = http
        result = await client.list_checks("scan-123")
        assert result.total == 1
        assert result.data[0].check_id == "technical.ai_crawler_access"


@pytest.mark.asyncio
async def test_get_check():
    """get_check sends GET and returns Check model."""
    transport = _mock_transport({
        "GET /api/v1/scans/scan-123/checks/technical.ai_crawler_access": (200, MOCK_CHECK),
    })
    async with httpx.AsyncClient(transport=transport, base_url="https://test/api/v1") as http:
        client = GeoScoredClient.__new__(GeoScoredClient)
        client._client = http
        check = await client.get_check("scan-123", "technical.ai_crawler_access")
        assert check.score == 90.0


@pytest.mark.asyncio
async def test_error_handling():
    """API errors raise GeoScoredError with structured fields."""
    error_body = {
        "error": {
            "type": "not_found",
            "code": "scan_not_found",
            "message": "Scan not found.",
            "param": "scan_id",
            "request_id": "req_abc123",
            "doc_url": "https://geoscored.ai/docs/api#scan_not_found",
        }
    }
    transport = _mock_transport({
        "GET /api/v1/scans/bad-id": (404, error_body),
    })
    async with httpx.AsyncClient(transport=transport, base_url="https://test/api/v1") as http:
        client = GeoScoredClient.__new__(GeoScoredClient)
        client._client = http
        with pytest.raises(GeoScoredError) as exc_info:
            await client.get_scan("bad-id")
        err = exc_info.value
        assert err.status_code == 404
        assert err.code == "scan_not_found"
        assert err.request_id == "req_abc123"


MOCK_BATCH = {
    "results": [
        {"index": 0, "scan": MOCK_SCAN, "error": None},
        {"index": 1, "scan": None, "error": {"code": "invalid_url", "message": "Bad URL", "param": "url"}},
    ],
    "total": 2,
    "succeeded": 1,
    "failed": 1,
}

MOCK_COMPARE = {
    "scan_a": MOCK_SCAN,
    "scan_b": dict(MOCK_SCAN, id="scan-456", overall_score=85.0, grade="B"),
    "overall_delta": 12.5,
    "overall_winner": "b",
    "pillars": [
        {"pillar": "technical", "score_a": 85.0, "score_b": 90.0, "delta": 5.0, "winner": "tie"},
    ],
    "checks": [
        {"check_id": "technical.robots_txt", "check_name": "AI Crawler Access", "pillar": "technical", "score_a": 90.0, "score_b": 95.0, "delta": 5.0, "winner": "tie"},
    ],
    "warnings": [],
}


@pytest.mark.asyncio
async def test_batch_create_scans():
    """batch_create_scans sends POST and returns BatchResult model."""
    transport = _mock_transport({
        "POST /api/v1/scans/batch": (201, {
            "results": [{"index": 0, "scan": MOCK_SCAN, "error": None}],
            "total": 1,
            "succeeded": 1,
            "failed": 0,
        }),
    })
    async with httpx.AsyncClient(transport=transport, base_url="https://test/api/v1") as http:
        client = GeoScoredClient.__new__(GeoScoredClient)
        client._client = http
        result = await client.batch_create_scans([{"url": "https://example.com", "brand_name": "Example"}])
        assert result.total == 1
        assert result.succeeded == 1
        assert result.results[0].scan.id == "scan-123"


@pytest.mark.asyncio
async def test_batch_partial_failure():
    """batch_create_scans handles mixed results."""
    transport = _mock_transport({
        "POST /api/v1/scans/batch": (200, MOCK_BATCH),
    })
    async with httpx.AsyncClient(transport=transport, base_url="https://test/api/v1") as http:
        client = GeoScoredClient.__new__(GeoScoredClient)
        client._client = http
        result = await client.batch_create_scans([
            {"url": "https://example.com", "brand_name": "Good"},
            {"url": "bad", "brand_name": "Bad"},
        ])
        assert result.succeeded == 1
        assert result.failed == 1
        assert result.results[0].scan is not None
        assert result.results[1].error is not None


@pytest.mark.asyncio
async def test_compare_scans():
    """compare_scans sends POST and returns CompareResult model."""
    transport = _mock_transport({
        "POST /api/v1/scans/compare": (200, MOCK_COMPARE),
    })
    async with httpx.AsyncClient(transport=transport, base_url="https://test/api/v1") as http:
        client = GeoScoredClient.__new__(GeoScoredClient)
        client._client = http
        result = await client.compare_scans("scan-123", "scan-456")
        assert result.overall_winner == "b"
        assert result.overall_delta == 12.5
        assert result.scan_a.id == "scan-123"
        assert result.scan_b.id == "scan-456"
        assert len(result.pillars) == 1


@pytest.mark.asyncio
async def test_compare_error():
    """compare_scans raises GeoScoredError on API error."""
    error_body = {
        "error": {
            "type": "invalid_request",
            "code": "compare_same_scan",
            "message": "Cannot compare a scan with itself.",
            "request_id": "req_123",
        }
    }
    transport = _mock_transport({
        "POST /api/v1/scans/compare": (422, error_body),
    })
    async with httpx.AsyncClient(transport=transport, base_url="https://test/api/v1") as http:
        client = GeoScoredClient.__new__(GeoScoredClient)
        client._client = http
        with pytest.raises(GeoScoredError) as exc_info:
            await client.compare_scans("scan-a", "scan-a")
        assert exc_info.value.code == "compare_same_scan"


@pytest.mark.asyncio
async def test_poll_scan_completes():
    """poll_scan loops until scan is complete."""
    call_count = 0
    pending_scan = dict(MOCK_SCAN, status="running")
    complete_scan = dict(MOCK_SCAN, status="complete")

    async def handler(request: httpx.Request) -> httpx.Response:
        nonlocal call_count
        call_count += 1
        if call_count < 3:
            return httpx.Response(200, json=pending_scan)
        return httpx.Response(200, json=complete_scan)

    transport = httpx.MockTransport(handler)
    async with httpx.AsyncClient(transport=transport, base_url="https://test/api/v1") as http:
        client = GeoScoredClient.__new__(GeoScoredClient)
        client._client = http
        scan = await client.poll_scan("scan-123", interval=0.01, timeout=5)
        assert scan.status == "complete"
        assert call_count == 3
