"""
Routage multi-canaux des paliers IA (spec « Fournisseur par palier IA »).

- Chaque route d'un palier est un couple {provider, model} : une route peut
  partir par z.ai direct (zhipu), DeepSeek direct, etc. ;
- une route dont la clé API manque est sautée au profit de la suivante ;
- l'ancien réglage à chaînes reste routé via openrouter (compatibilité) ;
- la facturation suit le PALIER (Premium = 5, Essentielle = 1), pas le canal.
"""

import asyncio
import pytest
from pathlib import Path

import routes.translate_routes as tr


# ---------------------------------------------------------------------------
# Résolution unitaire : (provider, model)
# ---------------------------------------------------------------------------
def _settings(ai_tiers: dict | None = None, **providers):
    from routes.admin_routes import SettingsConfig

    payload: dict = {"ai_tiers": ai_tiers} if ai_tiers is not None else {}
    for provider, cfg in providers.items():
        payload[provider] = cfg
    return SettingsConfig.model_validate(payload)


def test_route_with_configured_key_is_used(monkeypatch):
    monkeypatch.delenv("ZHIPU_API_KEY", raising=False)
    cfg = _settings(
        {
            "essential": {
                "models": [],
                "default_model": "",
                "routes": [{"provider": "zhipu", "model": "glm-5.3-flash"}],
            },
            "premium": {"models": [], "default_model": ""},
        },
        zhipu={"api_key": "zk-test"},
    )
    assert tr._resolve_tier_model(cfg, "pro", premium=False) == (
        "zhipu",
        "glm-5.3-flash",
    )


def test_route_without_key_falls_back_to_next(monkeypatch):
    monkeypatch.delenv("ZHIPU_API_KEY", raising=False)
    monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
    cfg = _settings(
        {
            "essential": {
                "models": [],
                "default_model": "",
                "routes": [
                    {"provider": "zhipu", "model": "glm-5.3-flash"},
                    {"provider": "openrouter", "model": "deepseek/deepseek-v4-flash"},
                ],
            },
            "premium": {"models": [], "default_model": ""},
        },
        openrouter={"api_key": "sk-or-test"},
    )
    assert tr._resolve_tier_model(cfg, "pro", premium=False) == (
        "openrouter",
        "deepseek/deepseek-v4-flash",
    )


def test_legacy_string_models_still_route_via_openrouter(monkeypatch):
    monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
    cfg = _settings(
        {
            "essential": {"models": ["deepseek/deepseek-v4-flash"], "default_model": ""},
            "premium": {"models": [], "default_model": ""},
        },
        openrouter={"api_key": "sk-or-test"},
    )
    provider, model = tr._resolve_tier_model(cfg, "pro", premium=False)
    assert (provider, model) == ("openrouter", "deepseek/deepseek-v4-flash")


def test_premium_route_resolves_premium_tier(monkeypatch):
    monkeypatch.delenv("ZHIPU_API_KEY", raising=False)
    cfg = _settings(
        {
            "essential": {"models": [], "default_model": ""},
            "premium": {
                "models": [],
                "default_model": "",
                "routes": [{"provider": "deepseek", "model": "deepseek-reasoner"}],
            },
        },
        deepseek={"api_key": "dk-test"},
    )
    assert tr._resolve_tier_model(cfg, "business", premium=True) == (
        "deepseek",
        "deepseek-reasoner",
    )


# ---------------------------------------------------------------------------
# Facturation par palier (pas par canal)
# ---------------------------------------------------------------------------
def test_cost_factor_follows_tier_not_channel():
    assert tr._compute_cost_factor(None, "zhipu", premium=True) == 5
    assert tr._compute_cost_factor(None, "deepseek", premium=True) == 5
    assert tr._compute_cost_factor(None, "zhipu", premium=False) == 1
    assert tr._compute_cost_factor(None, "openrouter_premium", premium=False) == 1


def test_cost_factor_legacy_heuristic_kept_without_tier():
    class _P:
        _model = "anthropic/claude-sonnet-5"

    assert tr._compute_cost_factor(_P(), "zai") == 5
    assert tr._compute_cost_factor(None, "openrouter_premium") == 5
    assert tr._compute_cost_factor(None, "openrouter") == 1


# ---------------------------------------------------------------------------
# Validation admin des routes (normalize_ai_tiers)
# ---------------------------------------------------------------------------
def test_normalize_rejects_unknown_route_provider():
    from routes.admin_routes import normalize_ai_tiers

    cfg = _settings(
        {
            "essential": {
                "models": [],
                "default_model": "",
                "routes": [{"provider": "anthropic", "model": "claude"}],
            },
            "premium": {"models": [], "default_model": ""},
        }
    )
    with pytest.raises(ValueError, match="inconnu"):
        normalize_ai_tiers(cfg.ai_tiers)


