"""
Endpoints marketing (spec « Stratégie LLM par abonnement ») :
- audiences avec compteurs (liste d'attente, inactifs, par plan, tous),
- envoi test obligatoire avant tout envoi en masse,
- désabonnés exclus de tout envoi,
- historique journalisé (envoyés/échecs),
- désabonnement public.
"""

import asyncio
import json
import pytest
from pathlib import Path
from fastapi.testclient import TestClient

ADMIN_LOGIN_URL = "/api/v1/admin/login"
AUDIENCES_URL = "/api/v1/admin/marketing/audiences"
SEND_URL = "/api/v1/admin/marketing/email/send"
HISTORY_URL = "/api/v1/admin/marketing/email/history"
UNSUBSCRIBE_URL = "/api/v1/marketing/unsubscribe"

SUBJECT = "Relance test"
HTML = "<html><body><p>Bonjour !</p></body></html>"


def _write_json(path: Path, data) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")


@pytest.fixture
def marketing_env(tmp_path: Path, monkeypatch):
    """Fichiers data isolés + service email simulé."""
    import routes.admin_routes as admin_routes_mod
    import routes.waitlist_routes as waitlist_mod
    import services.email_service as email_svc

    history_file = tmp_path / "marketing_emails.json"
    unsub_file = tmp_path / "unsubscribes.json"
    waitlist_file = tmp_path / "waitlist.json"

    monkeypatch.setattr(admin_routes_mod, "MARKETING_HISTORY_FILE", history_file)
    monkeypatch.setattr(admin_routes_mod, "UNSUBSCRIBES_FILE", unsub_file)
    monkeypatch.setattr(waitlist_mod, "WAITLIST_FILE", waitlist_file)
    monkeypatch.setattr(admin_routes_mod, "_active_campaign_hashes", set())

    sent_to: list[str] = []
    captured_headers: dict[str, dict] = {}

    async def _fake_send(to: str, subject: str, body: str, extra_headers=None) -> bool:
        sent_to.append(to)
        captured_headers[to] = extra_headers or {}
        return True

    async def _failing_send(to: str, subject: str, body: str, extra_headers=None) -> bool:
        return False

    monkeypatch.setattr(email_svc, "send_email_async", _fake_send)
    monkeypatch.setattr(email_svc, "is_smtp_configured", lambda: True)

    def set_sender(fn):
        monkeypatch.setattr(email_svc, "send_email_async", fn)

    return {
        "history_file": history_file,
        "unsub_file": unsub_file,
        "waitlist_file": waitlist_file,
        "sent_to": sent_to,
        "captured_headers": captured_headers,
        "fail_mode": lambda: set_sender(_failing_send),
        "set_sender": set_sender,
    }


@pytest.fixture
def client(marketing_env, monkeypatch, tmp_path: Path):
    """TestClient avec authentification JSON (sans base de données)."""
    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 main import app

    return TestClient(app, raise_server_exceptions=True)


@pytest.fixture
def admin_token(client, monkeypatch):
    import routes.admin_routes as admin_routes_mod

    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 r.json()["access_token"]


@pytest.fixture
def auth_headers(admin_token):
    return {"Authorization": f"Bearer {admin_token}"}


def _seed_users(tmp_path: Path) -> Path:
    users = {
        "u-pro": {
            "email": "pro@example.com",
            "name": "Pro User",
            "plan": "pro",
            "docs_translated_this_month": 5,
        },
        "u-idle": {
            "email": "idle@example.com",
            "name": "Idle User",
            "plan": "starter",
            "docs_translated_this_month": 0,
        },
    }
    path = tmp_path / "users.json"
    _write_json(path, users)
    return path


# ---------------------------------------------------------------------------
# Audiences
# ---------------------------------------------------------------------------
def test_audiences_counts_and_unsubscribed_excluded(
    client, auth_headers, marketing_env, tmp_path
):
    _seed_users(tmp_path)
    _write_json(
        marketing_env["waitlist_file"],
        [
            {"email": "lead1@example.com", "interest": "pdf", "joined_at": "2026-09-01"},
            {"email": "lead2@example.com", "interest": None, "joined_at": "2026-09-02"},
        ],
    )
    _write_json(marketing_env["unsub_file"], [{"email": "lead2@example.com"}])

    r = client.get(AUDIENCES_URL, headers=auth_headers)
    assert r.status_code == 200, r.text
    data = r.json()["data"]
    assert data["audiences"]["waitlist"] == 1  # lead2 désabonné → exclu
    assert data["audiences"]["plan:pro"] == 1
    assert data["audiences"]["inactive_30"] == 1  # idle@example.com
    assert data["audiences"]["all_users"] == 2
    assert data["unsubscribed_count"] == 1


