"""
Génération d'email par IA dans la page Marketing (spec « Fournisseur par
palier IA ») : prompt construit depuis MARKETING_PLAN.md, analyse robuste de
la sortie, nettoyage basique du HTML, aucun envoi automatique.
"""

import json
import pytest
from pathlib import Path

import routes.admin_routes as admin_routes_mod

ADMIN_LOGIN_URL = "/api/v1/admin/login"
GENERATE_URL = "/api/v1/admin/marketing/email/generate"

FAKE_PLAN = (
    "# Plan Marketing — Office Translator (Wordly.art)\n\n"
    "## 1. Positionnement & Proposition de Valeur\n"
    "Office Translator (marque Wordly.art) traduit Word, Excel, PowerPoint et "
    "PDF en préservant la mise en page. USP : « Traduit en place. Zéro perte "
    "de mise en page. »\n\n"
    "## 2. Tarification\n"
    "| Plan | Mensuel |\n| Free | 0 € |\n| Starter | 9 € |\n| Pro | 19 € |\n\n"
    "## 3. Actifs Marketing en Place\n"
    "(cette section ne doit PAS figurer dans le prompt)\n"
)


@pytest.fixture
def promo_file(tmp_path: Path, monkeypatch) -> Path:
    import services.payment_service as payment_svc

    path = tmp_path / "promo_codes.json"
    monkeypatch.setattr(payment_svc, "PROMO_CODES_FILE", path)
    return path


@pytest.fixture
def gen_env(tmp_path: Path, monkeypatch, promo_file):
    """Plan marketing isolé + aucune clé par défaut (chaque test choisit)."""
    plan_file = tmp_path / "MARKETING_PLAN.md"
    plan_file.write_text(FAKE_PLAN, encoding="utf-8")
    monkeypatch.setattr(admin_routes_mod, "MARKETING_PLAN_FILE", plan_file)
    monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
    monkeypatch.delenv("ZHIPU_API_KEY", raising=False)
    monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
    return {"plan_file": plan_file}


@pytest.fixture
def client(gen_env, monkeypatch, tmp_path: Path):
    import services.auth_service as auth_svc
    from middleware.rate_limiting import RateLimitManager

    monkeypatch.setattr(auth_svc, "USERS_FILE", tmp_path / "users.json")
    monkeypatch.setattr(auth_svc, "USE_DATABASE", False)
    monkeypatch.setattr(auth_svc, "DATABASE_AVAILABLE", False)

    async def _check_request_allow(self, request):
        return True, "ok", "test"

    async def _check_translation_allow(self, request, file_size_mb=0):
        return True, "ok"

    monkeypatch.setattr(RateLimitManager, "check_request", _check_request_allow)
    monkeypatch.setattr(RateLimitManager, "check_translation", _check_translation_allow)

    from fastapi.testclient import TestClient
    from main import app

    return TestClient(app, raise_server_exceptions=True)


@pytest.fixture
def admin_headers(client, monkeypatch):
    monkeypatch.setattr(admin_routes_mod, "ADMIN_USERNAME", "admin")
    monkeypatch.setattr(admin_routes_mod, "ADMIN_PASSWORD", "admin-secret")
    monkeypatch.setattr(admin_routes_mod, "ADMIN_PASSWORD_HASH", None)
    r = client.post(ADMIN_LOGIN_URL, json={"password": "admin-secret"})
    assert r.status_code == 200, r.text
    return {"Authorization": f"Bearer {r.json()['access_token']}"}


def _with_openrouter_key(monkeypatch):
    from routes.admin_routes import SettingsConfig

    monkeypatch.setattr(
        admin_routes_mod, "load_settings",
        lambda: SettingsConfig.model_validate({"openrouter": {"api_key": "sk-or-test"}}),
        raising=True,
    )


# ---------------------------------------------------------------------------
def test_generate_requires_known_channel(client, admin_headers, gen_env):
    r = client.post(
        GENERATE_URL,
        json={"provider": "mystery", "brief": "Relance d'activation."},
        headers=admin_headers,
    )
    assert r.status_code == 400
    assert r.json()["error"] == "UNKNOWN_CHANNEL"


def test_generate_requires_configured_key(client, admin_headers, gen_env, monkeypatch):
    monkeypatch.setattr(
        admin_routes_mod, "load_settings",
        lambda: admin_routes_mod.SettingsConfig(), raising=True,
    )
    r = client.post(
        GENERATE_URL,
        json={"provider": "openrouter", "brief": "Relance d'activation."},
        headers=admin_headers,
    )
    assert r.status_code == 400
    assert r.json()["error"] == "PROVIDER_NOT_CONFIGURED"


