#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""ZORAN 2 — Dynamique des traces, artefact autonome v1.1.

Q_total = Q_mesurable + Q_diffus est conservé sous atténuation.
Ce fichier est volontairement autonome : il ne dépend d'aucun autre moteur.
Les huit tests portent sur l'implémentation du modèle, pas sur la physique de q.
"""
from __future__ import annotations
import datetime as dt
import hashlib
import json
import math
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple

NON_MESURE = "NON_MESURÉ"
Pair = Tuple[str, str]

class AnnihilationError(ValueError): pass
class ConservationError(AssertionError): pass

@dataclass
class Registre:
    events: List[dict] = field(default_factory=list)
    def inscrire(self, operation: str, contenu: dict) -> None:
        self.events.append({"operation": operation, "contenu": contenu})
    def evenements(self, operation: Optional[str] = None) -> list:
        return [e for e in self.events if operation is None or e["operation"] == operation]

class TissuRelationnel:
    def __init__(self, registre: Optional[Registre] = None):
        self._aretes: Dict[Pair, float] = {}
        self._traces: Dict[str, List[List[str]]] = {}
        self.reg = registre or Registre()
    def _pair(self, a: str, b: str) -> Pair:
        if a == b: raise ValueError("self-relation interdite")
        return tuple(sorted((a, b)))
    def relier(self, a: str, b: str, q: float) -> None:
        if q <= 0: raise ValueError("q doit être positif")
        p = self._pair(a, b); self._aretes[p] = self._aretes.get(p, 0.0) + float(q)
        self.reg.inscrire("relier", {"arete": p, "q": q})
    def qualite_totale(self) -> float: return sum(self._aretes.values())
    def dissiper(self, a: str, plan: Dict[Pair, float]) -> None:
        if not plan or any(v <= 0 for v in plan.values()) or abs(sum(plan.values()) - 1) > 1e-12:
            raise AnnihilationError("plan non vide, positif et normalisé requis")
        q = sum(v for p, v in self._aretes.items() if a in p)
        if q <= 0: raise AnnihilationError("objet non relié")
        before = self.qualite_totale()
        self._aretes = {p: v for p, v in self._aretes.items() if a not in p}
        heirs = []
        for pair, weight in plan.items():
            p = self._pair(*pair); self._aretes[p] = self._aretes.get(p, 0.0) + q * weight
            heirs.append(list(p))
        self._traces[a] = heirs; self.reg.inscrire("dissiper", {"objet": a, "heritiers": heirs})
        if abs(before - self.qualite_totale()) > 1e-9: raise ConservationError("Q non conservé")
    def trace(self, a: str) -> List[List[str]]: return self._traces.get(a, [])

class TissuDynamique(TissuRelationnel):
    def __init__(self, lam: float = .1, epsilon: float = 1e-3, registre: Optional[Registre] = None):
        super().__init__(registre)
        if lam < 0 or epsilon <= 0: raise ValueError("λ ≥ 0 et ε > 0 requis")
        self.lam, self.eps, self.q_diffus, self.t = float(lam), float(epsilon), 0.0, 0.0
        self._sous_seuil: List[dict] = []
    def qualite_mesurable(self) -> float: return super().qualite_totale()
    def qualite_totale(self) -> float: return round(self.qualite_mesurable() + self.q_diffus, 12)
    def ecouler(self, seconds: float) -> dict:
        if seconds <= 0: raise ValueError("dt > 0 requis")
        before = self.qualite_totale(); factor = math.exp(-self.lam * seconds); diffuse = 0.; nm = []
        for p, q in list(self._aretes.items()):
            new = q * factor; diffuse += q - new
            if new < self.eps:
                diffuse += new; del self._aretes[p]; nm.append(list(p))
                self._sous_seuil.append({"arete": list(p), "q_residuel": new, "t": self.t + seconds})
            else: self._aretes[p] = new
        self.q_diffus = round(self.q_diffus + diffuse, 12); self.t = round(self.t + seconds, 12)
        self.reg.inscrire("ecouler", {"dt": seconds, "verse_au_diffus": round(diffuse, 12), "passees_non_mesure": nm})
        if abs(before - self.qualite_totale()) > 1e-9: raise ConservationError("Q_total non conservé")
        return {"t": self.t, "verse_au_diffus": round(diffuse, 12), "aretes_non_mesurees": nm, "Q_total": self.qualite_totale()}
    def mesurabilite(self, a: str, b: str): return self._aretes.get(self._pair(a, b), NON_MESURE)
    def demi_vie(self) -> Optional[float]: return None if self.lam == 0 else round(math.log(2) / self.lam, 6)
    def horizon_de_mesure(self, q0: float) -> Optional[float]:
        if self.lam == 0: return None
        return 0.0 if q0 <= self.eps else round(math.log(q0 / self.eps) / self.lam, 6)
    def localiser_derive(self, top: int = 3) -> list:
        return sorted(({"arete": list(p), "q": q, "fuite_par_unite_t": self.lam*q} for p, q in self._aretes.items()), key=lambda x: -x["fuite_par_unite_t"])[:top]

def run_tests() -> dict:
    out = {}
    t = TissuDynamique(.5, 1e-6); t.relier("a","b",10); t.relier("b","c",5); q=t.qualite_totale(); t.ecouler(3); out["D1_Q_total_invariant"] = t.qualite_totale()==q and t.q_diffus>0
    t = TissuDynamique(.3, 1e-6); t.relier("a","b",8); q=t.qualite_mesurable(); t.ecouler(2); out["D2_transfert_pas_perte"] = abs(q-t.qualite_mesurable()-t.q_diffus)<1e-9
    t = TissuDynamique(1,.5); t.relier("a","b",1); t.ecouler(2); out["D3_seuil_non_mesure"] = t.mesurabilite("a","b")==NON_MESURE and t.qualite_totale()==1
    t = TissuDynamique(.5,1e-15); t.relier("a","b",1); [t.ecouler(1) for _ in range(50)]; out["D4_asymptote_jamais_zero"] = t.mesurabilite("a","b")!=NON_MESURE and t.mesurabilite("a","b")>0
    t = TissuDynamique(.2,.1); t.relier("a","b",5); h=t.horizon_de_mesure(5); t.ecouler(h-.01); before=t.mesurabilite("a","b"); t.ecouler(.02); out["D5_peremption_principielle"] = before!=NON_MESURE and t.mesurabilite("a","b")==NON_MESURE
    t = TissuDynamique(.1,1e-9); t.relier("a","b",100); t.relier("c","d",1); out["D6_localise_derive"] = t.localiser_derive(1)[0]["arete"]==["a","b"]
    t = TissuDynamique(.4,1e-9); t.relier("a","b",6); t.relier("a","c",4); t.relier("b","c",2); q=t.qualite_totale(); t.dissiper("a",{("b","c"):1}); t.ecouler(5); out["D7_dissipation_puis_temps"] = abs(t.qualite_totale()-q)<1e-9 and t.trace("a")==[["b","c"]]
    t = TissuDynamique(1,.5); t.relier("a","b",1); t.ecouler(2); ev=t.reg.evenements("ecouler"); out["D8_rien_efface"] = len(t._sous_seuil)==1 and ev[-1]["contenu"]["passees_non_mesure"]==[["a","b"]]
    return out

if __name__ == "__main__":
    results = run_tests(); payload = {"tests": results, "verdict": "PASS_FORMEL" if all(results.values()) else "FAIL", "physical_validation": "NON_MESURE"}
    payload["results_sha256"] = hashlib.sha512(json.dumps(payload, sort_keys=True).encode()).hexdigest()
    print(json.dumps(payload, ensure_ascii=False, indent=2)); print(f"DYNAMIQUE DES TRACES : {sum(results.values())}/{len(results)} VERTS")
    raise SystemExit(0 if all(results.values()) else 1)