def test_normalize_rejects_route_without_model():
    from routes.admin_routes import normalize_ai_tiers

    cfg = _settings(
        {
            "essential": {
                "models": [],
                "default_model": "",
                "routes": [{"provider": "zhipu", "model": " "}],
            },
            "premium": {"models": [], "default_model": ""},
        }
    )
    with pytest.raises(ValueError, match="modèle"):
        normalize_ai_tiers(cfg.ai_tiers)


def test_normalize_deduplicates_routes_and_keeps_order():
    from routes.admin_routes import normalize_ai_tiers

    cfg = _settings(
        {
            "essential": {
                "models": [],
                "default_model": "",
                "routes": [
                    {"provider": "zhipu", "model": "glm-5.3-flash"},
                    {"provider": "zhipu", "model": "glm-5.3-flash", "label": "doublon"},
                    {"provider": "openrouter", "model": "deepseek/deepseek-v4-flash"},
                ],
            },
            "premium": {"models": [], "default_model": ""},
        }
    )
    normalized = normalize_ai_tiers(cfg.ai_tiers)
    assert [(r.provider, r.model) for r in normalized.essential.routes] == [
        ("zhipu", "glm-5.3-flash"),
        ("openrouter", "deepseek/deepseek-v4-flash"),
    ]


def test_normalize_route_cross_tier_guard():
    """Une route openrouter pointant un modèle Premium dans le palier
    Essentielle est refusée (anti-croisement inchangé)."""
    from routes.admin_routes import normalize_ai_tiers

    cfg = _settings(
        {
            "essential": {
                "models": [],
                "default_model": "",
                "routes": [{"provider": "openrouter", "model": "anthropic/claude-sonnet-5"}],
            },
            "premium": {"models": [], "default_model": ""},
        }
    )
    with pytest.raises(ValueError, match="Premium"):
        normalize_ai_tiers(cfg.ai_tiers)


def test_normalize_empty_routes_and_models_fills_official_range():
    from routes.admin_routes import normalize_ai_tiers
    from models.subscription import DEFAULT_AI_MODELS_ESSENTIAL

    cfg = _settings(
        {
            "essential": {"models": [], "default_model": ""},
            "premium": {"models": [], "default_model": ""},
        }
    )
    normalized = normalize_ai_tiers(cfg.ai_tiers)
    assert normalized.essential.models == list(DEFAULT_AI_MODELS_ESSENTIAL)
    assert normalized.essential.routes == []


# ---------------------------------------------------------------------------
# Intégration worker : le canal choisi instancie le bon fournisseur
# ---------------------------------------------------------------------------
@pytest.fixture
def minimal_xlsx(tmp_path):
    try:
        import openpyxl

        wb = openpyxl.Workbook()
        wb.active["A1"] = "Hello"
        p = tmp_path / "minimal.xlsx"
        wb.save(p)
        return p
    except ImportError:
        pytest.skip("openpyxl required")


def _register_job(job_id: str) -> None:
    tr._translation_jobs[job_id] = {
        "id": job_id,
        "status": "processing",
        "progress_percent": 0,
        "current_step": "",
        "provider": "openrouter",
        "created_at": "2026-09-05T00:00:00+00:00",
    }