def test_audiences_requires_admin(client):
    assert client.get(AUDIENCES_URL).status_code == 401


# ---------------------------------------------------------------------------
# Envoi test + garde-fou « test avant envoi réel »
# ---------------------------------------------------------------------------
def _post_send(client, auth_headers, **overrides):
    payload = {
        "audience": "waitlist",
        "subject": SUBJECT,
        "html": HTML,
        "test_mode": True,
    }
    payload.update(overrides)
    return client.post(SEND_URL, json=payload, headers=auth_headers)


def test_test_send_writes_history(client, auth_headers, marketing_env):
    _write_json(
        marketing_env["waitlist_file"],
        [{"email": "lead1@example.com"}],
    )
    r = _post_send(client, auth_headers, test_email="admin@example.com")
    assert r.status_code == 200, r.text
    assert r.json()["data"]["sent"] == 1
    assert marketing_env["sent_to"] == ["admin@example.com"]

    entries = json.loads(marketing_env["history_file"].read_text(encoding="utf-8"))
    assert len(entries) == 1
    assert entries[0]["test_mode"] is True
    assert entries[0]["sent_count"] == 1
    assert entries[0]["content_hash"]


def test_mass_send_requires_matching_test_first(client, auth_headers, marketing_env):
    _write_json(marketing_env["waitlist_file"], [{"email": "lead1@example.com"}])
    r = _post_send(client, auth_headers, test_mode=False)
    assert r.status_code == 400
    assert r.json()["error"] == "TEST_SEND_REQUIRED"
    assert marketing_env["sent_to"] == []


def test_mass_send_after_test_excludes_unsubscribed(client, auth_headers, marketing_env):
    _write_json(
        marketing_env["waitlist_file"],
        [
            {"email": "lead1@example.com"},
            {"email": "gone@example.com"},
        ],
    )
    _write_json(marketing_env["unsub_file"], [{"email": "gone@example.com"}])

    # Envoi test au contenu identique (préalable obligatoire)
    assert _post_send(client, auth_headers, test_email="admin@example.com").status_code == 200
    marketing_env["sent_to"].clear()

    r = _post_send(client, auth_headers, test_mode=False)
    assert r.status_code == 202, r.text
    assert r.json()["data"]["queued"] == 1  # gone@example.com exclu de la file


def test_bulk_executor_logs_history_and_excludes_unsubscribed(marketing_env):
    """L'exécuteur d'envoi de fond journalise envoyés/échecs et n'écrit
    jamais à un désabonné (résolution d'audience + envoi réel)."""
    import routes.admin_routes as admin_routes_mod

    _write_json(
        marketing_env["waitlist_file"],
        [
            {"email": "lead1@example.com"},
            {"email": "gone@example.com"},
        ],
    )
    _write_json(marketing_env["unsub_file"], [{"email": "gone@example.com"}])

    recipients = admin_routes_mod._resolve_audience("waitlist")
    assert [r["email"] for r in recipients] == ["lead1@example.com"]

    entry = {"id": "x", "audience": "waitlist", "test_mode": False,
             "recipients_total": len(recipients), "sent_count": 0,
             "failed_count": 0, "failed": [], "sent_at": "2026-09-05T00:00:00+00:00"}
    asyncio.run(
        admin_routes_mod._execute_bulk_send(SUBJECT, HTML, recipients, entry, "admin")
    )

    assert marketing_env["sent_to"] == ["lead1@example.com"]
    entries = json.loads(marketing_env["history_file"].read_text(encoding="utf-8"))
    assert entries[-1]["sent_count"] == 1
    assert entries[-1]["failed_count"] == 0
    assert "completed_at" in entries[-1]


def test_bulk_executor_continues_after_individual_smtp_failure(marketing_env):
    """Un échec SMTP individuel n'interrompt pas l'envoi (jamais d'erreur 500)."""
    import routes.admin_routes as admin_routes_mod
    async def _flaky_send(to: str, subject: str, body: str, extra_headers=None) -> bool:
        if to == "bad@example.com":
            raise RuntimeError("SMTP down")
        marketing_env["sent_to"].append(to)
        return True

    marketing_env["set_sender"](_flaky_send)
    _write_json(
        marketing_env["waitlist_file"],
        [{"email": "bad@example.com"}, {"email": "good@example.com"}],
    )

    recipients = admin_routes_mod._resolve_audience("waitlist")
    entry = {"id": "y", "audience": "waitlist", "test_mode": False,
             "recipients_total": len(recipients), "sent_count": 0,
             "failed_count": 0, "failed": [], "sent_at": "2026-09-05T00:00:00+00:00"}
    asyncio.run(
        admin_routes_mod._execute_bulk_send(SUBJECT, HTML, recipients, entry, "admin")
    )

    assert entry["sent_count"] == 1
    assert entry["failed_count"] == 1
    assert entry["failed"][0]["email"] == "bad@example.com"
    assert marketing_env["sent_to"] == ["good@example.com"]