def test_generate_builds_prompt_from_marketing_plan(client, admin_headers, gen_env, monkeypatch):
    """Le prompt système embarque le plan marketing (positionnement + tarifs)
    et le brief ; les sections suivantes du plan n'y figurent pas."""
    _with_openrouter_key(monkeypatch)
    captured: dict = {}

    async def _fake_llm(admin_cfg, provider, model, system_prompt, user_prompt):
        captured["system"] = system_prompt
        captured["user"] = user_prompt
        captured["provider"], captured["model"] = provider, model
        return json.dumps({
            "subject": "Votre document vous attend",
            "html": "<html><body><p>Terminez votre traduction.</p></body></html>",
        })

    monkeypatch.setattr(admin_routes_mod, "_call_llm_chat", _fake_llm)
    r = client.post(
        GENERATE_URL,
        json={"provider": "openrouter", "brief": "Relance des inscrits de la liste d'attente."},
        headers=admin_headers,
    )
    assert r.status_code == 200, r.text
    assert "Wordly.art" in captured["system"]
    assert "Traduit en place" in captured["system"]
    assert "19 €" in captured["system"]
    assert "Actifs Marketing" not in captured["system"]
    assert "liste d'attente" in captured["user"]
    assert captured["model"]  # modèle par défaut du canal


def test_generate_parses_fenced_json(client, admin_headers, gen_env, monkeypatch):
    _with_openrouter_key(monkeypatch)

    async def _fake_llm(admin_cfg, provider, model, system_prompt, user_prompt):
        return (
            "```json\n"
            '{"subject": "Offre de lancement", "html": "<p>Bonjour !</p>"}\n'
            "```"
        )

    monkeypatch.setattr(admin_routes_mod, "_call_llm_chat", _fake_llm)
    r = client.post(
        GENERATE_URL,
        json={"provider": "openrouter", "brief": "b"},
        headers=admin_headers,
    )
    assert r.status_code == 200
    data = r.json()["data"]
    assert data["subject"] == "Offre de lancement"
    assert data["html"] == "<p>Bonjour !</p>"


def test_generate_falls_back_to_title(client, admin_headers, gen_env, monkeypatch):
    """Sortie non JSON : repli <title> = sujet, corps = HTML."""
    _with_openrouter_key(monkeypatch)

    async def _fake_llm(admin_cfg, provider, model, system_prompt, user_prompt):
        return (
            "<html><head><title>Relance</title></head>"
            "<body><p>Reprenez votre traduction.</p></body></html>"
        )

    monkeypatch.setattr(admin_routes_mod, "_call_llm_chat", _fake_llm)
    r = client.post(
        GENERATE_URL,
        json={"provider": "openrouter", "brief": "b"},
        headers=admin_headers,
    )
    assert r.status_code == 200
    data = r.json()["data"]
    assert data["subject"] == "Relance"
    assert "Reprenez votre traduction." in data["html"]


def test_generate_unparseable_output_returns_502(client, admin_headers, gen_env, monkeypatch):
    _with_openrouter_key(monkeypatch)

    async def _fake_llm(admin_cfg, provider, model, system_prompt, user_prompt):
        return "Je ne peux pas répondre à cela."

    monkeypatch.setattr(admin_routes_mod, "_call_llm_chat", _fake_llm)
    r = client.post(
        GENERATE_URL,
        json={"provider": "openrouter", "brief": "b"},
        headers=admin_headers,
    )
    assert r.status_code == 502
    assert r.json()["error"] == "AI_GENERATION_FAILED"


def test_generate_llm_error_returns_502(client, admin_headers, gen_env, monkeypatch):
    _with_openrouter_key(monkeypatch)

    async def _fake_llm(admin_cfg, provider, model, system_prompt, user_prompt):
        raise RuntimeError("upstream timeout")

    monkeypatch.setattr(admin_routes_mod, "_call_llm_chat", _fake_llm)
    r = client.post(
        GENERATE_URL,
        json={"provider": "openrouter", "brief": "b"},
        headers=admin_headers,
    )
    assert r.status_code == 502
    assert r.json()["error"] == "AI_GENERATION_FAILED"


def test_generate_sanitizes_html(client, admin_headers, gen_env, monkeypatch):
    """Nettoyage basique : pas de <script>, pas de gestionnaires d'événements."""
    _with_openrouter_key(monkeypatch)

    async def _fake_llm(admin_cfg, provider, model, system_prompt, user_prompt):
        return json.dumps({
            "subject": "s",
            "html": "<p onclick=\"evil()\">ok</p><script>alert(1)</script>"
                    "<a href=\"javascript:alert(2)\">x</a>",
        })

    monkeypatch.setattr(admin_routes_mod, "_call_llm_chat", _fake_llm)
    r = client.post(
        GENERATE_URL,
        json={"provider": "openrouter", "brief": "b"},
        headers=admin_headers,
    )
    assert r.status_code == 200
    html = r.json()["data"]["html"]
    assert "<script" not in html
    assert "onclick" not in html
    assert "javascript:" not in html


