"""Vue lisible d'un profil d'anonymisation (logique pure, sans display).

Sous-tend le sous-onglet « Profils » et la fenêtre « Tableau des termes ».
"""

from __future__ import annotations

import pytest

from gui_v6.profile_view import (
    filter_term_rows,
    profile_term_rows,
    summarize_profile,
)

_PROFILE = {
    "label": "Standard local",
    "description": "Profil par défaut.",
    "require_manual_mask": True,
    "force_disable_vlm": True,
    "preferred_manual_mask_template": "config/mask_templates/x.json",
    "param_lists": {
        "whitelist_phrases": ["classification internationale", "prise en charge"],
        "blacklist_force_mask_terms": ["CHUXX"],
        "additional_stopwords": [],
    },
}


def test_summarize_profile_reads_real_fields():
    s = summarize_profile("standard_local", _PROFILE)
    assert s.key == "standard_local"
    assert s.label == "Standard local"
    assert s.description == "Profil par défaut."
    assert s.require_manual_mask is True
    assert s.disable_vlm is True
    assert s.mask_template == "config/mask_templates/x.json"
    assert s.list_counts == {"À conserver": 2, "À masquer": 1, "À ignorer": 0}


def test_summarize_profile_tolerates_empty():
    s = summarize_profile("vide", {})
    assert s.label == "vide"
    assert s.description == ""
    assert s.require_manual_mask is False
    assert s.mask_template == ""
    assert s.list_counts == {"À conserver": 0, "À masquer": 0, "À ignorer": 0}

    s2 = summarize_profile("none", None)
    assert s2.list_counts["À masquer"] == 0


def test_profile_term_rows_type_term_source():
    rows = profile_term_rows(_PROFILE)
    assert ("À conserver", "classification internationale", "Standard local") in rows
    assert ("À masquer", "CHUXX", "Standard local") in rows
    # 2 whitelist + 1 blacklist + 0 stopwords
    assert len(rows) == 3


def test_filter_term_rows_by_query():
    rows = profile_term_rows(_PROFILE)
    assert len(filter_term_rows(rows, "")) == 3
    assert filter_term_rows(rows, "chuxx") == [("À masquer", "CHUXX", "Standard local")]
    assert filter_term_rows(rows, "conserver") == [
        r for r in rows if r[0] == "À conserver"
    ]
    assert filter_term_rows(rows, "zzz") == []


# --- Smokes headless (fenêtre tableau + infobulle) --------------------------

@pytest.fixture
def ctk_root():
    ctk = pytest.importorskip("customtkinter")
    try:
        root = ctk.CTk()
    except Exception as exc:
        pytest.skip(f"display Tk indisponible: {exc}")
    root.withdraw()
    try:
        yield root
    finally:
        try:
            root.destroy()
        except Exception:
            pass


def test_terms_table_window_filters_and_disables_add(ctk_root):
    from gui_v6 import theme as theme_mod
    from gui_v6.terms_table_window import TermsTableWindow

    p = theme_mod.get_palette(theme_mod.DEFAULT_THEME)
    win = TermsTableWindow(ctk_root, p, profile_term_rows(_PROFILE), profile_label="Standard local")
    ctk_root.update_idletasks()
    assert win.visible_count() == 3
    assert win.add_is_disabled()  # action non câblée → désactivée
    win.set_query("chuxx")
    assert win.visible_count() == 1
    win.set_query("")
    assert win.visible_count() == 3
    win.destroy()


def test_attach_tooltip_does_not_break_widget(ctk_root):
    import customtkinter as ctk

    from gui_v6 import ui_kit

    lbl = ctk.CTkLabel(ctk_root, text="x")
    lbl.pack()
    ctk_root.update_idletasks()
    tip = ui_kit.attach_tooltip(lbl, "aide contextuelle")
    tip.show()
    ctk_root.update_idletasks()
    tip.hide()
    assert lbl.winfo_exists()


def test_subtabs_include_editable_profils():
    """Retour Dom : sous-onglet Profils réintroduit (éditeur)."""
    from gui_v6.tabs.tab_config import _SUBTABS

    keys = [k for k, _ in _SUBTABS]
    labels = [lbl for _, lbl in _SUBTABS]
    assert "pro" in keys
    assert any("Profils" in lbl for lbl in labels)


