"""Tests du générateur de Role Profile (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 bundle valide contre roleprofile.schema.json (maison + oracle).
  2. Couverture BIJECTIVE : chaque rôle du contrat dans exactement un profil.
  3. Un profil par portail distinct du contrat ; nom = convention déterministe.
  4. Cohérence portail : tous les rôles d'un profil partagent le même portail.
  5. Manifeste fidèle : portails, flag métier (vs plateforme), comptes par profil.
  6. Anti-invention : aucun rôle hors contrat, aucun DocType custom (que du natif v15).
  7. Déterminisme : deux builds successifs bit-à-bit identiques + tris stables.
  8. Garde-fous : portail vide et profil vide → ValueError.
  9. 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, ".."))            # roleprofile_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 profilelib 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, "roleprofile.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(
        "roleprofile_gen", os.path.join(_GEN, "roleprofile_gen.py")
    )
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


class RoleProfileGenTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.contract = _load(_CONTRACT)
        cls.schema = _load(_SCHEMA)
        cls.bundle = builder.build_bundle(cls.contract)
        cls.profiles = cls.bundle["role_profile"]
        cls.portail_of_role = {
            r["erpnext_role_name"]: r["portail"] for r in cls.contract["roles"]
        }

    # 1 · conformité au schéma (validateur maison)
    def test_bundle_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_bundle_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_bijective_50_roles(self):
        assigned = [row["role"] for p in self.profiles for row in p["roles"]]
        self.assertEqual(len(assigned), 50)
        self.assertEqual(len(assigned), len(set(assigned)), "Rôle dans plusieurs profils")
        self.assertEqual(set(assigned), set(self.portail_of_role), "Couverture ≠ contrat")

    # 3 · un profil par portail distinct, nom = convention déterministe
    def test_un_profil_par_portail(self):
        portails = {r["portail"] for r in self.contract["roles"]}
        noms = {p["name"] for p in self.profiles}
        self.assertEqual(len(self.profiles), len(portails))
        self.assertEqual(
            noms, {frappe.role_profile_name(p) for p in portails}
        )
        for p in self.profiles:
            self.assertEqual(p["name"], p["role_profile"])
            self.assertTrue(p["name"].startswith("OTO Portail "))

    # 4 · cohérence portail : un profil ne mélange pas les portails
    def test_coherence_portail(self):
        for p in self.profiles:
            portails = {self.portail_of_role[row["role"]] for row in p["roles"]}
            self.assertEqual(len(portails), 1, f"{p['name']} mélange {portails}")
            portail = portails.pop()
            self.assertEqual(p["name"], frappe.role_profile_name(portail))

    # 5 · manifeste fidèle (portails, flag métier, comptes)
    def test_manifeste_fidele(self):
        m = self.bundle["manifest"]
        portails = {r["portail"] for r in self.contract["roles"]}
        metier = set(self.contract["portails_business"])
        self.assertEqual({x["portail"] for x in m["profiles"]}, portails)
        nb_reel = {}
        for p in self.profiles:
            portail = self.portail_of_role[p["roles"][0]["role"]]
            nb_reel[portail] = len(p["roles"])
        for x in m["profiles"]:
            self.assertEqual(x["metier"], x["portail"] in metier)
            self.assertEqual(x["nb_roles"], nb_reel[x["portail"]])
            self.assertEqual(x["role_profile"], frappe.role_profile_name(x["portail"]))
        self.assertEqual(m["counts"]["roles_couverts"], 50)
        self.assertEqual(m["counts"]["role_profiles"], len(self.profiles))
        self.assertEqual(m["counts"]["portails_metier"], len(metier & portails))

    # 6 · anti-invention : que du natif v15, aucun rôle hors contrat
    def test_anti_invention(self):
        for p in self.profiles:
            self.assertEqual(p["doctype"], "Role Profile")
            for row in p["roles"]:
                self.assertEqual(row["doctype"], "Has Role")
                self.assertIn(row["role"], self.portail_of_role, "Rôle hors contrat")

    # 7 · déterminisme + tris stables
    def test_deterministe(self):
        a = json.dumps(builder.build_bundle(_load(_CONTRACT)))
        b = json.dumps(builder.build_bundle(_load(_CONTRACT)))
        self.assertEqual(a, b, "Génération non déterministe")
        portails_seq = [x["portail"] for x in self.bundle["manifest"]["profiles"]]
        self.assertEqual(portails_seq, sorted(portails_seq), "profils non triés")
        for p in self.profiles:
            roles_seq = [row["role"] for row in p["roles"]]
            self.assertEqual(roles_seq, sorted(roles_seq), f"{p['name']}: rôles non triés")

    # 8 · garde-fous
    def test_portail_vide_rejete(self):
        with self.assertRaises(ValueError):
            frappe.role_profile_name("")

    def test_profil_vide_rejete(self):
        with self.assertRaises(ValueError):
            frappe.role_profile_fixture("ventes", [])

    # 9 · 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)