def test_generate_mentions_active_promo_in_prompt(client, admin_headers, gen_env, monkeypatch):
    """Le sélecteur de promo alimente le prompt (code + valeur exacts)."""
    _with_openrouter_key(monkeypatch)
    import services.payment_service as payment_svc

    payment_svc.save_promo_codes([{
        "code": "RENTREE10", "active": True, "type": "percent", "value": 10,
        "plans": ["pro"], "max_redemptions": 50, "times_used": 3,
        "expires_at": None, "stripe_coupon_id": None,
        "stripe_promotion_code_id": None, "stripe_mode": "local_only",
        "created_at": "t", "deactivated_at": None,
    }])

    captured: dict = {}

    async def _fake_llm(admin_cfg, provider, model, system_prompt, user_prompt):
        captured["system"] = system_prompt
        return json.dumps({"subject": "s", "html": "<p>x</p>"})

    monkeypatch.setattr(admin_routes_mod, "_call_llm_chat", _fake_llm)
    r = client.post(
        GENERATE_URL,
        json={"provider": "openrouter", "brief": "b", "promo_code": "rentree10"},
        headers=admin_headers,
    )
    assert r.status_code == 200
    assert "RENTREE10" in captured["system"]
    assert "-10 %" in captured["system"]


def test_generate_uses_requested_channel_and_model(client, admin_headers, gen_env, monkeypatch):
    from routes.admin_routes import SettingsConfig

    monkeypatch.setattr(
        admin_routes_mod, "load_settings",
        lambda: SettingsConfig.model_validate({"zhipu": {"api_key": "zk"}}),
        raising=True,
    )
    captured: dict = {}

    async def _fake_llm(admin_cfg, provider, model, system_prompt, user_prompt):
        captured["provider"], captured["model"] = provider, model
        return json.dumps({"subject": "s", "html": "<p>x</p>"})

    monkeypatch.setattr(admin_routes_mod, "_call_llm_chat", _fake_llm)
    r = client.post(
        GENERATE_URL,
        json={"provider": "zhipu", "model": "glm-5.3-flash", "brief": "b"},
        headers=admin_headers,
    )
    assert r.status_code == 200
    assert captured["provider"] == "zhipu"
    assert captured["model"] == "glm-5.3-flash"


def test_generate_requires_admin(client, gen_env):
    r = client.post(GENERATE_URL, json={"provider": "openrouter", "brief": "b"})
    assert r.status_code == 401


# ---------------------------------------------------------------------------
# Helpers d'analyse (tests unitaires directs)
# ---------------------------------------------------------------------------
def test_parse_generated_email_variants():
    parse = admin_routes_mod._parse_generated_email
    assert parse('{"subject":"a","html":"<p>b</p>"}') == ("a", "<p>b</p>")
    assert parse('Blabla\n{"subject":"a","html":"<p>b</p>"}\nFin') == ("a", "<p>b</p>")
    assert parse("pas de contenu utile") is None
    assert parse("") is None
    # JSON sans html → None (sujet ET html requis)
    assert parse('{"subject":"a"}') is None


# ---------------------------------------------------------------------------
# Correctifs de relecture : plan manquant, promo inconnue, CTA, RTL, bornes
# ---------------------------------------------------------------------------
def test_generate_without_marketing_plan_is_refused(client, admin_headers, gen_env, monkeypatch):
    """Plan marketing absent : erreur claire, jamais de chiffres inventés."""
    _with_openrouter_key(monkeypatch)
    monkeypatch.setattr(
        admin_routes_mod, "MARKETING_PLAN_FILE", gen_env["plan_file"].with_suffix(".absent")
    )
    r = client.post(
        GENERATE_URL,
        json={"provider": "openrouter", "brief": "b"},
        headers=admin_headers,
    )
    assert r.status_code == 503
    assert r.json()["error"] == "MARKETING_PLAN_MISSING"


def test_generate_unknown_promo_returns_promo_not_found(
    client, admin_headers, gen_env, monkeypatch
):
    """Code promo inexistant ou inactif : 400 explicite, pas de génération
    silencieuse sans offre."""
    _with_openrouter_key(monkeypatch)

    async def _fake_llm(*a, **kw):  # ne doit PAS être appelé
        raise AssertionError("l'appel LLM ne doit pas avoir lieu")

    monkeypatch.setattr(admin_routes_mod, "_call_llm_chat", _fake_llm)
    r = client.post(
        GENERATE_URL,
        json={"provider": "openrouter", "brief": "b", "promo_code": "NEXISTEPAS"},
        headers=admin_headers,
    )
    assert r.status_code == 400
    assert r.json()["error"] == "PROMO_NOT_FOUND"