def test_profils_editor_creates_and_persists(ctk_root, tmp_path, monkeypatch):
    """L'éditeur crée un profil, le rend éditable, et persiste les modifications."""
    from gui_v6.tabs import tab_config

    monkeypatch.setattr(tab_config, "_app_base_dir", lambda: tmp_path)
    profiles = tmp_path / "profiles.yml"
    tab = tab_config.ConfigTab(ctk_root)
    tab._profiles_path = profiles
    tab._show_sub("pro")
    tab.update_idletasks()

    # création d'un profil runtime
    tab._pro_new()
    tab.update_idletasks()
    key = tab._pro_edit_key
    assert key and key.startswith("nouveau_profil")

    # éditer : nom + un terme à masquer, puis enregistrer
    tab._pro_label_var.set("Profil cabinet")
    tab._pro_require_mask_var.set(True)
    tab._pro_term_lists["blacklist"].add_term("CHUXX")
    tab._pro_save()
    tab.update_idletasks()

    from profile_defaults import list_effective_profiles

    saved = list_effective_profiles(profiles)[key]
    assert saved["label"] == "Profil cabinet"
    assert saved["require_manual_mask"] is True
    assert saved["param_lists"]["blacklist_force_mask_terms"] == ["CHUXX"]
    tab.destroy()


def test_profils_default_profile_is_read_only(ctk_root, tmp_path, monkeypatch):
    """Un profil par défaut n'est pas éditable (bouton Enregistrer désactivé)."""
    from gui_v6.tabs import tab_config

    monkeypatch.setattr(tab_config, "_app_base_dir", lambda: tmp_path)
    tab = tab_config.ConfigTab(ctk_root)
    tab._profiles_path = tmp_path / "profiles.yml"
    tab._show_sub("pro")
    tab._pro_load("standard_local")  # profil défaut
    tab.update_idletasks()
    assert str(tab._pro_save_btn.cget("state")) == "disabled"
    tab.destroy()


def test_editable_term_list_add_remove(ctk_root):
    from gui_v6 import theme as theme_mod
    from gui_v6.editable_list import EditableTermList

    p = theme_mod.get_palette(theme_mod.DEFAULT_THEME)
    lst = EditableTermList(ctk_root, p, title="À masquer", initial=["A", "B"])
    ctk_root.update_idletasks()
    assert lst.terms() == ["A", "B"]
    assert lst.add_term("C") is True
    assert lst.add_term("C") is False  # pas de doublon
    lst.remove_term("A")
    assert lst.terms() == ["B", "C"]
    lst.set_editable(False)
    assert str(lst._add_btn.cget("state")) == "disabled"
    lst.destroy()


def _all_texts(widget):
    out = []
    try:
        out.append(str(widget.cget("text")))
    except Exception:
        pass
    for child in widget.winfo_children():
        out += _all_texts(child)
    return out


def test_reglages_labels_renamed_and_profile_readable(ctk_root, tmp_path, monkeypatch):
    from gui_v6.tabs import tab_config

    monkeypatch.setattr(tab_config, "_app_base_dir", lambda: tmp_path)
    tab = tab_config.ConfigTab(ctk_root)
    tab.update_idletasks()

    texts = " | ".join(_all_texts(tab))
    assert "Profil d'anonymisation" in texts  # addendum : renommage
    assert "Profil métier" not in texts
    assert "Dossier de sortie" in texts  # addendum : « Sortie… » clarifié

    # retour Dom : accès direct au tableau depuis Réglages, plus de pastilles inline
    assert "Ouvrir le tableau des termes" in texts
    assert "Voir le profil" not in texts
    assert "FUROSEMIDE" not in texts  # plus de pastilles de termes exemple inline

    # profil lisible : résumé avec les 3 listes
    summary = tab._active_profile_summary()
    assert set(summary.list_counts.keys()) == {"À conserver", "À masquer", "À ignorer"}

    # tableau des termes ouvrable DIRECTEMENT depuis Réglages (sans onglet Profils)
    tab._open_terms_table()
    tab.update_idletasks()
    tab.destroy()


def test_usage_tab_finish_calls_reporter(ctk_root):
    """Câblage : la fin de run appelle le reporter de télémétrie (non bloquant)."""
    import threading

    from gui_v6.processing_runner import RunSummary
    from gui_v6.tabs.tab_usage import UsageTab

    called = threading.Event()
    captured = {}

    def reporter(summary):
        captured["summary"] = summary
        called.set()

    tab = UsageTab(ctk_root, usage_reporter=reporter)
    ctk_root.update_idletasks()
    summary = RunSummary(total=1, succeeded=1)
    tab._finish(summary)
    assert called.wait(timeout=3.0)  # reporter appelé en thread daemon
    assert captured["summary"] is summary
    tab.destroy()


