"""Tests du générateur de config app mobile (Sprint 5 · Mobile).

Stdlib pur (unittest) — aucune dépendance pip. Couvre :
  - conformité au schéma + les 15 invariants du CLI (chemin nominal) ;
  - la cross-cohérence RBAC ↔ portails ↔ seo (onglets, rôles, labels/icônes, langues) ;
  - la fiche store dérivée (langues seo, limites de caractères, portails/langues
    injectés depuis la surface réelle) ;
  - l'anti-invention (#6) par injections négatives (identifiant de store fabriqué,
    rôles désynchronisés de la surface RBAC, langue inventée, token de marque altéré,
    description store dépassant la limite ou désalignée des portails réels).
"""

from __future__ import annotations

import copy
import json
import os
import sys
import unittest

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

import app_config_gen as gen  # noqa: E402
from mobilelib import builder, deps  # noqa: E402


def build():
    return gen._build_bundle()


class TestNominal(unittest.TestCase):
    def setUp(self):
        self.bundle = build()

    def test_build_valide(self):
        self.assertEqual(gen._validate_bundle(self.bundle), [])

    def test_schema_seul(self):
        schema = deps.load_json(deps.SCHEMA_PATH)
        self.assertEqual(deps.validate(self.bundle, schema), [])

    def test_onglets_portails_metier_dans_ordre(self):
        contract = deps.load_contract()
        spec = deps.load_spec()
        keys = [n["portail"] for n in self.bundle["role_navigation"]]
        self.assertEqual(keys, spec["portail_order"])
        self.assertEqual(set(keys), set(contract["portails_business"]))

    def test_roles_allowed_egal_surface_rbac(self):
        """Ancrage sécurité : un onglet ne cible QUE les rôles RBAC de son portail."""
        contract = deps.load_contract()
        roles_map = deps.roles_by_portail(contract)
        for n in self.bundle["role_navigation"]:
            self.assertEqual(n["roles_allowed"], sorted(roles_map[n["portail"]]))
            self.assertEqual(n["n_roles"], len(n["roles_allowed"]))
            self.assertTrue(n["roles_allowed"], f"{n['portail']} sans rôle")

    def test_labels_icones_depuis_portails_spec(self):
        pspec = deps.load_portails_spec()
        labels = {p["key"]: p["label"] for p in pspec["portails"]}
        icons = {p["key"]: p["icon"] for p in pspec["portails"]}
        for n in self.bundle["role_navigation"]:
            self.assertEqual(n["label"], labels[n["portail"]])
            self.assertEqual(n["icon"], icons[n["portail"]])

    def test_langues_depuis_seo(self):
        langs, default_lang = deps.load_langs()
        app = self.bundle["app_config"]["expo"]
        self.assertEqual(sorted(app["locales"].keys()), sorted(langs))
        self.assertEqual(app["extra"]["defaultLocale"], default_lang)
        self.assertEqual(self.bundle["manifest"]["langues"]["liste"], list(langs))

    def test_tokens_marque_4(self):
        app = self.bundle["app_config"]["expo"]
        self.assertEqual(app["backgroundColor"], deps.branding.COLOR_BG)
        self.assertEqual(app["splash"]["backgroundColor"], deps.branding.COLOR_BG)
        self.assertEqual(app["primaryColor"], deps.branding.COLOR_ACCENT)
        self.assertEqual(app["userInterfaceStyle"], "dark")

    def test_devises_usd_dop(self):
        app = self.bundle["app_config"]["expo"]
        self.assertEqual(
            app["extra"]["devises"],
            [deps.branding.DEVISE_PRIMAIRE, deps.branding.DEVISE_SECONDAIRE],
        )

    def test_identifiants_store_null(self):
        """Anti-invention : chaque champ a_confirmer reste null (jamais fabriqué)."""
        spec = deps.load_spec()
        for entry in spec["a_confirmer"]:
            found, value = gen._resolve_path(self.bundle, entry["champ"])
            self.assertTrue(found, f"champ absent: {entry['champ']}")
            self.assertIsNone(value, f"{entry['champ']} devrait être null")
        self.assertEqual(self.bundle["manifest"]["identifiants_statut"], "a_confirmer")

    def test_expo_sdk_54(self):
        spec = deps.load_spec()
        self.assertEqual(
            self.bundle["app_config"]["expo"]["extra"]["expoSdkMajor"],
            spec["app"]["expo_sdk_major"],
        )

    def test_eas_profils_et_submit_null(self):
        spec = deps.load_spec()
        eas = self.bundle["eas_build"]
        self.assertEqual(sorted(eas["build"].keys()), sorted(spec["eas"]["build_profiles"]))
        sub = eas["submit"]["production"]
        self.assertIsNone(sub["ios"]["appleId"])
        self.assertIsNone(sub["android"]["serviceAccountKeyPath"])

    def test_determinisme(self):
        a = json.dumps(build(), sort_keys=True, ensure_ascii=False)
        b = json.dumps(build(), sort_keys=True, ensure_ascii=False)
        self.assertEqual(a, b)


