"""Tests du générateur de plan User Permission (stdlib `unittest`, zéro pip).

Réutilise le validateur maison Publiciste (workflow #5 · pas de duplication) et,
s'il est installé, l'oracle `jsonschema` en plus. Invariants couverts :
  1. Le plan valide contre userperm.schema.json (maison + oracle).
  2. Exactement 50 entrées, une par rôle du contrat (couverture bijective).
  3. Le mécanisme de chaque entrée = le mapping natif de son scope_donnees.
  4. Un template User Permission existe SSI portée `entite` (own/groupe/equipe → null).
  5. Fidélité du template : allow=Company, for_value=entite du contrat, user=sentinelle.
  6. Anti-invention : aucun utilisateur réel (toujours la sentinelle), aucune
     restriction fabriquée hors portée `entite`.
  7. Manifeste : Companies + rôles `equipe` à confirmer = exactement ceux du contrat.
  8. Déterminisme : deux builds successifs sont bit-à-bit identiques.
  9. Garde-fous : scope inconnu et `entite`+périmètre consolidé → ValueError.
 10. Le CLI validate fonctionne de bout en bout.
"""

import importlib.util
import json
import os
import sys
import unittest

_HERE = os.path.dirname(os.path.abspath(__file__))
_GEN = os.path.normpath(os.path.join(_HERE, ".."))            # userperm_gen/
_RBAC = os.path.normpath(os.path.join(_GEN, ".."))            # rbac/
_DELIVERABLES = os.path.normpath(os.path.join(_RBAC, ".."))   # 05_deliverables_mvp/

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

from permlib import builder, frappe  # noqa: E402
from lib import validator as maison  # noqa: E402

_CONTRACT = os.path.join(_RBAC, "rbac_50_roles.json")
_SCHEMA = os.path.join(_GEN, "userperm.schema.json")


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


def _load_cli():
    spec = importlib.util.spec_from_file_location(
        "userperm_gen", os.path.join(_GEN, "userperm_gen.py")
    )
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


class UserPermGenTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.contract = _load(_CONTRACT)
        cls.schema = _load(_SCHEMA)
        cls.bundle = builder.build_plan(cls.contract)
        cls.plan = cls.bundle["plan"]
        cls.by_role = {r["erpnext_role_name"]: r for r in cls.contract["roles"]}

    # 1 · conformité au schéma (validateur maison)
    def test_plan_valide_schema_maison(self):
        errors = maison.validate(self.bundle, self.schema)
        self.assertEqual(errors, [], f"Erreurs de schéma (maison) : {errors}")

    # 1bis · oracle jsonschema si disponible
    def test_plan_valide_oracle_jsonschema(self):
        try:
            import jsonschema  # noqa: PLC0415
        except ImportError:
            self.skipTest("jsonschema non installé — oracle ignoré")
        jsonschema.validate(self.bundle, self.schema)

    # 2 · couverture bijective des 50 rôles
    def test_couverture_50_roles(self):
        self.assertEqual(len(self.plan), 50)
        roles = [e["erpnext_role_name"] for e in self.plan]
        self.assertEqual(len(roles), len(set(roles)), "Rôle dupliqué")
        self.assertEqual(set(roles), set(self.by_role), "Plan ≠ contrat")

    # 3 · mécanisme = mapping natif du scope
    def test_mecanisme_reflete_scope(self):
        for e in self.plan:
            scope = self.by_role[e["erpnext_role_name"]]["scope_donnees"]
            self.assertEqual(e["scope_donnees"], scope)
            self.assertEqual(e["mechanism"], frappe.SCOPE_MECHANISM[scope])

    # 4 · template SSI portée entite
    def test_template_ssi_entite(self):
        for e in self.plan:
            tmpl = e["user_permission_template"]
            if e["scope_donnees"] == "entite":
                self.assertIsNotNone(tmpl, f"{e['erpnext_role_name']}: template manquant")
            else:
                self.assertIsNone(
                    tmpl, f"{e['erpnext_role_name']}: template fabriqué hors portée entite"
                )

    # 5 · fidélité du template au contrat
    def test_template_fidele_au_contrat(self):
        for e in self.plan:
            if e["scope_donnees"] != "entite":
                continue
            tmpl = e["user_permission_template"]
            entite = self.by_role[e["erpnext_role_name"]]["entite_principale"]
            self.assertEqual(tmpl["doctype"], "User Permission")
            self.assertEqual(tmpl["allow"], "Company")
            self.assertEqual(tmpl["for_value"], entite)
            self.assertEqual(tmpl["apply_to_all_doctypes"], 1)

    # 6 · anti-invention : jamais d'utilisateur réel, jamais de Company consolidée
    def test_anti_invention_user_et_company(self):
        for e in self.plan:
            tmpl = e["user_permission_template"]
            if tmpl is not None:
                self.assertEqual(
                    tmpl["user"], frappe.USER_PLACEHOLDER,
                    "Utilisateur inventé dans un template (viole #6)",
                )
                self.assertNotIn(tmpl["for_value"], frappe.NON_COMPANY_ENTITES)

    # 7 · manifeste = exactement le contrat (companies + equipe)
    def test_manifeste_fidele(self):
        m = self.bundle["manifest"]
        companies = sorted(
            {r["entite_principale"] for r in self.contract["roles"] if r["scope_donnees"] == "entite"}
        )
        self.assertEqual(m["companies_a_confirmer"], companies)
        equipe = sorted(
            r["erpnext_role_name"] for r in self.contract["roles"] if r["scope_donnees"] == "equipe"
        )
        self.assertEqual(m["roles_scope_equipe_a_confirmer"], equipe)
        self.assertEqual(sum(m["counts"]["by_mechanism"].values()), 50)

    # 8 · déterminisme bit-à-bit
    def test_deterministe(self):
        a = json.dumps(builder.build_plan(_load(_CONTRACT)), sort_keys=False)
        b = json.dumps(builder.build_plan(_load(_CONTRACT)), sort_keys=False)
        self.assertEqual(a, b, "Génération non déterministe")

    # 9 · garde-fous
    def test_scope_inconnu_rejete(self):
        with self.assertRaises(ValueError):
            frappe.mechanism_for("planetaire")

    def test_entite_consolidee_rejetee(self):
        with self.assertRaises(ValueError):
            frappe.user_permission_template("Groupe")

    # 10 · CLI validate de bout en bout
    def test_cli_validate_ok(self):
        cli = _load_cli()
        self.assertEqual(cli.main(["validate"]), 0)


if __name__ == "__main__":
    unittest.main(verbosity=2)
