#!/usr/bin/env python3
"""Tests du générateur d'audit 5D de conformité (QA · Sprint 5).

Stdlib pur (`unittest`) → aucune installation pip requise sur le runner Gitea
(CLAUDE.md #2). Couvre : chargement des artefacts, chaque contrôle (positif +
injection négative sur copie profonde de l'artefact), synthèse par dimension,
verdict, open items, invariants du générateur, déterminisme et schéma.

Axe central : l'audit LIT les hand-off `out/` des livrables et remonte les
paramètres réglementaires non confirmés en A_CONFIRMER — jamais en valeur
fabriquée (#6). Un FAIL ne doit jamais survenir sur les livrables réels.
"""

from __future__ import annotations

import copy
import json
import os
import sys
import unittest

_HERE = os.path.dirname(os.path.abspath(__file__))
_MOD = os.path.normpath(os.path.join(_HERE, ".."))
sys.path.insert(0, _MOD)

from qalib import artifacts, builder, controls  # noqa: E402
import audit_5d_gen as gen  # noqa: E402

PASS, FAIL, AC = controls.PASS, controls.FAIL, controls.A_CONFIRMER
_FN = {c["id"]: c["fn"] for c in controls.CONTROLS}


def _spec():
    with open(os.path.join(_MOD, "audit_spec.json"), encoding="utf-8") as fh:
        return json.load(fh)


def _art():
    return artifacts.load_artifacts()


def _run(cid, art, spec):
    return _FN[cid](art, spec)


class ArtifactsTest(unittest.TestCase):
    def test_load_all_present(self):
        art = _art()
        self.assertEqual(set(art), artifacts.known_ids())
        self.assertEqual(art["workflow"]["doctype"], "Workflow")  # liste dépliée

    def test_missing_artifact_raises(self):
        orig = artifacts.ARTIFACT_PATHS.copy()
        artifacts.ARTIFACT_PATHS["ecf"] = os.path.join("fiscal", "n_existe_pas.json")
        try:
            self.assertRaises(FileNotFoundError, artifacts.load_artifacts)
        finally:
            artifacts.ARTIFACT_PATHS.clear()
            artifacts.ARTIFACT_PATHS.update(orig)


class ControlsHappyPathTest(unittest.TestCase):
    def test_no_fail_on_real_artifacts(self):
        art, spec = _art(), _spec()
        for c in controls.CONTROLS:
            statut, detail = c["fn"](art, spec)
            self.assertIn(statut, (PASS, AC), f"{c['id']} inattendu {statut}")
            self.assertTrue(detail, f"{c['id']} sans détail")

    def test_open_items_are_the_four_regulatory_params(self):
        art, spec = _art(), _spec()
        ac = {c["id"] for c in controls.CONTROLS if c["fn"](art, spec)[0] == AC}
        self.assertEqual(ac, {"D1.1", "D1.2", "D1.3", "D2.3"})


class D1TracabiliteTest(unittest.TestCase):
    def test_d1_1_fabricated_rate_fails(self):
        art, spec = copy.deepcopy(_art()), _spec()
        art["commissions"]["evenements"][0]["taux_pct"] = 5.0
        art["commissions"]["evenements"][0]["a_confirmer"] = False
        self.assertEqual(_run("D1.1", art, spec)[0], FAIL)

    def test_d1_1_rate_with_source_passes(self):
        art, spec = copy.deepcopy(_art()), _spec()
        for e in art["commissions"]["evenements"]:
            e["taux_pct"], e["source"], e["a_confirmer"] = 3.0, "barème Direction 2026", False
        self.assertEqual(_run("D1.1", art, spec)[0], PASS)

    def test_d1_2_fabricated_rnc_fails(self):
        art, spec = copy.deepcopy(_art()), _spec()
        art["ecf"]["emisor"]["rnc_emisor"] = "131000000"
        self.assertEqual(_run("D1.2", art, spec)[0], FAIL)

    def test_d1_3_fabricated_itbis_fails(self):
        art, spec = copy.deepcopy(_art()), _spec()
        art["ecf"]["taxes"][0]["taux_pct"] = 18.0
        self.assertEqual(_run("D1.3", art, spec)[0], FAIL)

    def test_d1_4_default_on_data_field_fails(self):
        art, spec = copy.deepcopy(_art()), _spec()
        for f in art["confotur"]["fields"]:
            if f["fieldname"] == "referencia_autoridad":
                f["default"] = "RES-CONFOTUR-0000"
        self.assertEqual(_run("D1.4", art, spec)[0], FAIL)

    def test_d1_4_passes_on_real(self):
        self.assertEqual(_run("D1.4", _art(), _spec())[0], PASS)


class D2AmlUafTest(unittest.TestCase):
    def test_d2_1_missing_client_fails(self):
        art, spec = copy.deepcopy(_art()), _spec()
        art["dossier"]["fields"] = [f for f in art["dossier"]["fields"]
                                    if f["fieldname"] != "client"]
        self.assertEqual(_run("D2.1", art, spec)[0], FAIL)

    def test_d2_2_missing_identity_piece_fails(self):
        art, spec = copy.deepcopy(_art()), _spec()
        art["confotur"]["field_order"] = [x for x in art["confotur"]["field_order"]
                                          if x != "piece_identidad_cliente"]
        self.assertEqual(_run("D2.2", art, spec)[0], FAIL)

    def test_d2_3_threshold_is_open_item(self):
        self.assertEqual(_run("D2.3", _art(), _spec())[0], AC)

    def test_d2_3_fabricated_threshold_fails(self):
        spec = _spec()
        spec["uaf"]["seuil_operacion"] = 1000000
        self.assertEqual(_run("D2.3", _art(), spec)[0], FAIL)


