# -*- coding: utf-8 -*-
"""Déroule une génération complète, avec les cas tordus, sans appeler l'API."""

import os
import shutil
import sys
import tempfile
import zipfile

BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, BASE)
sys.path.insert(0, os.path.join(BASE, "tests"))

# Bac à sable : base et projets isolés.
_BAC = tempfile.mkdtemp(prefix="plateforme-test-")
os.environ["PLATEFORME_PROJETS"] = os.path.join(_BAC, "projets")
os.environ["PLATEFORME_BASE"] = os.path.join(_BAC, "projets.db")
os.environ["PLATEFORME_BUDGET_SECONDES"] = "60"

import config, claude, livraison, projets, prompt, worker
import faux_client as fc

OK = [0, 0]


def v(cond, libelle):
    OK[0 if cond else 1] += 1
    print("  %s %s" % ("ok  " if cond else "ECHEC", libelle))


REF = "/tmp/claude-0/-home-user/62f16eb1-774a-5977-a315-11818ad1cb02/scratchpad/ref"

BRIEF = {
    "client": "B To Health",
    "secteur": "HealthTech",
    "nature": "Site événementiel — convention d'affaires",
    "objectif": "Inscriptions visiteurs et candidatures exposants",
    "public": "Donneurs d'ordres, PME de la filière santé",
    "slogan": "Votre business en Bourgogne-Franche-Comté",
    "domaine": "btohealth.com",
    "coordonnees": "03 81 40 47 56 — a.nolot@pmt-innovation.com",
    "reseaux": "linkedin.com/company/b-to-health-pmt",
    "date_lieu": "6 avril 2027, Stade Gaston Gérard, 21000 Dijon",
    "couleurs": "#1b537d dominante, #1a88b7, #47bcc8 accent, #555959 textes",
    "police_titres": "Dosis",
    "police_textes": "Roboto",
    "variantes_logo": "Trois variantes",
    "arborescence": "Accueil · Programme · Contact",
    "contenus": "Page d'accueil : diaporama, présentation, chiffres 2026",
    "references": "larentreedudm.com, innovativetherapiesdays.com",
    "remarques": "Visuel pas figé, ouverts aux propositions.",
    "pages": "la page d'accueil",
    "brief_libre": "Événement créé en 2022, 3e édition.",
}

print("=== Préparation ===")
projets.initialiser()
pid = projets.creer(BRIEF["client"])
entrees = projets.dossier_entrees(pid)
for source, nom in [
    (f"{REF}/assets/img/logos/logo-b-to-health.png", "logo-b-to-health.png"),
    (f"{REF}/assets/img/photos/DSC00635.jpg", "DSC00635.jpg"),
    (f"{REF}/assets/img/photos/DSC00041.jpg", "DSC00041.jpg"),
    (f"{REF}/assets/fonts/dosis-normal-latin.woff2", "dosis-normal-latin.woff2"),
]:
    shutil.copy2(source, os.path.join(entrees, nom))
open(os.path.join(entrees, "charte.pdf"), "wb").write(
    b"%PDF-1.4\n1 0 obj<</Type/Catalog>>endobj\ntrailer<</Root 1 0 R>>\n%%EOF\n")

conversation = prompt.preparer(pid, BRIEF)
site = projets.dossier_site(pid)

v(os.path.isfile(os.path.join(site, "assets/img/logos/logo-b-to-health.png")),
  "logo rangé dans assets/img/logos/")
v(os.path.isfile(os.path.join(site, "assets/img/photos/DSC00635.jpg")),
  "photo rangée dans assets/img/photos/")
v(os.path.isfile(os.path.join(site, "assets/fonts/dosis-normal-latin.woff2")),
  "police rangée dans assets/fonts/")

blocs = conversation["messages"][0]["content"]
types = [b["type"] for b in blocs]
v(types.count("document") == 1, "la charte PDF est jointe en document")
v(types.count("image") == 3, "3 visuels joints en image (vu : %d)" % types.count("image"))
v(types[-1] == "text", "le brief ferme le premier message")
v(blocs[-1].get("cache_control", {}).get("ttl") == "1h", "point de reprise du cache posé sur le brief")
v(conversation["systeme"][0].get("cache_control") is not None, "système mis en cache")
texte_brief = blocs[-1]["text"]
v("B To Health" in texte_brief and "assets/img/photos/DSC00635.jpg" in texte_brief,
  "le brief cite les chemins définitifs des visuels")
