import json
import os

import pytest

from machine_backup.modules import shell as shell_module
from machine_backup.modules.base import ActionState, ModuleStatus
from machine_backup.modules.shell import ShellModule

from conftest import make_runner


def _fake_home(tmp_path, with_zsh=True, with_fish=True):
    home = tmp_path / "home"
    home.mkdir(parents=True, exist_ok=True)
    if with_fish:
        fish = home / ".config" / "fish"
        (fish / "functions").mkdir(parents=True)
        (fish / "config.fish").write_text("set -g theme x")
        (fish / "fish_variables").write_text("SETUVAR secret:abc")
        # Residus laisses par des sauvegardes manuelles : a ne pas embarquer.
        (fish / "config.fish.backup").write_text("vieux")
        (fish / "config.fish.bak-2026-07-06").write_text("vieux")
        (fish / "functions" / "ll.fish").write_text("function ll; end")
    if with_zsh:
        (home / ".zshrc").write_text("export EDITOR=vim")
        omz = home / ".oh-my-zsh" / "custom"
        omz.mkdir(parents=True)
        (omz / "mine.zsh").write_text("alias g=git")
    return home


def _collect(tmp_path, home, dest_name="shell", **kw):
    mod = ShellModule(home=home, runner=make_runner(), **kw)
    dest = tmp_path / dest_name
    result = mod.collect(dest)
    return mod, dest, result


def test_collect_excludes_residues(tmp_path):
    home = _fake_home(tmp_path)
    _, dest, _ = _collect(tmp_path, home)

    assert (dest / "fish" / "config.fish").exists()
    assert (dest / "fish" / "functions" / "ll.fish").exists()
    assert not (dest / "fish" / "config.fish.backup").exists()
    assert not (dest / "fish" / "config.fish.bak-2026-07-06").exists()


def test_collect_marks_fish_variables_sensitive(tmp_path):
    home = _fake_home(tmp_path)
    _, _, result = _collect(tmp_path, home)
    sensitive = {a.path for a in result.artifacts if a.sensitive}
    assert any(p.endswith("fish_variables") for p in sensitive)
    assert not any(p.endswith("config.fish") for p in sensitive)


def test_collect_captures_zsh_and_omz_custom(tmp_path):
    home = _fake_home(tmp_path)
    _, dest, _ = _collect(tmp_path, home)
    assert (dest / "zsh" / ".zshrc").exists()
    assert (dest / "zsh" / "oh-my-zsh-custom" / "mine.zsh").exists()


def test_collect_records_login_shell_without_reading_env(tmp_path, monkeypatch):
    monkeypatch.setenv("SHELL", "/mensonge/shell")
    monkeypatch.setattr(shell_module, "_login_shell", lambda: "/opt/homebrew/bin/fish")
    home = _fake_home(tmp_path)
    _, dest, result = _collect(tmp_path, home)

    meta = json.loads((dest / "meta.json").read_text())
    # $SHELL est ignore : c'est pw_shell qui fait foi.
    assert meta["login_shell"] == "/opt/homebrew/bin/fish"
    assert result.meta["login_shell"] == "/opt/homebrew/bin/fish"


def test_check_fatal_when_nothing_to_save(tmp_path):
    home = tmp_path / "vide"
    home.mkdir()
    problems = ShellModule(home=home).check()
    assert problems and problems[0].fatal


def test_check_ok_when_only_zsh(tmp_path):
    home = _fake_home(tmp_path, with_fish=False)
    assert ShellModule(home=home).check() == []


def test_plan_restore_three_states(tmp_path):
    home = _fake_home(tmp_path)
    _, dest, _ = _collect(tmp_path, home)

    # Cible : config.fish identique, ll.fish absent, .zshrc different.
    (home / ".config" / "fish" / "functions" / "ll.fish").unlink()
    (home / ".zshrc").write_text("export EDITOR=nano")

    mod = ShellModule(home=home, runner=make_runner())
    actions = mod.plan_restore(dest)
    by_target = {a.target: a.state for a in actions}

    assert by_target[str(home / ".config" / "fish" / "config.fish")] is (
        ActionState.SATISFIED
    )
    assert by_target[str(home / ".config" / "fish" / "functions" / "ll.fish")] is (
        ActionState.ADDITIVE
    )
    assert by_target[str(home / ".zshrc")] is ActionState.DESTRUCTIVE


def test_shell_choice_fish_ignores_zsh(tmp_path):
    home = _fake_home(tmp_path)
    _, dest, _ = _collect(tmp_path, home)
    mod = ShellModule(home=home, shell_choice="fish", runner=make_runner())
    targets = [a.target for a in mod.plan_restore(dest)]
    assert not any(".zshrc" in t or "oh-my-zsh" in t for t in targets)
    assert any("fish" in t for t in targets)