def test_generate_prompt_contains_pricing_cta_link(
    client, admin_headers, gen_env, monkeypatch
):
    """Avec une réduction : le prompt impose un bouton vers /pricing?promo=CODE."""
    _with_openrouter_key(monkeypatch)
    import services.payment_service as payment_svc

    payment_svc.save_promo_codes([{
        "code": "RENTREE10", "active": True, "type": "percent", "value": 10,
        "plans": ["all"], "max_redemptions": None, "times_used": 0,
        "expires_at": None, "stripe_coupon_id": None,
        "stripe_promotion_code_id": None, "stripe_mode": "local_only",
        "created_at": "t", "deactivated_at": None,
    }])
    monkeypatch.setenv("FRONTEND_URL", "https://wordly.art")

    captured: dict = {}

    async def _fake_llm(admin_cfg, provider, model, system_prompt, user_prompt):
        captured["system"] = system_prompt
        return json.dumps({"subject": "s", "html": "<p>x</p>"})

    monkeypatch.setattr(admin_routes_mod, "_call_llm_chat", _fake_llm)
    r = client.post(
        GENERATE_URL,
        json={"provider": "openrouter", "brief": "b", "promo_code": "rentree10"},
        headers=admin_headers,
    )
    assert r.status_code == 200
    assert "https://wordly.art/pricing?promo=RENTREE10" in captured["system"]


def test_generate_wraps_rtl_language_in_dir_rtl(
    client, admin_headers, gen_env, monkeypatch
):
    """Arabe et persan : le HTML généré est enveloppé dans <div dir="rtl">."""
    _with_openrouter_key(monkeypatch)

    async def _fake_llm(admin_cfg, provider, model, system_prompt, user_prompt):
        assert 'dir="rtl"' in system_prompt  # consigne donnée au modèle
        return json.dumps({"subject": "s", "html": "<p>مرحبا</p>"})

    monkeypatch.setattr(admin_routes_mod, "_call_llm_chat", _fake_llm)
    for language in ("العربية", "فارسی"):
        r = client.post(
            GENERATE_URL,
            json={"provider": "openrouter", "brief": "b", "language": language},
            headers=admin_headers,
        )
        assert r.status_code == 200
        html = r.json()["data"]["html"]
        assert html.startswith('<div dir="rtl">')
        assert html.endswith("</div>")


def test_generate_field_bounds_are_enforced(client, admin_headers, gen_env):
    # Les bornes Pydantic sont appliquées (422 brut ou 400 via le gestionnaire
    # global RequestValidationError de l'application).
    r = client.post(
        GENERATE_URL,
        json={"provider": "openrouter", "brief": "b", "tone": "x" * 60},
        headers=admin_headers,
    )
    assert r.status_code in (400, 422)
    r2 = client.post(
        GENERATE_URL,
        json={"provider": "openrouter", "brief": "b", "model": "m" * 130},
        headers=admin_headers,
    )
    assert r2.status_code in (400, 422)


# ---------------------------------------------------------------------------
# Durcissement du nettoyage HTML (casse mixte, balises orphelines, data:)
# ---------------------------------------------------------------------------
def test_sanitize_generated_html_hardened():
    sanitize = admin_routes_mod._sanitize_generated_html
    # script orphelin (jamais fermé) et casse mixte
    assert "<script" not in sanitize('<p>ok</p><ScRiPt>alert(1)')
    # iframe / object / embed / form retirés avec leur contenu
    html = sanitize('<p>a</p><iframe src="https://x.y"><p>hidden</p></iframe><p>b</p>')
    assert "iframe" not in html and "hidden" not in html
    html = sanitize('<object data="x"></object><embed src="y"><form action="z"></form>')
    for tag in ("object", "embed", "form"):
        assert tag not in html
    # javascript: quel que soit le style, et data:
    assert "javascript:" not in sanitize("<a href='JaVa ScRiPt:alert(1)'>x</a>")
    assert "javascript:" not in sanitize("<a href='java\tscript:alert(1)'>x</a>")
    assert "data:" not in sanitize("<a href='DATA:text/html;base64,xxx'>x</a>")
    # le contenu sain passe intact
    assert sanitize('<p style="color:#333">Bonjour</p>') == '<p style="color:#333">Bonjour</p>'


def test_marketing_plan_excerpt_truncated(tmp_path, monkeypatch):
    plan = tmp_path / "MARKETING_PLAN.md"
    plan.write_text("## 1. A\n" + "x" * 9000 + "\n## 3. B\n", encoding="utf-8")
    monkeypatch.setattr(admin_routes_mod, "MARKETING_PLAN_FILE", plan)
    excerpt = admin_routes_mod._load_marketing_plan_excerpt()
    assert len(excerpt) <= admin_routes_mod.MARKETING_PLAN_PROMPT_MAX_CHARS + 10
    assert excerpt.startswith("## 1.")
    assert "## 3." not in excerpt