class D3FiscalTest(unittest.TestCase):
    def test_d3_1_devise_mismatch_fails(self):
        art, spec = copy.deepcopy(_art()), _spec()
        art["ecf"]["devise_field"] = "champ_inexistant"
        self.assertEqual(_run("D3.1", art, spec)[0], FAIL)

    def test_d3_2_emission_on_draft_fails(self):
        art, spec = copy.deepcopy(_art()), _spec()
        for s in art["workflow"]["states"]:
            if s["update_value"] == "contrat":
                s["doc_status"] = "0"
        self.assertEqual(_run("D3.2", art, spec)[0], FAIL)

    def test_d3_3_wrong_formapago_fails(self):
        art, spec = copy.deepcopy(_art()), _spec()
        art["ecf"]["forma_pago_defaut"]["code"] = "1"
        self.assertEqual(_run("D3.3", art, spec)[0], FAIL)


class D4IntegriteTest(unittest.TestCase):
    def test_d4_1_states_diverge_fails(self):
        art, spec = copy.deepcopy(_art()), _spec()
        for f in art["dossier"]["fields"]:
            if f["fieldname"] == "workflow_state":
                f["options"] = "Lead\nÉtat fantôme"
        self.assertEqual(_run("D4.1", art, spec)[0], FAIL)

    def test_d4_2_base_not_currency_fails(self):
        art, spec = copy.deepcopy(_art()), _spec()
        art["commissions"]["evenements"][0]["base_field"] = "prospect"
        self.assertEqual(_run("D4.2", art, spec)[0], FAIL)

    def test_d4_3_link_wrong_target_fails(self):
        art, spec = copy.deepcopy(_art()), _spec()
        for f in art["confotur"]["fields"]:
            if f["fieldname"] == "dossier_vente":
                f["options"] = "Autre DocType"
        self.assertEqual(_run("D4.3", art, spec)[0], FAIL)

    def test_d4_4_unknown_update_value_fails(self):
        art, spec = copy.deepcopy(_art()), _spec()
        art["commissions"]["evenements"][0]["update_value"] = "etat_inconnu"
        self.assertEqual(_run("D4.4", art, spec)[0], FAIL)


class D5GouvernanceTest(unittest.TestCase):
    def test_d5_1_unknown_role_fails(self):
        art, spec = copy.deepcopy(_art()), _spec()
        art["commissions"]["evenements"][0]["role_id"] = "role-fantome-999"
        self.assertEqual(_run("D5.1", art, spec)[0], FAIL)

    def test_d5_1_passes_on_real(self):
        self.assertEqual(_run("D5.1", _art(), _spec())[0], PASS)

    def test_d5_2_segregation_breach_fails(self):
        art, spec = copy.deepcopy(_art()), _spec()
        for ev in art["ecf"]["emission_events"]:
            ev["role_id"] = "ventes-reservations"
        self.assertEqual(_run("D5.2", art, spec)[0], FAIL)

    def test_d5_3_perms_mismatch_fails(self):
        art, spec = copy.deepcopy(_art()), _spec()
        art["confotur"]["permissions"][0]["submit"] = 0
        self.assertEqual(_run("D5.3", art, spec)[0], FAIL)

    def test_d5_3_passes_on_real(self):
        self.assertEqual(_run("D5.3", _art(), _spec())[0], PASS)


class BuilderTest(unittest.TestCase):
    def test_verdict_pass_with_open_items(self):
        r = builder.build(_spec())
        self.assertEqual(r["verdict"], "PASS_WITH_OPEN_ITEMS")
        self.assertEqual(r["totals"], {"controls": 17, "pass": 13, "fail": 0, "a_confirmer": 4})

    def test_deterministic(self):
        self.assertEqual(builder.build(_spec()), builder.build(_spec()))

    def test_open_items_match_a_confirmer(self):
        r = builder.build(_spec())
        ac = {c["id"] for c in r["controls"] if c["statut"] == AC}
        oi = {o["control"] for o in r["open_items"]}
        self.assertEqual(ac, oi)
        self.assertTrue(all(o["owner"] for o in r["open_items"]))

    def test_dimensions_cover_all_controls(self):
        r = builder.build(_spec())
        self.assertEqual(sum(d["controls_total"] for d in r["dimensions"]), 17)
        self.assertEqual(len(r["dimensions"]), 5)

    def test_controls_sorted(self):
        ids = [c["id"] for c in builder.build(_spec())["controls"]]
        self.assertEqual(ids, sorted(ids))


class GeneratorTest(unittest.TestCase):
    def test_validate_clean(self):
        spec = _spec()
        self.assertEqual(gen._validate(spec, builder.build(spec)), [])

    def test_spec_code_ids_match(self):
        self.assertEqual(sorted(c["id"] for c in _spec()["controls"]),
                         controls.control_ids())

    def test_report_validates_schema(self):
        from qalib.deps import validate as maison
        with open(os.path.join(_MOD, "audit.schema.json"), encoding="utf-8") as fh:
            schema = json.load(fh)
        self.assertEqual(list(maison(builder.build(_spec()), schema)), [])

    def test_validate_detects_injected_fail(self):
        spec = _spec()
        r = copy.deepcopy(builder.build(spec))
        r["controls"][0]["statut"] = FAIL
        self.assertTrue(any("FAIL" in e for e in gen._validate(spec, r)))

    def test_validate_detects_id_drift(self):
        spec = _spec()
        r = builder.build(spec)
        spec2 = copy.deepcopy(spec)
        spec2["controls"].pop()
        self.assertNotEqual(gen._validate(spec2, r), [])

    def test_cli_validate_returns_zero(self):
        self.assertEqual(gen.main(["validate"]), 0)


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