def test_shell_choice_zsh_ignores_fish(tmp_path):
    home = _fake_home(tmp_path)
    _, dest, _ = _collect(tmp_path, home)
    mod = ShellModule(home=home, shell_choice="zsh", runner=make_runner())
    targets = [a.target for a in mod.plan_restore(dest)]
    assert not any("/.config/fish/" in t for t in targets)


def test_zsh_requested_without_zsh_in_snapshot_installs_omz(tmp_path):
    home = _fake_home(tmp_path, with_zsh=False)
    _, dest, _ = _collect(tmp_path, home)
    assert not (dest / "zsh").exists()

    mod = ShellModule(home=home, shell_choice="zsh", runner=make_runner())
    actions = mod.plan_restore(dest)
    descriptions = " ".join(a.description for a in actions)
    assert "oh-my-zsh" in descriptions
    # Aucun fichier n'est ecrase : seul le changement de shell de login peut
    # etre destructif, et il reste soumis a --force.
    file_actions = [
        a
        for a in actions
        if a.target not in (shell_module.CHSH_TARGET, shell_module.OMZ_TARGET)
    ]
    assert all(a.state is not ActionState.DESTRUCTIVE for a in file_actions)


def test_both_without_zsh_does_not_install_omz(tmp_path):
    home = _fake_home(tmp_path, with_zsh=False)
    _, dest, _ = _collect(tmp_path, home)
    mod = ShellModule(home=home, shell_choice="both", runner=make_runner())
    descriptions = " ".join(a.description for a in mod.plan_restore(dest))
    assert "oh-my-zsh" not in descriptions


def test_chsh_is_destructive_and_never_applied_without_force(tmp_path, monkeypatch):
    home = _fake_home(tmp_path)
    monkeypatch.setattr(shell_module, "_login_shell", lambda: "/opt/homebrew/bin/fish")
    _, dest, _ = _collect(tmp_path, home)

    monkeypatch.setattr(shell_module, "_login_shell", lambda: "/bin/zsh")
    runner = make_runner()
    mod = ShellModule(home=home, runner=runner)
    actions = mod.plan_restore(dest)

    chsh = [a for a in actions if a.target == shell_module.CHSH_TARGET]
    assert chsh and chsh[0].state is ActionState.DESTRUCTIVE

    mod.restore(dest, actions, force=False)
    assert not any(os.path.basename(c[0]) == "chsh" for c in runner.calls)


def test_chsh_runs_under_force(tmp_path, monkeypatch):
    home = _fake_home(tmp_path)
    monkeypatch.setattr(shell_module, "_login_shell", lambda: "/opt/homebrew/bin/fish")
    _, dest, _ = _collect(tmp_path, home)

    monkeypatch.setattr(shell_module, "_login_shell", lambda: "/bin/zsh")
    runner = make_runner()
    mod = ShellModule(home=home, runner=runner)
    mod.restore(dest, mod.plan_restore(dest), force=True)

    chsh_calls = [c for c in runner.calls if os.path.basename(c[0]) == "chsh"]
    assert chsh_calls and "/opt/homebrew/bin/fish" in chsh_calls[0]


def test_restore_additive_copies_files(tmp_path):
    home = _fake_home(tmp_path)
    _, dest, _ = _collect(tmp_path, home)
    target = home / ".config" / "fish" / "functions" / "ll.fish"
    target.unlink()

    mod = ShellModule(home=home, runner=make_runner())
    result = mod.restore(dest, mod.plan_restore(dest), force=False)
    assert result.status is ModuleStatus.OK
    assert target.read_text() == "function ll; end"


def test_restore_never_overwrites_without_force(tmp_path):
    home = _fake_home(tmp_path)
    _, dest, _ = _collect(tmp_path, home)
    (home / ".zshrc").write_text("MODIFIE A LA MAIN")

    mod = ShellModule(home=home, runner=make_runner())
    mod.restore(dest, mod.plan_restore(dest), force=False)
    assert (home / ".zshrc").read_text() == "MODIFIE A LA MAIN"


def test_restore_overwrites_with_force(tmp_path):
    home = _fake_home(tmp_path)
    _, dest, _ = _collect(tmp_path, home)
    (home / ".zshrc").write_text("MODIFIE A LA MAIN")

    mod = ShellModule(home=home, runner=make_runner())
    mod.restore(dest, mod.plan_restore(dest), force=True)
    assert (home / ".zshrc").read_text() == "export EDITOR=vim"


def test_no_translation_between_shells(tmp_path):
    """Restaurer en zsh ne doit rien ecrire sous la config fish, et l'inverse."""
    home = _fake_home(tmp_path)
    _, dest, _ = _collect(tmp_path, home)

    import shutil as _shutil

    _shutil.rmtree(home / ".config" / "fish")
    (home / ".zshrc").unlink()

    mod = ShellModule(home=home, shell_choice="zsh", runner=make_runner())
    mod.restore(dest, mod.plan_restore(dest), force=True)

    assert (home / ".zshrc").exists()
    assert not (home / ".config" / "fish").exists()