v("le PDF fait foi" in texte_brief, "consigne de lecture de la charte présente")
v("{" not in texte_brief.replace("{", "", 0) or "{client}" not in texte_brief,
  "aucun champ de gabarit non remplacé")
v(projets.lire(pid)["etat"] == projets.EN_ATTENTE, "projet mis en file")

print("\n=== Scénario : 8 tours, dont les cas tordus ===")
CSS = "/* Charte */\n:root{--bleu-nuit:#1b537d}\nbody{font-family:'Roboto'}\n"
HTML = ("<!DOCTYPE html><html lang=fr><head><title>B To Health</title>"
        "<link rel=stylesheet href=assets/css/style.css>"
        "<link rel=stylesheet href=assets/css/absente.css></head>"
        "<body><img src=assets/img/photos/DSC00635.jpg alt=Photo>"
        "<img src=assets/img/photos/JAMAIS.jpg alt=Manquante>"
        "<p>Sponsor Platinum : [[ à compléter ]]</p>"
        "<a href=https://exemple.fr>externe</a></body></html>")

scenario = [
    # 1 — annonce + première écriture
    fc.Reponse([fc.reflexion(), fc.texte("Charte retenue : #1b537d dominante, Dosis/Roboto."),
                fc.appel("t1", "ecrire_fichier",
                         {"chemin": "assets/css/style.css", "contenu": CSS})], "tool_use"),
    # 2 — tentative d'écriture hors périmètre : doit être refusée sans casser
    fc.Reponse([fc.appel("t2", "ecrire_fichier",
                         {"chemin": "../../evade.txt", "contenu": "X"})], "tool_use"),
    # 3 — réponse tronquée au plafond de jetons, avec un appel inexploitable
    fc.Reponse([fc.appel("t3", "ecrire_fichier",
                         {"chemin": "index.html", "contenu": "<!DOCTYPE"})], "max_tokens"),
    # 4 — le modèle bavarde sans rien faire : doit être relancé
    fc.Reponse([fc.texte("Je réfléchis à la mise en page.")], "end_turn"),
    # 5 — relecture
    fc.Reponse([fc.appel("t5", "lire_fichier", {"chemin": "assets/css/style.css"})], "tool_use"),
    # 6 — deux écritures dans le même tour
    fc.Reponse([fc.appel("t6a", "ecrire_fichier", {"chemin": "index.html", "contenu": HTML}),
                fc.appel("t6b", "ecrire_fichier",
                         {"chemin": "README.md", "contenu": "# B To Health\n"})], "tool_use"),
    # 7 — indisponibilité passagère de l'API
    RuntimeError("APIStatusError: overloaded_error"),
    # 8 — clôture
    fc.Reponse([fc.appel("t8", "terminer",
                         {"resume": "Accueil livrée. À remplacer : le sponsor Platinum."})],
               "tool_use"),
]

faux = fc.FauxClient(scenario)
claude._client = lambda: faux

print("\n=== Passage du worker (budget 60 s) ===")
worker.main()

p = projets.lire(pid)
print("\n--- état après le premier passage ---")
print("  etat =", p["etat"], "| tours =", p["tours"], "| echecs =", p["echecs"])
v(p["etat"] == projets.EN_ATTENTE, "l'échec API remet le projet en file (pas d'abandon)")
v(p["tours"] == 6, "6 tours effectués avant l'erreur (vu : %d)" % p["tours"])

print("\n=== Second passage (reprise) ===")
worker.main()
p = projets.lire(pid)
print("  etat =", p["etat"], "| tours =", p["tours"], "| echecs =", p["echecs"])
v(p["etat"] == projets.TERMINE, "terminé après reprise")
v(p["echecs"] == 0, "compteur d'échecs remis à zéro par le tour réussi")
v("sponsor Platinum" in (p["resume"] or ""), "résumé de livraison enregistré")

print("\n=== Vérifications ===")
v(not os.path.exists(os.path.join(os.path.dirname(projets.dossier(pid)), "evade.txt")),
  "aucune évasion hors du dossier projet")
