#!/usr/bin/env python3
"""Tests du générateur de manifest de dépendances PIE · Annexe 12 · V10.1.

Stdlib pur (`unittest`) → aucune installation pip requise sur le runner Gitea.
La bibliothèque `jsonschema` sert d'*oracle* quand elle est présente.

Axes :
  1. BASELINE : le contrat vanille passe schéma + 10 invariants, est déterministe,
     et l'artefact `out/` commité == build frais (reproductibilité · #6).
  2. MUTATION : chaque invariant attrape bien la dérive qu'il protège (on casse
     le bundle en mémoire et on vérifie que `_validate` remonte une erreur).
  3. ANCRAGES : codes marques ⊆ CLAUDE.md §Projets ; annexe/version verbatim dans
     la directive ; modules downstream « gated » = répertoires de livrables réels.
"""

from __future__ import annotations

import copy
import json
import os
import subprocess
import sys
import tempfile
import unittest

_HERE = os.path.dirname(os.path.abspath(__file__))
_MODULE = os.path.normpath(os.path.join(_HERE, ".."))
_DELIVERABLES = os.path.normpath(os.path.join(_MODULE, "..", ".."))

sys.path.insert(0, _MODULE)
sys.path.insert(0, os.path.join(_DELIVERABLES, "publiciste"))

from pielib import builder, deps  # noqa: E402
from lib import validator as maison  # type: ignore  # noqa: E402
import pie_manifest_gen as gen  # noqa: E402

try:
    import jsonschema  # type: ignore

    _HAS_JSONSCHEMA = True
except Exception:  # pragma: no cover
    _HAS_JSONSCHEMA = False


def _load(path: str) -> dict:
    with open(path, encoding="utf-8") as fh:
        return json.load(fh)


class Baseline(unittest.TestCase):
    """Le contrat vanille passe schéma + 10 invariants et est déterministe."""

    def setUp(self):
        self.bundle = gen._build()

    def test_validate_clean(self):
        self.assertEqual(gen._validate(self.bundle), [])

    def test_counts_match_directive(self):
        c = self.bundle["traceability"]["counts"]
        self.assertEqual(c["master_data_groups"], 12)   # §16-29
        self.assertEqual(c["sync_rules"], 4)             # §82-86
        self.assertEqual(c["workflow_steps"], 10)        # §108-133
        self.assertEqual(c["brands"], 9)                 # §90-102

    def test_deterministic_rebuild(self):
        self.assertEqual(gen._build(), gen._build())

    def test_schema_oracle(self):
        if not _HAS_JSONSCHEMA:
            self.skipTest("jsonschema absent")
        schema = _load(gen._SCHEMA_PATH)
        jsonschema.validate(self.bundle["manifest"], schema)  # ne lève pas


class Reproducibility(unittest.TestCase):
    """L'artefact commité `out/` == build frais (byte-identique)."""

    def test_committed_matches_fresh(self):
        out = os.path.join(_MODULE, "out")
        if not os.path.isdir(out):
            self.skipTest("out/ non commité")
        with tempfile.TemporaryDirectory() as tmp:
            rc = gen.main(["build", "-o", tmp])
            self.assertEqual(rc, 0)
            for name in ("pie_manifest.json", "MANIFEST.json"):
                with open(os.path.join(out, name), encoding="utf-8") as a, \
                     open(os.path.join(tmp, name), encoding="utf-8") as b:
                    self.assertEqual(a.read(), b.read(), f"{name} a dérivé de sa source")


class Anchors(unittest.TestCase):
    """Les ancrages hors-module pointent bien vers du réel (anti-invention #6)."""

    def setUp(self):
        self.man = gen._build()["manifest"]

    def test_brand_codes_subset_of_claude_md(self):
        codes = {b["code"] for b in self.man["brands"]}
        canon = deps.claude_md_project_codes()
        self.assertTrue(canon, "aucun code Pxx lu dans CLAUDE.md §Projets")
        self.assertTrue(codes <= canon, f"codes hors constitution : {codes - canon}")

    def test_annexe_version_in_directive(self):
        decl = deps.directive_declares(self.man_directive(), "Annexe 12", "V10.1")
        self.assertTrue(all(decl.values()), f"ancrage directive rompu : {decl}")

    def test_gated_modules_exist_on_fs(self):
        for d in self.man["downstream_registry"]:
            if d["statut"] == "gated":
                self.assertTrue(deps.module_exists(d["module"]),
                                f"module gated introuvable : {d['module']}")

    def man_directive(self) -> str:
        return gen._build()["traceability"]["directive_source"]


class Mutations(unittest.TestCase):
    """Chaque invariant attrape la dérive qu'il protège."""

    def setUp(self):
        self.bundle = gen._build()

    def _breaks(self, mutate) -> None:
        bundle = copy.deepcopy(self.bundle)
        mutate(bundle)
        self.assertNotEqual(gen._validate(bundle), [],
                            "la mutation aurait dû être refusée")

    def test_inv2_missing_group(self):
        self._breaks(lambda b: b["manifest"]["master_data_groups"].pop())

    def test_inv2_wrong_group_key(self):
        def m(b):
            b["manifest"]["master_data_groups"][0]["cle"] = "inventé"
        self._breaks(m)

    def test_inv3_unknown_trigger(self):
        def m(b):
            b["manifest"]["sync_rules"][0]["trigger"] = "couleur"
        self._breaks(m)

    def test_inv4_dangling_downstream(self):
        def m(b):
            b["manifest"]["sync_rules"][0]["downstream"].append("fantome")
        self._breaks(m)

    def test_inv4_dangling_trigger_group(self):
        def m(b):
            b["manifest"]["sync_rules"][0]["trigger_group"] = "fantome"
        self._breaks(m)

    def test_inv5_status_incoherent(self):
        def m(b):
            b["manifest"]["downstream_registry"][0]["statut"] = "gated"
            b["manifest"]["downstream_registry"][0]["module"] = None
        self._breaks(m)

    def test_inv6_phantom_module(self):
        def m(b):
            for d in b["manifest"]["downstream_registry"]:
                if d["module"]:
                    d["module"] = "faisabilite/inexistant"
                    return
        self._breaks(m)

    def test_inv7_non_contiguous_workflow(self):
        def m(b):
            b["manifest"]["workflow"][0]["etape"] = 99
        self._breaks(m)

    def test_inv8_alien_brand_code(self):
        def m(b):
            b["manifest"]["brands"][0]["code"] = "P99"
        self._breaks(m)

    def test_inv9_wrong_annexe(self):
        def m(b):
            b["manifest"]["annexe"] = 7  # « Annexe 7 » n'est pas dans la directive
        self._breaks(m)

    def test_inv10_storage_not_vps(self):
        def m(b):
            b["manifest"]["storage_layout"]["racine"] = "05_deliverables_mvp/pie/"
        self._breaks(m)

    def test_count_incoherent(self):
        def m(b):
            b["traceability"]["counts"]["brands"] = 999
        self._breaks(m)


if __name__ == "__main__":  # pragma: no cover
    unittest.main()