def test_masquage_moved_into_profils(ctk_root, tmp_path, monkeypatch):
    """Le sous-onglet Masquage est retiré ; son contenu utile est dans Profils."""
    from gui_v6.tabs import tab_config

    keys = [k for k, _ in tab_config._SUBTABS]
    assert "msk" not in keys  # plus de sous-onglet Masquage séparé

    monkeypatch.setattr(tab_config, "_app_base_dir", lambda: tmp_path)
    tab = tab_config.ConfigTab(ctk_root)
    tab._show_sub("pro")
    tab.update_idletasks()

    # apparence du masque relocalisée dans la section Profils > Masquage
    assert getattr(tab, "_swatch_buttons", None)
    # un template enregistré depuis l'éditeur remplit le champ Template du profil
    saved = tmp_path / "config" / "mask_templates" / "depuis_editeur.json"
    saved.parent.mkdir(parents=True, exist_ok=True)
    tab._on_mask_template_saved(saved)
    assert tab._pro_template_var.get().endswith("depuis_editeur.json")
    tab.destroy()


def test_regles_moved_into_profils(ctk_root, tmp_path, monkeypatch):
    """Retour Dom : le sous-onglet Règles séparé est retiré ; les règles du
    profil sont une section de Profils."""
    from gui_v6.tabs import tab_config

    keys = [k for k, _ in tab_config._SUBTABS]
    assert "rul" not in keys  # plus de sous-onglet Règles séparé

    monkeypatch.setattr(tab_config, "_app_base_dir", lambda: tmp_path)
    tab = tab_config.ConfigTab(ctk_root)
    tab._show_sub("pro")
    tab.update_idletasks()

    # la section des règles du profil est dans le panneau Profils
    texts = " | ".join(_all_texts(tab._panels["pro"]))
    assert "Règles d'anonymisation portées par ce profil" in texts
    assert "Masquer le sigle CHUXX" in texts  # table de règles relocalisée dans Profils
    # le builder du sous-onglet séparé n'existe plus
    assert "rul" not in tab._panels
    tab.destroy()


def test_unavailable_engines_disabled_in_reglages(ctk_root, tmp_path, monkeypatch):
    """Honnêteté moteurs : EDS-Pseudo / GLiNER non embarqués → switch désactivé
    et état forcé à False ; CamemBERT-bio reste actif."""
    import engine_capabilities as ec
    from gui_v6.tabs import tab_config

    monkeypatch.setattr(tab_config, "_app_base_dir", lambda: tmp_path)
    fake = {
        "camembert": ec.EngineCapability("camembert", "CamemBERT-bio (standard)", True, True, "ok"),
        "eds": ec.EngineCapability("eds", "EDS-Pseudo (optionnel)", False, False, "non embarqué dans cette version (manque : edsnlp, spacy)"),
        "gliner": ec.EngineCapability("gliner", "GLiNER (optionnel)", False, False, "non embarqué dans cette version (manque : gliner)"),
    }
    monkeypatch.setattr(tab_config.engine_capabilities, "capabilities_map", lambda probes=None: fake)

    tab = tab_config.ConfigTab(ctk_root)
    tab.update_idletasks()

    assert str(tab._tog_ner.switch.cget("state")) == "normal"  # CamemBERT standard actif
    assert str(tab._tog_eds.switch.cget("state")) == "disabled"
    assert str(tab._tog_gli.switch.cget("state")) == "disabled"
    assert tab._state.enable_eds is False
    assert tab._state.enable_gliner is False
    tab.destroy()


def test_available_engines_enabled_in_reglages(ctk_root, tmp_path, monkeypatch):
    """Si les moteurs optionnels sont embarqués, leurs switches restent actifs."""
    import engine_capabilities as ec
    from gui_v6.tabs import tab_config

    monkeypatch.setattr(tab_config, "_app_base_dir", lambda: tmp_path)
    fake = {
        "camembert": ec.EngineCapability("camembert", "CamemBERT-bio (standard)", True, True, "ok"),
        "eds": ec.EngineCapability("eds", "EDS-Pseudo (optionnel)", True, False, "edsnlp + spacy disponibles"),
        "gliner": ec.EngineCapability("gliner", "GLiNER (optionnel)", True, False, "gliner disponible"),
    }
    monkeypatch.setattr(tab_config.engine_capabilities, "capabilities_map", lambda probes=None: fake)

    tab = tab_config.ConfigTab(ctk_root)
    tab.update_idletasks()
    assert str(tab._tog_eds.switch.cget("state")) == "normal"
    assert str(tab._tog_gli.switch.cget("state")) == "normal"
    tab.destroy()