class TestStoreListing(unittest.TestCase):
    """Fiche store : couverture langues, limites de caractères, injection sourcée."""

    def setUp(self):
        self.bundle = build()
        self.sl = self.bundle["store_listing"]

    def test_langues_egal_seo(self):
        langs, _ = deps.load_langs()
        self.assertEqual(sorted(self.sl["content"].keys()), sorted(langs))
        self.assertEqual(self.sl["langues"], list(langs))

    def test_limites_caracteres_store(self):
        spec = deps.load_spec()
        limits = spec["store_listing"]["limits"]
        for lang, fields in self.sl["content"].items():
            for champ, limit in limits.items():
                self.assertTrue(fields.get(champ), f"{lang}.{champ} vide")
                self.assertLessEqual(len(fields[champ]), limit, f"{lang}.{champ} > {limit}")

    def test_portails_et_langues_injectes(self):
        """Anti-dérive : la copie cite les portails/langues RÉELS, pas une liste figée."""
        langs, _ = deps.load_langs()
        nav = self.bundle["role_navigation"]
        portails_str = " · ".join(n["label"] for n in nav)
        langues_str = "/".join(l.upper() for l in langs)
        for lang, fields in self.sl["content"].items():
            self.assertIn(portails_str, fields["full_description"])
            self.assertIn(langues_str, fields["full_description"])
            self.assertNotIn("{", fields["full_description"])


class TestAntiInvention(unittest.TestCase):
    """Injections négatives : chaque garde-fou doit MORDRE (invariant → erreur)."""

    def setUp(self):
        self.bundle = build()

    def test_identifiant_fabrique_detecte(self):
        bad = copy.deepcopy(self.bundle)
        bad["app_config"]["expo"]["ios"]["bundleIdentifier"] = "com.oto.inventé"
        errs = gen._validate_bundle(bad)
        self.assertTrue(any("null" in e for e in errs), errs)

    def test_roles_desync_detecte(self):
        bad = copy.deepcopy(self.bundle)
        bad["role_navigation"][0]["roles_allowed"] = ["OTO Rôle Inventé"]
        errs = gen._validate_bundle(bad)
        self.assertTrue(any("roles_allowed" in e for e in errs), errs)

    def test_langue_inventee_detecte(self):
        bad = copy.deepcopy(self.bundle)
        bad["app_config"]["expo"]["locales"]["de"] = None
        errs = gen._validate_bundle(bad)
        self.assertTrue(any("locales" in e for e in errs), errs)

    def test_token_marque_altere_detecte(self):
        bad = copy.deepcopy(self.bundle)
        bad["app_config"]["expo"]["primaryColor"] = "#ffffff"
        errs = gen._validate_bundle(bad)
        self.assertTrue(any("primaryColor" in e for e in errs), errs)

    def test_ordre_onglets_altere_detecte(self):
        bad = copy.deepcopy(self.bundle)
        bad["role_navigation"].reverse()
        errs = gen._validate_bundle(bad)
        self.assertTrue(any("ordre des onglets" in e for e in errs), errs)

    def test_fiche_store_trop_longue_detecte(self):
        bad = copy.deepcopy(self.bundle)
        bad["store_listing"]["content"]["fr"]["subtitle"] = "x" * 200
        errs = gen._validate_bundle(bad)
        self.assertTrue(any("limite store" in e for e in errs), errs)

    def test_fiche_store_portails_desalignes_detecte(self):
        bad = copy.deepcopy(self.bundle)
        bad["store_listing"]["content"]["en"]["full_description"] = "No portals listed here."
        errs = gen._validate_bundle(bad)
        self.assertTrue(any("full_description" in e for e in errs), errs)


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