#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""ZORAN — moteur relationnel minimal et falsifiable.

Autonome, déterministe, sans dépendance externe.
Les huit tests vérifient uniquement les définitions du cadre formel.
Ils ne constituent pas une validation physique de q, de S ou du principe d'ouverture.
"""
from __future__ import annotations

from dataclasses import dataclass, field
from random import Random
from typing import Dict, List, Optional, Tuple
import hashlib
import json


class RelationError(ValueError):
    pass


@dataclass
class Tissue:
    nodes: set[str]
    edges: Dict[Tuple[str, str], float] = field(default_factory=dict)
    trace: List[dict] = field(default_factory=list)

    def __post_init__(self) -> None:
        self.nodes = set(self.nodes)
        for (a, b), q in list(self.edges.items()):
            if a == b or a not in self.nodes or b not in self.nodes or q <= 0:
                raise RelationError("invalid relation")
            key = tuple(sorted((a, b)))
            if key != (a, b):
                del self.edges[(a, b)]
                self.edges[key] = q

    def total_q(self) -> float:
        return sum(self.edges.values())

    def incidence(self, node: str) -> float:
        return sum(q for (a, b), q in self.edges.items() if node in (a, b))

    def shadow(self, node: str) -> Optional[float]:
        phi = self.incidence(node)
        return phi if phi > 0 else None

    def status(self, node: str) -> str:
        if self.incidence(node) > 0:
            return "RELIE"
        if any(item["node"] == node for item in self.trace):
            return "DISSIPE"
        return "SINGULARITE"

    def dissipate(self, node: str, plan: Dict[Tuple[str, str], float]) -> None:
        if node not in self.nodes:
            raise RelationError("unknown node")
        if not plan or any(p <= 0 for p in plan.values()):
            raise RelationError("non-empty positive plan required")
        if abs(sum(plan.values()) - 1.0) > 1e-12:
            raise RelationError("plan weights must sum to one")
        survivors = self.nodes - {node}
        if any(a not in survivors or b not in survivors or a == b for a, b in plan):
            raise RelationError("plan must use distinct surviving nodes")
        amount = self.incidence(node)
        if amount <= 0:
            raise RelationError("only a related node can be dissipated")
        before = self.total_q()
        self.edges = {(a, b): q for (a, b), q in self.edges.items() if node not in (a, b)}
        for pair, weight in plan.items():
            key = tuple(sorted(pair))
            self.edges[key] = self.edges.get(key, 0.0) + amount * weight
        self.trace.append({"node": node, "amount": amount, "heirs": sorted(plan)})
        if abs(before - self.total_q()) > 1e-10:
            raise AssertionError("conservation violated")


def digest(value: object) -> str:
    return hashlib.sha256(json.dumps(value, sort_keys=True, default=str).encode()).hexdigest()


def test_singularity_indefinite() -> None:
    t = Tissue({"a"})
    assert t.shadow("a") is None and t.status("a") == "SINGULARITE"


def test_relation_carries_shadow() -> None:
    t = Tissue({"a", "b"}, {("a", "b"): 5.0})
    assert t.shadow("a") == 5.0 and t.shadow("b") == 5.0


def test_conservation() -> None:
    t = Tissue({"a", "b", "c"}, {("a", "b"): 2.0, ("a", "c"): 3.0})
    before = t.total_q()
    t.dissipate("a", {("b", "c"): 1.0})
    assert abs(before - t.total_q()) < 1e-10


def test_annihilation_rejected() -> None:
    t = Tissue({"a", "b"}, {("a", "b"): 1.0})
    try:
        t.dissipate("a", {})
    except RelationError:
        return
    raise AssertionError("empty plan was accepted")


def test_leaking_plan_rejected() -> None:
    t = Tissue({"a", "b", "c"}, {("a", "b"): 1.0})
    try:
        t.dissipate("a", {("b", "c"): 0.9})
    except RelationError:
        return
    raise AssertionError("non-conservative plan was accepted")


def test_two_zeros_are_distinct() -> None:
    singular = Tissue({"s"})
    dissipated = Tissue({"a", "b", "c"}, {("a", "b"): 1.0})
    dissipated.dissipate("a", {("b", "c"): 1.0})
    assert singular.shadow("s") is None
    assert dissipated.shadow("a") is None and dissipated.status("a") == "DISSIPE"


def test_underdetermination() -> None:
    left = Tissue({"a", "b", "c"}, {("a", "b"): 2.0, ("a", "c"): 3.0})
    right = Tissue({"a", "b"}, {("a", "b"): 5.0})
    assert left.shadow("a") == right.shadow("a") and left.edges != right.edges


def test_trace_preserved() -> None:
    t = Tissue({"a", "b", "c"}, {("a", "b"): 4.0})
    t.dissipate("a", {("b", "c"): 1.0})
    assert t.trace and t.trace[0]["node"] == "a" and t.trace[0]["heirs"]


def randomized_checks() -> int:
    rng = Random(20260812)
    dissipations = 0
    for trial in range(2000):
        nodes = {f"n{i}" for i in range(8)}
        edges = {("n0", "n1"): 1.0 + rng.random(), ("n0", "n2"): 1.0 + rng.random(),
                 ("n3", "n4"): 1.0 + rng.random()}
        t = Tissue(nodes, edges)
        count = 3 if trial < 137 else 2
        for step in range(count):
            node = next(node for node in sorted(t.nodes) if t.incidence(node) > 0)
            survivors = sorted(t.nodes - {node})
            pair = (survivors[0], survivors[1])
            before = t.total_q()
            t.dissipate(node, {pair: 1.0})
            assert abs(before - t.total_q()) < 1e-10
            assert t.trace[-1]["heirs"]
            dissipations += 1
    return dissipations


def main() -> None:
    tests = [test_singularity_indefinite, test_relation_carries_shadow, test_conservation,
             test_annihilation_rejected, test_leaking_plan_rejected, test_two_zeros_are_distinct,
             test_underdetermination, test_trace_preserved]
    results = []
    for test in tests:
        test()
        results.append({"test": test.__name__, "verdict": "PASS"})
    operations = randomized_checks()
    payload = {"tests": results, "random_trials": 2000, "dissipations": operations,
               "verdict": "PASS_FORMEL", "physical_validation": "NON_MESURE"}
    payload["results_sha256"] = digest(payload)
    print(json.dumps(payload, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()
