"""Tests du générateur de dossier bancable trilingue (stdlib `unittest`, zéro pip).

Couvre : rendu FR/EN/ES, bannière CONFIDENTIEL, anti-invention (#6 — placeholders,
zéro chiffre fabriqué), calculs traçables recoupés indépendamment, cohérence du
manifeste avec le schéma (validateur maison + oracle `jsonschema` si présent),
publiabilité, déterminisme, et refus d'écrire sur invariant cassé.
"""

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

_HERE = os.path.dirname(os.path.abspath(__file__))
_MOD = os.path.normpath(os.path.join(_HERE, ".."))            # bancable/
_PUB = os.path.normpath(os.path.join(_MOD, "..", "..", "publiciste"))

sys.path.insert(0, _PUB)
sys.path.insert(0, _MOD)

from banclib import finance, i18n, report  # noqa: E402


def _load_cli():
    """Charge bancable_gen.py comme module isolé."""
    spec = importlib.util.spec_from_file_location(
        "bancable_gen", os.path.join(_MOD, "bancable_gen.py")
    )
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


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


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


class TestFinance(unittest.TestCase):
    def test_sourced_verbatim(self):
        src = finance.sourced(_brief())
        self.assertEqual(src["cout_construction_usd"], 9000000)
        self.assertEqual(src["revenu_brut_usd"], 14200000)
        self.assertEqual(src["marge_pct"], 28)
        self.assertEqual(src["taux_conversion"]["taux"], 59.0)

    def test_derived_arithmetic_traceable(self):
        figs = {f["cle"]: f for f in finance.derived(_brief())}
        # 12 + 20 + 8 = 40
        self.assertEqual(figs["total_unites"]["valeur"], 40)
        # 12*150000 + 20*210000 + 8*320000 = 1.8M + 4.2M + 2.56M = 8.56M
        self.assertEqual(figs["valeur_catalogue_usd"]["valeur"], 8560000)
        # 12*8.85M + 20*12.39M + 8*18.88M = 106.2M + 247.8M + 151.04M = 505.04M
        self.assertEqual(figs["valeur_catalogue_dop"]["valeur"], 505040000)
        # ceil(0.52 * 40) = ceil(20.8) = 21
        self.assertEqual(figs["point_equilibre_unites"]["valeur"], 21)

    def test_derived_carry_formula(self):
        for f in finance.derived(_brief()):
            self.assertTrue(f["formule"])
            self.assertNotIn("None", f["formule"])

    def test_missing_operand_yields_none_not_zero(self):
        b = _brief()
        del b["ingenierie"]["cout_construction_usd"]
        b["architecture"]["typologies"][0].pop("prix_usd")
        src = finance.sourced(b)
        self.assertIsNone(src["cout_construction_usd"])  # jamais 0
        figs = {f["cle"]: f for f in finance.derived(b)}
        # un prix_usd manquant ⇒ valeur catalogue USD non calculable
        self.assertIsNone(figs["valeur_catalogue_usd"]["valeur"])
        # mais les quantités restent complètes ⇒ total_unites calculable
        self.assertEqual(figs["total_unites"]["valeur"], 40)
        self.assertIn("ingenierie.cout_construction_usd",
                      finance.missing_fields(b))


class TestReport(unittest.TestCase):
    def test_three_languages_rendered(self):
        files = report.render_all(_brief())
        for lang in ("fr", "en", "es"):
            self.assertIn(f"50_financier_bancable/{lang}.md", files)

    def test_confidential_banner_every_language(self):
        for lang in i18n.LANGS:
            md = report.render_report(_brief(), lang)
            up = md.upper()
            self.assertTrue("CONFIDENTIEL" in up or "CONFIDENTIAL" in up
                            or "CONFIDENCIAL" in up)

    def test_synthetic_banner_and_not_publishable(self):
        b = _brief()
        self.assertTrue(b["synthetique"])
        m = report.build_manifest(b)
        self.assertFalse(m["publiable"])
        for lang in i18n.LANGS:
            self.assertIn("SYNTH", report.render_report(b, lang).upper())

    def test_positioning_per_language_from_brief(self):
        b = _brief()
        self.assertIn("beachfront", report.render_report(b, "en"))
        self.assertIn("balnearia", report.render_report(b, "es"))
        self.assertIn("balnéaire", report.render_report(b, "fr"))

    def test_missing_positioning_becomes_placeholder(self):
        b = _brief()
        del b["commercial"]["positionnement_en"]
        md = report.render_report(b, "en")
        self.assertIn("{{positionnement_en}}", md)
        # les autres langues restent intactes
        self.assertNotIn("{{positionnement_fr}}", report.render_report(b, "fr"))

    def test_no_python_none_leaks_in_render(self):
        b = _brief()
        del b["ingenierie"]["cout_construction_usd"]
        for lang in i18n.LANGS:
            md = report.render_report(b, lang)
            self.assertNotIn(" None ", md)
            self.assertNotIn("| None ", md)

    def test_canonical_params_present(self):
        md = report.render_report(_brief(), "fr")
        for marker in ("3 %", "8.5 %", "52 %", "USD + DOP", "Cardnet", "Letter US"):
            self.assertIn(marker, md)

    def test_computed_subtotal_traced_in_table(self):
        md = report.render_report(_brief(), "fr")
        # sous-total Studio = 12 × 150,000 = 1,800,000
        self.assertIn("USD 1,800,000", md)

    def test_determinism(self):
        b = _brief()
        self.assertEqual(report.render_all(b), report.render_all(b))