def test_unsubscribed_via_public_link_is_excluded(client, auth_headers, marketing_env):
    _write_json(marketing_env["waitlist_file"], [{"email": "bye@example.com"}])
    r = client.get(UNSUBSCRIBE_URL, params={"email": "bye@example.com"})
    assert r.status_code == 200
    unsub = json.loads(marketing_env["unsub_file"].read_text(encoding="utf-8"))
    assert [u["email"] for u in unsub] == ["bye@example.com"]

    # Audiences vides → l'envoi est refusé proprement (pas d'erreur 500).
    r2 = _post_send(client, auth_headers, test_mode=False)
    assert r2.status_code == 400
    assert r2.json()["error"] in ("AUDIENCE_EMPTY", "TEST_SEND_REQUIRED")


# ---------------------------------------------------------------------------
# Historique + résilience SMTP
# ---------------------------------------------------------------------------
def test_history_endpoint_returns_entries(client, auth_headers, marketing_env):
    _write_json(
        marketing_env["history_file"],
        [
            {
                "id": "abc",
                "sent_at": "2026-09-05T10:00:00+00:00",
                "audience": "waitlist",
                "subject": SUBJECT,
                "test_mode": True,
                "recipients_total": 1,
                "sent_count": 1,
                "failed_count": 0,
                "failed": [],
            }
        ],
    )
    r = client.get(HISTORY_URL, headers=auth_headers)
    assert r.status_code == 200
    entries = r.json()["data"]
    assert len(entries) == 1
    assert entries[0]["id"] == "abc"


def test_bulk_failure_does_not_raise_and_is_logged(client, auth_headers, marketing_env):
    marketing_env["fail_mode"]()
    _write_json(marketing_env["waitlist_file"], [{"email": "lead1@example.com"}])
    assert _post_send(client, auth_headers, test_email="admin@example.com").status_code == 502

    entry = json.loads(marketing_env["history_file"].read_text(encoding="utf-8"))[0]
    assert entry["sent_count"] == 0
    assert entry["failed_count"] == 1

    marketing_env["sent_to"].clear()
    # L'envoi test ayant échoué, l'envoi réel reste bloqué.
    r = _post_send(client, auth_headers, test_mode=False)
    assert r.status_code == 400
    assert r.json()["error"] == "TEST_SEND_REQUIRED"


# ---------------------------------------------------------------------------
# En-têtes de délivrabilité, verrou anti-simultané, re-vérification des désabonnés
# ---------------------------------------------------------------------------
def test_bulk_send_carries_list_unsubscribe_headers(marketing_env):
    """Les emails marketing embarquent List-Unsubscribe / List-Unsubscribe-Post."""
    import routes.admin_routes as admin_routes_mod

    _write_json(marketing_env["waitlist_file"], [{"email": "lead1@example.com"}])
    recipients = admin_routes_mod._resolve_audience("waitlist")
    entry = {"id": "h1", "audience": "waitlist", "test_mode": False,
             "recipients_total": 1, "sent_count": 0, "failed_count": 0,
             "failed": [], "content_hash": "abc", "sent_at": "t"}
    asyncio.run(
        admin_routes_mod._execute_bulk_send(SUBJECT, HTML, recipients, entry, "admin")
    )

    headers = marketing_env["captured_headers"]["lead1@example.com"]
    assert headers["List-Unsubscribe"].startswith("<http")
    assert "unsubscribe?email=" in headers["List-Unsubscribe"]
    from urllib.parse import unquote

    assert "lead1@example.com" in unquote(headers["List-Unsubscribe"])
    assert headers["List-Unsubscribe-Post"] == "List-Unsubscribe=One-Click"
    # L'entrée « running » mise à jour (pas dupliquée) et clôturée.
    entries = json.loads(marketing_env["history_file"].read_text(encoding="utf-8"))
    assert len(entries) == 1
    assert entries[0]["status"] == "completed"


def test_mass_send_writes_running_entry_then_completes(client, auth_headers, marketing_env):
    _write_json(marketing_env["waitlist_file"], [{"email": "lead1@example.com"}])
    assert _post_send(client, auth_headers, test_email="a@ex.com").status_code == 200
    marketing_env["sent_to"].clear()

    assert _post_send(client, auth_headers, test_mode=False).status_code == 202
    entries = json.loads(marketing_env["history_file"].read_text(encoding="utf-8"))
    bulk = [e for e in entries if not e["test_mode"]]
    assert len(bulk) == 1  # une seule entrée : « running » puis mise à jour
    assert bulk[0]["status"] in ("running", "completed")


