import ast
import os
import pytest
from unittest.mock import AsyncMock, MagicMock, patch

from app.core.ai.domain.models import (
    AiCapability,
    AiModelSpec,
    AiProviderType,
    AiResponse,
    AiStructuredRequest,
    AiTextRequest,
    AiVisionRequest,
)
from app.core.ai.infrastructure.circuit_breaker import ProviderCircuitBreaker
from app.core.ai.infrastructure.gateway import EnterpriseAiGateway, get_ai_gateway
from app.core.ai.infrastructure.registry import CanonicalModelRegistry
from app.modules.core.application.ocr.multimodal_ocr_service import MultimodalOcrService


# --------------------------------------------------------------------------
# 1. CANONICAL MODEL REGISTRY & PLUG-AND-PLAY TESTS
# --------------------------------------------------------------------------
def test_canonical_registry_resolution_and_aliases():
    """Ensures all legacy, deprecated, and aliased model IDs resolve accurately."""
    # Direct canonical
    spec = CanonicalModelRegistry.resolve_spec("gemini-3.8-flash")
    assert spec.canonical_id == "gemini-3.8-flash"
    assert spec.provider_type == AiProviderType.ANTIGRAVITY
    assert AiCapability.MULTIMODAL_VISION in spec.capabilities

    # Deprecated 3.1 Pro -> Canonical 3.8 Flash
    alias_31 = CanonicalModelRegistry.resolve_spec("gemini-3.1-pro")
    assert alias_31.canonical_id == "gemini-3.8-flash"

    # Expired free OpenRouter -> OpenRouter 3.8 Flash
    exp_free = CanonicalModelRegistry.resolve_spec("google/gemini-2.0-flash-exp:free")
    assert exp_free.canonical_id == "openrouter/gemini-3.8-flash"
    assert exp_free.provider_type == AiProviderType.OPENROUTER

    # Local LM Studio alias
    lfm = CanonicalModelRegistry.resolve_spec("liquid/lfm-2.5-2.6b")
    assert lfm.canonical_id == "lfm-2.5-2.6b"
    assert lfm.provider_type == AiProviderType.LMSTUDIO

    # Dynamic plug-and-play OpenRouter vendor/model
    dynamic_spec = CanonicalModelRegistry.resolve_spec("qwen/qwen-2.5-vl-72b-instruct")
    assert dynamic_spec.provider_type == AiProviderType.OPENROUTER
    assert dynamic_spec.provider_model_id == "qwen/qwen-2.5-vl-72b-instruct"


# --------------------------------------------------------------------------
# 2. CIRCUIT BREAKER ISOLATION & FAILOVER TESTS
# --------------------------------------------------------------------------
def test_circuit_breaker_trips_on_503_and_recovers():
    """Verifies that 503 errors instantly trip the circuit to protect against latency spikes."""
    cb = ProviderCircuitBreaker(recovery_time_seconds=2.0)
    assert cb.can_attempt(AiProviderType.ANTIGRAVITY) is True

    # 503 error trips the circuit immediately
    cb.record_failure(
        AiProviderType.ANTIGRAVITY,
        Exception("503 Service Unavailable: spikes in demand on free tier"),
    )
    assert cb.can_attempt(AiProviderType.ANTIGRAVITY) is False

    # Other providers remain unaffected
    assert cb.can_attempt(AiProviderType.OPENROUTER) is True

    # After recovery, success closes circuit
    cb.record_success(AiProviderType.ANTIGRAVITY)
    assert cb.can_attempt(AiProviderType.ANTIGRAVITY) is True