v(os.path.isfile(os.path.join(site, "index.html")), "index.html écrit")
v(open(os.path.join(site, "index.html")).read() == HTML, "contenu fidèle")

anomalies = livraison.verifier(pid)
print("  anomalies relevées :")
for a in anomalies:
    print("    -", a)
v(any("absente.css" in a for a in anomalies), "feuille de styles absente détectée")
v(any("JAMAIS.jpg" in a for a in anomalies), "image absente détectée")
v(any("marqueur" in a for a in anomalies), "marqueur [[ ]] détecté")
v(not any("DSC00635" in a for a in anomalies), "la photo réellement présente n'est pas signalée")
v(not any("exemple.fr" in a for a in anomalies), "les liens externes sont ignorés")

archive = projets.chemin_archive(pid)
v(os.path.isfile(archive), "livraison.zip produit")
with zipfile.ZipFile(archive) as zf:
    noms = set(zf.namelist())
print("  archive :", len(noms), "fichiers")
v("index.html" in noms and "assets/css/style.css" in noms, "HTML et CSS dans l'archive")
v("assets/img/photos/DSC00635.jpg" in noms, "les visuels déposés sont dans l'archive")
v("assets/fonts/dosis-normal-latin.woff2" in noms, "les polices aussi")

print("\n=== Conversation et cache ===")
conv = projets.lire_conversation(pid)
msgs = conv["messages"]
v(msgs[0]["role"] == "user" and msgs[-1]["role"] == "user", "dialogue bien formé")
paires = [(m["role"]) for m in msgs]
v(all(paires[i] != paires[i + 1] for i in range(len(paires) - 1)), "rôles alternés")

# chaque tool_use doit avoir son tool_result : sinon l'API refuse la suite
appels, resultats = set(), set()
for m in msgs:
    if isinstance(m["content"], list):
        for b in m["content"]:
            if b.get("type") == "tool_use":
                appels.add(b["id"])
            if b.get("type") == "tool_result":
                resultats.add(b["tool_use_id"])
v(appels == resultats, "chaque appel d'outil a son résultat (%d/%d)" % (len(resultats), len(appels)))

v(any(b.get("type") == "thinking" for m in msgs if isinstance(m["content"], list)
      for b in m["content"]), "les blocs de réflexion sont conservés")

derniere = faux.requetes[-1]
points = 0
for bloc in derniere["system"]:
    points += 1 if bloc.get("cache_control") else 0
for m in derniere["messages"]:
    if isinstance(m.get("content"), list):
        for b in m["content"]:
            points += 1 if isinstance(b, dict) and b.get("cache_control") else 0
print("  points de reprise du cache dans la dernière requête :", points)
v(1 < points <= 4, "entre 2 et 4 points de reprise (limite de l'API)")
v(derniere["model"] == config.MODELE, "modèle épinglé : %s" % derniere["model"])
v(derniere["tools"] is not None and len(derniere["tools"]) == 4, "4 outils transmis")
v("temperature" not in derniere, "aucun paramètre d'échantillonnage (retiré sur Opus 5)")

# le disque ne doit pas conserver les points glissants
import json as _json
conv_disque = _json.loads(open(projets.chemin_conversation(pid), encoding="utf-8").read())
poses = sum(1 for m in conv_disque["messages"] if isinstance(m["content"], list)
            for b in m["content"] if isinstance(b, dict) and b.get("cache_control"))
v(poses == 1, "un seul point de reprise figé dans les messages sur disque (vu : %d) "
              "— le point glissant est recalculé à chaque requête" % poses)

print("\n=== Coût ===")
print("  jetons : entrée %d · sortie %d · cache lu %d" %
      (p["jetons_entree"], p["jetons_sortie"], p["jetons_cache_lecture"]))
print("  coût cumulé : %.4f USD" % projets.cout_dollars(p))
v(projets.cout_dollars(p) > 0, "coût calculé")

print("\n=== Journal ===")
for j in projets.journal(pid):
    detail = (j["detail"] or "").replace("\n", " ")[:72]
    print("  %-14s %s" % (j["evenement"], detail))

print("\n%d ok, %d échecs" % (OK[0], OK[1]))
shutil.rmtree(_BAC, ignore_errors=True)
sys.exit(1 if OK[1] else 0)