def test_second_real_send_same_content_refused_while_running(client, auth_headers, marketing_env):
    """Deux campagnes simultanées de même contenu : la seconde est refusée."""
    import routes.admin_routes as admin_routes_mod

    _write_json(marketing_env["waitlist_file"], [{"email": "lead1@example.com"}])
    assert _post_send(client, auth_headers, test_email="a@ex.com").status_code == 200

    # On simule une campagne déjà en cours pour ce contenu exact.
    content_hash = admin_routes_mod._marketing_content_hash(SUBJECT, HTML)
    admin_routes_mod._active_campaign_hashes.add(content_hash)
    try:
        r = _post_send(client, auth_headers, test_mode=False)
        assert r.status_code == 409
        assert r.json()["error"] == "CAMPAIGN_ALREADY_RUNNING"
    finally:
        admin_routes_mod._active_campaign_hashes.discard(content_hash)


def test_unsubscribe_during_bulk_send_is_honored(marketing_env):
    """Un désabonnement déposé pendant la campagne stoppe l'envoi restant."""
    import routes.admin_routes as admin_routes_mod
    import services.email_service as email_svc

    _write_json(
        marketing_env["waitlist_file"],
        [{"email": "a@example.com"}, {"email": "b@example.com"}],
    )

    async def _slow_send(to: str, subject: str, body: str, extra_headers=None) -> bool:
        if to == "a@example.com":
            # Se désabonne pendant que la campagne est en cours.
            with admin_routes_mod._marketing_lock:
                entries = admin_routes_mod._load_unsubscribes()
                entries.append({"email": "b@example.com", "unsubscribed_at": "t"})
                admin_routes_mod._save_unsubscribes(entries)
        marketing_env["sent_to"].append(to)
        return True

    marketing_env["set_sender"](_slow_send)

    recipients = admin_routes_mod._resolve_audience("waitlist")
    entry = {"id": "h2", "audience": "waitlist", "test_mode": False,
             "recipients_total": len(recipients), "sent_count": 0,
             "failed_count": 0, "failed": [], "content_hash": "xyz", "sent_at": "t"}
    asyncio.run(
        admin_routes_mod._execute_bulk_send(SUBJECT, HTML, recipients, entry, "admin")
    )

    assert marketing_env["sent_to"] == ["a@example.com"]  # b est passé en désabonné
    assert entry["sent_count"] == 1
    assert entry["skipped_unsubscribed"] == 1
    assert entry["status"] == "completed"


def test_unsubscribe_page_escapes_email(client, marketing_env):
    r = client.get(UNSUBSCRIBE_URL, params={"email": "<script>alert(1)</script>@x.com"})
    assert r.status_code == 200
    body = r.text
    assert "<script>" not in body
    assert "&lt;script&gt;" in body


def test_put_settings_pristine_ai_tiers_keeps_existing(client, auth_headers, monkeypatch, tmp_path):
    """Un PUT avec ai_tiers vierge ne réinitialise pas les paliers personnalisés."""
    import routes.admin_routes as admin_routes_mod

    settings_file = tmp_path / "provider_settings.json"
    monkeypatch.setattr(admin_routes_mod, "SETTINGS_FILE", str(settings_file))
    custom = {
        "ai_tiers": {
            "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"],
                "default_model": "anthropic/claude-sonnet-5",
            },
        }
    }
    admin_routes_mod.save_settings(admin_routes_mod.SettingsConfig.model_validate(custom))

    # PUT avec un bloc ai_tiers vierge (client sans gestion des paliers).
    r = client.put(
        "/api/v1/admin/settings",
        json={"smtp": {}, "ai_tiers": {"essential": {"models": [], "default_model": ""},
                                       "premium": {"models": [], "default_model": ""}}},
        headers=auth_headers,
    )
    assert r.status_code == 200, r.text
    saved = admin_routes_mod.load_settings()
    assert saved.ai_tiers.essential.default_model == "z-ai/glm-5.3-flash"
    assert saved.ai_tiers.essential.models[0] == "z-ai/glm-5.3-flash"

    # Un PUT avec un défaut Essentielle appartenant à la gamme Premium → 400.
    r2 = client.put(
        "/api/v1/admin/settings",
        json={"ai_tiers": {"essential": {"models": [], "default_model": "anthropic/claude-sonnet-5"},
                           "premium": {"models": [], "default_model": ""}}},
        headers=auth_headers,
    )
    assert r2.status_code == 400
    assert r2.json()["error"] == "INVALID_AI_TIER"