def _run_job(monkeypatch, tmp_path, minimal_xlsx, admin_settings, provider: str,
             user_plan: str, user_id=None, cost_capture: dict | None = None) -> list:
    """Lance _run_translation_job avec un faux fournisseur et renvoie les
    instanciations capturées (model, base_url, api_key).

    ``user_id`` + ``cost_capture`` : enregistre le facteur de facturation
    transmis à ``record_usage`` (substitué — aucun accès à la base)."""
    import routes.admin_routes as admin_routes_mod
    import services.providers.openai_provider as openai_provider_mod
    import services.providers.deepseek_provider as deepseek_provider_mod

    captured: list = []

    class FakeProvider:
        def __init__(self, api_key=None, model=None, base_url=None, timeout=60, **kwargs):
            self.model = model
            self._model = model
            self.base_url = base_url
            captured.append({"model": model, "base_url": base_url, "api_key": api_key})

    class FakeExcelTranslator:
        def __init__(self, provider=None):
            self.provider = provider

        def translate_file(self, input_path, output_path, target_lang,
                           source_lang="auto", progress_callback=None,
                           translate_images=False, **kwargs):
            out = Path(output_path)
            out.parent.mkdir(parents=True, exist_ok=True)
            out.write_bytes(minimal_xlsx.read_bytes())

        def get_translation_stats(self):
            return {"attempted": 1, "changed": 1}

    def _fake_record_usage(uid, pages, cost_factor, reserved_docs=1):
        if cost_capture is not None:
            cost_capture["cost_factor"] = cost_factor

    monkeypatch.setattr(openai_provider_mod, "OpenAITranslationProvider", FakeProvider)
    monkeypatch.setattr(deepseek_provider_mod, "DeepSeekTranslationProvider", FakeProvider)
    monkeypatch.setattr(tr, "ExcelTranslator", FakeExcelTranslator)
    monkeypatch.setattr(tr, "record_usage", _fake_record_usage)
    monkeypatch.setattr(
        admin_routes_mod, "load_settings", lambda: admin_settings, raising=True
    )

    _register_job("job-routes")
    try:
        asyncio.run(
            tr._run_translation_job(
                job_id="job-routes",
                input_path=Path(minimal_xlsx),
                file_extension=".xlsx",
                target_lang="fr",
                source_lang="en",
                provider=provider,
                user_id=user_id,
                custom_prompt=None,
                glossary_id=None,
                prompt_id=None,
                webhook_url=None,
                user_plan=user_plan,
            )
        )
    finally:
        tr._translation_jobs.pop("job-routes", None)
    return captured


def test_worker_routes_zhipu_channel(monkeypatch, tmp_path, minimal_xlsx):
    monkeypatch.delenv("ZHIPU_API_KEY", raising=False)
    settings = _settings(
        {
            "essential": {
                "models": [],
                "default_model": "",
                "routes": [{"provider": "zhipu", "model": "glm-5.3-flash"}],
            },
            "premium": {"models": [], "default_model": ""},
        },
        zhipu={"api_key": "zk-test"},
    )
    captured = _run_job(monkeypatch, tmp_path, minimal_xlsx, settings,
                        provider="openrouter", user_plan="PlanType.PRO")
    assert captured, "le fournisseur n'a pas été instancié"
    assert captured[0]["model"] == "glm-5.3-flash"
    assert captured[0]["base_url"] == "https://api.z.ai/api/paas/v4"


def test_worker_skips_route_without_key(monkeypatch, tmp_path, minimal_xlsx):
    """Clé zhipu absente → route suivante (openrouter) comme le veut la spec."""
    monkeypatch.delenv("ZHIPU_API_KEY", raising=False)
    settings = _settings(
        {
            "essential": {
                "models": [],
                "default_model": "",
                "routes": [
                    {"provider": "zhipu", "model": "glm-5.3-flash"},
                    {"provider": "openrouter", "model": "deepseek/deepseek-v4-flash"},
                ],
            },
            "premium": {"models": [], "default_model": ""},
        },
        openrouter={"api_key": "sk-or-test"},
    )
    captured = _run_job(monkeypatch, tmp_path, minimal_xlsx, settings,
                        provider="openrouter", user_plan="PlanType.PRO")
    assert captured[0]["base_url"] == "https://openrouter.ai/api/v1"
    assert captured[0]["model"] == "deepseek/deepseek-v4-flash"


def test_worker_legacy_settings_keep_openrouter(monkeypatch, tmp_path, minimal_xlsx):
    """Ancien réglage (chaînes) : routé via openrouter comme aujourd'hui."""
    monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
    settings = _settings(
        {
            "essential": {"models": ["deepseek/deepseek-v4-flash"], "default_model": ""},
            "premium": {"models": [], "default_model": ""},
        },
        openrouter={"api_key": "sk-or-test"},
    )
    captured = _run_job(monkeypatch, tmp_path, minimal_xlsx, settings,
                        provider="openrouter", user_plan="PlanType.PRO")
    assert captured[0]["base_url"] == "https://openrouter.ai/api/v1"
    assert captured[0]["model"] == "deepseek/deepseek-v4-flash"


def test_worker_deepseek_route_uses_deepseek_provider(monkeypatch, tmp_path, minimal_xlsx):
    monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
    settings = _settings(
        {
            "essential": {
                "models": [],
                "default_model": "",
                "routes": [{"provider": "deepseek", "model": "deepseek-chat"}],
            },
            "premium": {"models": [], "default_model": ""},
        },
        deepseek={"api_key": "dk-test"},
    )
    captured = _run_job(monkeypatch, tmp_path, minimal_xlsx, settings,
                        provider="openrouter", user_plan="PlanType.PRO")
    assert captured[0]["base_url"] == "https://api.deepseek.com/v1"
    assert captured[0]["api_key"] == "dk-test"