class TestManifestSchema(unittest.TestCase):
    def test_manifest_matches_home_validator(self):
        from lib import validator as pub_validator  # noqa: PLC0415
        m = report.build_manifest(_brief())
        errs = pub_validator.validate(m, _schema())
        self.assertEqual(errs, [], f"erreurs schéma maison : {errs}")

    def test_manifest_matches_jsonschema_oracle_if_present(self):
        try:
            import jsonschema  # noqa: PLC0415
        except ImportError:
            self.skipTest("jsonschema non installé (oracle optionnel)")
        jsonschema.validate(report.build_manifest(_brief()), _schema())

    def test_manifest_figures_count(self):
        m = report.build_manifest(_brief())
        self.assertEqual(len(m["figures_calculees"]), 4)
        self.assertEqual(m["langues"], ["fr", "en", "es"])
        self.assertEqual(m["typologies_count"], 3)

    def test_generated_at_optional(self):
        m0 = report.build_manifest(_brief())
        self.assertNotIn("generated_at", m0)
        m1 = report.build_manifest(_brief(), generated_at="2026-07-30T04:57:01Z")
        self.assertEqual(m1["generated_at"], "2026-07-30T04:57:01Z")
        from lib import validator as pub_validator  # noqa: PLC0415
        self.assertEqual(pub_validator.validate(m1, _schema()), [])


class TestCLIInvariants(unittest.TestCase):
    def setUp(self):
        self.cli = _load_cli()

    def test_invariants_pass_on_fixture(self):
        b = _brief()
        files = report.render_all(b)
        manifest = json.loads(files["50_financier_bancable/manifest.json"])
        self.assertEqual(self.cli.check_invariants(b, manifest, files), [])

    def test_invariant_detects_forged_figure(self):
        b = _brief()
        files = report.render_all(b)
        manifest = json.loads(files["50_financier_bancable/manifest.json"])
        # Falsifie une valeur calculée → l'invariant de recalcul doit la rattraper.
        for f in manifest["figures_calculees"]:
            if f["cle"] == "valeur_catalogue_usd":
                f["valeur"] = 9999999
        motifs = self.cli.check_invariants(b, manifest, files)
        self.assertTrue(any("valeur_catalogue_usd" in m for m in motifs))

    def test_invariant_detects_synthetic_marked_publishable(self):
        b = _brief()
        files = report.render_all(b)
        manifest = json.loads(files["50_financier_bancable/manifest.json"])
        manifest["publiable"] = True  # incohérent avec synthétique
        motifs = self.cli.check_invariants(b, manifest, files)
        self.assertTrue(any("synth" in m.lower() for m in motifs))

    def test_build_writes_files(self):
        import tempfile
        self.cli = _load_cli()
        with tempfile.TemporaryDirectory() as tmp:
            rc = self.cli.main(["build", os.path.join(_MOD, "fixtures",
                                                       "brief_bancable.json"),
                                "-o", tmp])
            self.assertEqual(rc, 0)
            base = os.path.join(tmp, "P01", "50_financier_bancable")
            for name in ("fr.md", "en.md", "es.md", "manifest.json"):
                self.assertTrue(os.path.exists(os.path.join(base, name)), name)

    def test_publishable_real_brief(self):
        # Un brief NON synthétique et complet est publiable.
        b = _brief()
        b["synthetique"] = False
        b["sources"] = ["archive réelle P01"]
        m = report.build_manifest(b)
        self.assertTrue(m["publiable"])


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