"""AI provider registry tests with mocked network calls."""

from __future__ import annotations

from dataclasses import replace
from pathlib import Path
from typing import Any

import pytest
from requests.exceptions import ConnectionError

from app.services import runtime_config
from app.services.ai import provider_registry as ai_provider_registry


class MockResponse:
    """Minimal response mock with status code and JSON payload."""

    def __init__(self, status_code: int, payload: Any):
        self.status_code = status_code
        self._payload = payload
        self.text = str(payload)

    def json(self) -> Any:
        return self._payload


@pytest.fixture()
def isolated_runtime(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    """Isolate runtime config and AI settings for deterministic tests."""
    config_path = tmp_path / ".sentineledge" / "config.json"
    monkeypatch.setenv("SENTINELEDGE_CONFIG_PATH", str(config_path))
    runtime_config.reset_runtime_state()
    patched_settings = replace(
        runtime_config.settings,
        ai_provider="gemini",
        gemini_api_key=None,
        ai_allow_fallback=False,
        ollama_model="llama3.2:3b",
        ollama_base_url="http://localhost:11434",
    )
    monkeypatch.setattr(runtime_config, "settings", patched_settings)
    monkeypatch.setattr(ai_provider_registry, "settings", replace(patched_settings))
    yield
    runtime_config.reset_runtime_state()


def _save_ai_config(
    *,
    ai_provider: str,
    gemini_api_key: str | None = None,
    gemini_model: str | None = None,
    ollama_model: str | None = None,
    allow_fallback: bool = False,
) -> None:
    runtime_config.save_config(
        todoist_api_token=None,
        vault_paths=[],
        assistant_default_language="es",
        language_configured=False,
        ai_provider=ai_provider,
        gemini_api_key=gemini_api_key,
        gemini_model=gemini_model,
        ollama_model=ollama_model,
        allow_fallback=allow_fallback,
        providers=runtime_config.get_providers_snapshot(),
        default_task_provider="todoist",
        default_notes_provider="vault_local",
        db=None,
    )


def test_ollama_unavailable_raises_controlled_error(
    isolated_runtime: None,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Local provider selection should fail gracefully when Ollama is down."""
    from app.services.ai.providers import ollama_provider

    _save_ai_config(ai_provider="ollama", allow_fallback=False)

    def fail_post(*args: Any, **kwargs: Any) -> MockResponse:
        raise ConnectionError("refused")

    monkeypatch.setattr(ollama_provider.requests, "post", fail_post)

    with pytest.raises(ai_provider_registry.AIProviderUnavailableError):
        ai_provider_registry.resolve_generation_provider()

    health = ai_provider_registry.selected_provider_health()
    assert health["selected"] == "ollama"
    assert health["available"] is False


def test_ollama_available_generates_text(
    isolated_runtime: None,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Ollama should be selected and used when reachable."""
    from app.services.ai.providers import ollama_provider

    _save_ai_config(ai_provider="ollama", ollama_model="llama3.2:3b")

    def fake_post(*args: Any, **kwargs: Any) -> MockResponse:
        return MockResponse(200, {"response": "local-response"})

    monkeypatch.setattr(ollama_provider.requests, "post", fake_post)

    selection = ai_provider_registry.resolve_generation_provider()
    assert selection.selected_provider == "ollama"
    assert selection.effective_provider == "ollama"
    assert selection.fallback_used is False
    assert ai_provider_registry.generate_text("hello") == "local-response"


def test_gemini_selected_generates_text(
    isolated_runtime: None,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Gemini provider should be used when selected and configured."""
    from app.services.ai.providers import gemini_provider

    _save_ai_config(ai_provider="gemini", gemini_api_key="test-key")

    def fake_post(*args: Any, **kwargs: Any) -> MockResponse:
        return MockResponse(
            200,
            {
                "candidates": [
                    {"content": {"parts": [{"text": "remote-response"}]}},
                ],
            },
        )

    monkeypatch.setattr(gemini_provider.requests, "post", fake_post)

    selection = ai_provider_registry.resolve_generation_provider()
    assert selection.selected_provider == "gemini"
    assert selection.effective_provider == "gemini"
    assert selection.fallback_used is False
    assert ai_provider_registry.generate_text("ping") == "remote-response"


def test_config_switching_and_fallback_to_gemini(
    isolated_runtime: None,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """When local fails and fallback is enabled, registry should use Gemini."""
    from app.services.ai.providers import gemini_provider, ollama_provider

    _save_ai_config(
        ai_provider="ollama",
        gemini_api_key="fallback-key",
        allow_fallback=True,
        ollama_model="llama3.2:3b",
    )

    def gemini_success(*args: Any, **kwargs: Any) -> MockResponse:
        return MockResponse(
            200,
            {
                "candidates": [
                    {"content": {"parts": [{"text": "fallback-response"}]}},
                ],
            },
        )

    monkeypatch.setattr(ollama_provider.OllamaProvider, "health_check", lambda self: False)
    monkeypatch.setattr(gemini_provider.requests, "post", gemini_success)

    selection = ai_provider_registry.resolve_generation_provider()
    assert selection.selected_provider == "ollama"
    assert selection.effective_provider == "gemini"
    assert selection.fallback_used is True
    assert ai_provider_registry.generate_text("hello") == "fallback-response"


def test_config_overrides_env_gemini_key(
    isolated_runtime: None,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Config Gemini key must take precedence over environment fallback."""
    from app.services.ai.providers import gemini_provider

    patched_settings = replace(
        runtime_config.settings,
        ai_provider="gemini",
        gemini_api_key="env-gemini-key",
    )
    monkeypatch.setattr(runtime_config, "settings", patched_settings)
    monkeypatch.setattr(ai_provider_registry, "settings", replace(patched_settings))

    _save_ai_config(ai_provider="gemini", gemini_api_key="config-gemini-key")
    seen_keys: list[str] = []

    def fake_post(*args: Any, **kwargs: Any) -> MockResponse:
        seen_keys.append(str((kwargs.get("params") or {}).get("key") or ""))
        return MockResponse(
            200,
            {
                "candidates": [
                    {"content": {"parts": [{"text": "ok"}]}},
                ],
            },
        )

    monkeypatch.setattr(gemini_provider.requests, "post", fake_post)
    assert ai_provider_registry.generate_text("ping") == "ok"
    assert seen_keys[-1] == "config-gemini-key"


def test_save_has_immediate_effect_without_restart(
    isolated_runtime: None,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """A second save should immediately update effective Gemini credentials."""
    from app.services.ai.providers import gemini_provider

    seen_keys: list[str] = []
    seen_urls: list[str] = []

    def fake_post(*args: Any, **kwargs: Any) -> MockResponse:
        seen_keys.append(str((kwargs.get("params") or {}).get("key") or ""))
        seen_urls.append(str(kwargs.get("url") or ""))
        return MockResponse(
            200,
            {
                "candidates": [
                    {"content": {"parts": [{"text": "ok"}]}},
                ],
            },
        )

    monkeypatch.setattr(gemini_provider.requests, "post", fake_post)

    _save_ai_config(ai_provider="gemini", gemini_api_key="first-key", gemini_model="gemini-2.5-flash")
    assert ai_provider_registry.generate_text("hello") == "ok"
    _save_ai_config(ai_provider="gemini", gemini_api_key="second-key", gemini_model="gemini-2.5-pro")
    assert ai_provider_registry.generate_text("hello again") == "ok"

    assert seen_keys[-2:] == ["first-key", "second-key"]
    assert "gemini-2.5-flash" in seen_urls[-2]
    assert "gemini-2.5-pro" in seen_urls[-1]


def test_config_overrides_env_gemini_model(
    isolated_runtime: None,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Gemini model from config must override environment fallback."""
    from app.services.ai.providers import gemini_provider

    patched_settings = replace(
        runtime_config.settings,
        ai_provider="gemini",
        gemini_api_key="env-key",
        gemini_model="gemini-env-model",
    )
    monkeypatch.setattr(runtime_config, "settings", patched_settings)
    monkeypatch.setattr(ai_provider_registry, "settings", replace(patched_settings))

    _save_ai_config(ai_provider="gemini", gemini_api_key="config-key", gemini_model="gemini-config-model")
    seen_urls: list[str] = []

    def fake_post(*args: Any, **kwargs: Any) -> MockResponse:
        seen_urls.append(str(kwargs.get("url") or ""))
        return MockResponse(
            200,
            {
                "candidates": [
                    {"content": {"parts": [{"text": "ok"}]}},
                ],
            },
        )

    monkeypatch.setattr(gemini_provider.requests, "post", fake_post)
    assert ai_provider_registry.generate_text("ping") == "ok"
    assert "gemini-config-model" in seen_urls[-1]


def test_restart_keeps_saved_gemini_config(
    isolated_runtime: None,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Runtime reset should still load persisted Gemini key from config file."""
    from app.services.ai.providers import gemini_provider

    _save_ai_config(ai_provider="gemini", gemini_api_key="persisted-key")
    runtime_config.reset_runtime_state()
    ai_provider_registry.invalidate_runtime_cache(reason="test-restart")

    seen_keys: list[str] = []

    def fake_post(*args: Any, **kwargs: Any) -> MockResponse:
        seen_keys.append(str((kwargs.get("params") or {}).get("key") or ""))
        return MockResponse(
            200,
            {
                "candidates": [
                    {"content": {"parts": [{"text": "ok"}]}},
                ],
            },
        )

    monkeypatch.setattr(gemini_provider.requests, "post", fake_post)
    assert ai_provider_registry.generate_text("after restart") == "ok"
    assert seen_keys[-1] == "persisted-key"


def test_env_only_resolution_without_ui_config_fallback(
    isolated_runtime: None,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """When no UI config file exists, resolver should use explicit env values."""
    monkeypatch.setenv("AI_PROVIDER", "gemini")
    monkeypatch.setenv("GEMINI_API_KEY", "env-key")
    monkeypatch.setenv("GEMINI_MODEL", "env-model")
    runtime_config.reset_runtime_state()
    values = ai_provider_registry.resolve_runtime_ai_values()
    assert values.selected_provider == "gemini"
    assert values.selected_provider_source == "env"
    assert values.gemini_model == "env-model"
    assert values.gemini_model_source == "env"
    assert values.gemini_api_key_source == "env"


def test_invalid_gemini_model_returns_actionable_suggestions(
    isolated_runtime: None,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Gemini 404 should include actionable model suggestions from ListModels."""
    from app.services.ai.providers import gemini_provider

    _save_ai_config(ai_provider="gemini", gemini_api_key="test-key", gemini_model="gemini-2.5-pro-flash")

    def fake_post(*args: Any, **kwargs: Any) -> MockResponse:
        return MockResponse(
            404,
            {
                "error": {
                    "message": "models/gemini-2.5-pro-flash is not found for API version v1beta",
                },
            },
        )

    def fake_get(*args: Any, **kwargs: Any) -> MockResponse:
        return MockResponse(
            200,
            {
                "models": [
                    {
                        "name": "models/gemini-2.5-flash",
                        "supportedGenerationMethods": ["generateContent"],
                    },
                    {
                        "name": "models/gemini-2.5-pro",
                        "supportedGenerationMethods": ["generateContent"],
                    },
                ],
            },
        )

    monkeypatch.setattr(gemini_provider.requests, "post", fake_post)
    monkeypatch.setattr(gemini_provider.requests, "get", fake_get)

    with pytest.raises(ai_provider_registry.AIProviderResponseError) as exc_info:
        ai_provider_registry.generate_text("ping")

    error_text = str(exc_info.value)
    assert "gemini-2.5-pro-flash" in error_text
    assert "gemini-2.5-flash" in error_text
    assert "Change model in Settings" in error_text


def test_gemini_test_provider_uses_same_invalid_model_path(
    isolated_runtime: None,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Config Test Gemini should surface invalid-model guidance from runtime path."""
    from app.services.ai.providers import gemini_provider

    def fake_post(*args: Any, **kwargs: Any) -> MockResponse:
        return MockResponse(
            404,
            {
                "error": {
                    "message": "models/gemini-2.5-pro-flash is not found for API version v1beta",
                },
            },
        )

    def fake_get(*args: Any, **kwargs: Any) -> MockResponse:
        return MockResponse(
            200,
            {
                "models": [
                    {
                        "name": "models/gemini-2.5-flash",
                        "supportedGenerationMethods": ["generateContent"],
                    },
                ],
            },
        )

    monkeypatch.setattr(gemini_provider.requests, "post", fake_post)
    monkeypatch.setattr(gemini_provider.requests, "get", fake_get)

    result = ai_provider_registry.test_provider(
        provider_id="gemini",
        gemini_api_key="test-key",
        gemini_model="gemini-2.5-pro-flash",
    )

    assert result["provider"] == "gemini"
    assert result["ok"] is False
    detail = str(result["detail"])
    assert "gemini-2.5-pro-flash" in detail
    assert "gemini-2.5-flash" in detail