# --------------------------------------------------------------------------
# 3. GATEWAY RESILIENT MULTI-TIER FAILOVER
# --------------------------------------------------------------------------
def test_gateway_failover_from_antigravity_to_openrouter():
    """Verifies that when Antigravity fails (e.g. 503), the gateway automatically routes to OpenRouter."""
    import asyncio

    async def _run_test():
        gateway = EnterpriseAiGateway()

        # Mock Antigravity adapter to simulate 503 spike
        mock_ag = AsyncMock()
        mock_ag.generate_text.side_effect = RuntimeError("503 Service Unavailable")

        # Mock OpenRouter adapter to simulate successful failover
        mock_or = AsyncMock()
        mock_or.generate_text.return_value = AiResponse(
            content="Nội dung đã được xử lý qua OpenRouter Fallback",
            model_used="openrouter/gemini-3.8-flash",
            provider_used=AiProviderType.OPENROUTER,
            latency_ms=120.0,
        )

        with patch.object(gateway, "_antigravity_adapter", mock_ag), patch.object(
            gateway, "_openrouter_adapter", mock_or
        ):
            req = AiTextRequest(
                prompt="Kiểm tra thẩm định hồ sơ công trình",
                canonical_model_id="gemini-3.8-flash",
            )
            response = await gateway.generate_text(req)

            assert response.content == "Nội dung đã được xử lý qua OpenRouter Fallback"
            assert response.provider_used == AiProviderType.OPENROUTER
            assert response.is_fallback is True
            mock_ag.generate_text.assert_awaited_once()
            mock_or.generate_text.assert_awaited_once()

    asyncio.run(_run_test())


# --------------------------------------------------------------------------
# 4. MULTIMODAL OCR DELEGATION TO AI GATEWAY
# --------------------------------------------------------------------------
def test_multimodal_ocr_delegates_to_ai_gateway():
    """Verifies that MultimodalOcrService calls gateway.analyze_vision_sync and eliminates raw urllib calls."""
    ocr_service = MultimodalOcrService()
    dummy_data_uri = "data:image/jpeg;base64,/9j/4AAQSkZJRg=="
    prompt = "Đọc và phiên âm toàn bộ chữ viết trong ảnh"

    mock_resp = AiResponse(
        content="CỘNG HÒA XÃ HỘI CHỦ NGHĨA VIỆT NAM\nQUYẾT ĐỊNH 1310/QĐ-UBND",
        model_used="gemini-3.8-flash",
        provider_used=AiProviderType.ANTIGRAVITY,
    )

    with patch(
        "app.core.ai.infrastructure.gateway.EnterpriseAiGateway.analyze_vision_sync",
        return_value=mock_resp,
    ) as mock_sync:
        result = ocr_service._call_vision_model(dummy_data_uri, prompt)
        assert "QUYẾT ĐỊNH 1310/QĐ-UBND" in result
        mock_sync.assert_called_once()
        call_req = mock_sync.call_args[0][0]
        assert isinstance(call_req, AiVisionRequest)
        assert call_req.data_uri == dummy_data_uri
        assert call_req.prompt == prompt


# --------------------------------------------------------------------------
# 5. STRICT AST ARCHITECTURE BOUNDARY ENFORCEMENT
# --------------------------------------------------------------------------
def test_ast_architecture_boundary_no_urllib_in_ocr_or_agents():
    """Architectural Gate: Ensures NO module in application OCR or LLM service uses raw urllib.request."""
    target_files = [
        "app/modules/core/application/ocr/multimodal_ocr_service.py",
        "app/modules/agents/application/llm/service.py",
    ]

    for rel_path in target_files:
        abs_path = os.path.join(os.getcwd(), rel_path.replace("/", os.sep))
        assert os.path.exists(abs_path), f"File not found: {abs_path}"

        with open(abs_path, "r", encoding="utf-8") as f:
            tree = ast.parse(f.read(), filename=abs_path)

        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                for alias in node.names:
                    assert (
                        "urllib" not in alias.name
                    ), f"Architecture violation: Forbidden '{alias.name}' imported in {rel_path}"
            elif isinstance(node, ast.ImportFrom):
                if node.module:
                    assert (
                        "urllib" not in node.module
                    ), f"Architecture violation: Forbidden from '{node.module}' imported in {rel_path}"