# ---------------------------------------------------------------------------
# Facturation exécutée dans le worker : palier Essentielle = 1, Premium = 5
# ---------------------------------------------------------------------------
def test_worker_bills_essential_tier_at_cost_1(monkeypatch, tmp_path, minimal_xlsx):
    monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
    settings = _settings(
        {
            "essential": {"models": ["deepseek/deepseek-v4-flash"], "default_model": ""},
            "premium": {"models": [], "default_model": ""},
        },
        openrouter={"api_key": "sk-or-test"},
    )
    cost: dict = {}
    _run_job(monkeypatch, tmp_path, minimal_xlsx, settings,
             provider="openrouter", user_plan="PlanType.PRO",
             user_id="u1", cost_capture=cost)
    assert cost["cost_factor"] == 1


def test_worker_bills_premium_tier_at_cost_5(monkeypatch, tmp_path, minimal_xlsx):
    monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
    settings = _settings(
        {
            "essential": {"models": [], "default_model": ""},
            "premium": {"models": ["anthropic/claude-sonnet-5"], "default_model": ""},
        },
        openrouter={"api_key": "sk-or-test"},
    )
    cost: dict = {}
    _run_job(monkeypatch, tmp_path, minimal_xlsx, settings,
             provider="openrouter_premium", user_plan="PlanType.BUSINESS",
             user_id="u1", cost_capture=cost)
    assert cost["cost_factor"] == 5


# ---------------------------------------------------------------------------
# Première sauvegarde depuis l'éditeur : migration fidèle hérité → routes
# ---------------------------------------------------------------------------
def test_first_editor_save_migrates_legacy_settings_without_loss(monkeypatch):
    """Des réglages à chaînes (models/default_model) convertis en routes par
    l'éditeur, puis normalisés, doivent résoudre EXACTEMENT les mêmes
    canaux/modèles qu'avant la migration."""
    from routes.admin_routes import SettingsConfig, normalize_ai_tiers

    legacy = _settings(
        {
            "essential": {
                "models": ["z-ai/glm-5.3-flash", "deepseek/deepseek-v4-flash"],
                "default_model": "z-ai/glm-5.3-flash",
            },
            "premium": {
                "models": ["anthropic/claude-sonnet-5", "deepseek/deepseek-v4-pro"],
                "default_model": "anthropic/claude-sonnet-5",
            },
        },
        openrouter={"api_key": "sk-or-test"},
    )
    before_essential = tr._resolve_tier_model(legacy, "pro", premium=False)
    before_premium = tr._resolve_tier_model(legacy, "business", premium=True)

    def _editor_payload(tier):
        """Miroir de la conversion faite par la page Modèles : le défaut en
        tête, puis le reste de la liste — le tout en routes openrouter, avec
        le miroir hérité conservé (models/default_model)."""
        ordered = [tier.default_model] + [
            m for m in tier.models if m != tier.default_model
        ]
        routes = [{"provider": "openrouter", "model": m} for m in ordered]
        return {"models": ordered, "default_model": tier.default_model, "routes": routes}

    payload = SettingsConfig.model_validate(
        {
            "openrouter": {"api_key": "sk-or-test"},
            "ai_tiers": {
                "essential": _editor_payload(legacy.ai_tiers.essential),
                "premium": _editor_payload(legacy.ai_tiers.premium),
            },
        }
    )
    normalized = normalize_ai_tiers(payload.ai_tiers)
    migrated = SettingsConfig.model_validate(
        {"openrouter": {"api_key": "sk-or-test"}, "ai_tiers": normalized.model_dump()}
    )

    after_essential = tr._resolve_tier_model(migrated, "pro", premium=False)
    after_premium = tr._resolve_tier_model(migrated, "business", premium=True)

    assert after_essential == before_essential == ("openrouter", "z-ai/glm-5.3-flash")
    assert after_premium == before_premium == ("openrouter", "anthropic/claude-sonnet-5")
    # Aucune perte : tous les modèles d'origine restent résolubles dans l'ordre.
    assert [r.model for r in normalized.essential.routes] == [
        "z-ai/glm-5.3-flash",
        "deepseek/deepseek-v4-flash",
    ]
    assert [r.model for r in normalized.premium.routes] == [
        "anthropic/claude-sonnet-5",
        "deepseek/deepseek-v4-pro",
    ]
