import json

from osx_backup.context import Context
from osx_backup.modules.base import NullProgress
from osx_backup.modules.pathset import PathItem, backup_paths, inventory_paths, restore_paths


def _seed(ctx: Context) -> None:
    (ctx.home / ".zshrc").write_text("zsh")
    fish = ctx.home / ".config" / "fish"
    fish.mkdir(parents=True)
    (fish / "config.fish").write_text("fish")
    (fish / "fish_variables").write_text("vars")
    (fish / "cache").mkdir()
    (fish / "cache" / "x").write_text("cache")


ITEMS = [PathItem(".zshrc"), PathItem(".config/fish", ("fish_variables",)), PathItem(".missing")]


def test_inventory_paths(ctx: Context) -> None:
    _seed(ctx)
    inv = inventory_paths(ctx, ITEMS)
    assert inv.items == 2
    assert inv.size_bytes == len("zsh") + len("fish")
    assert inv.warnings == ["absent : ~/.missing"]


def test_backup_and_restore_roundtrip(ctx: Context, fake_home) -> None:
    _seed(ctx)
    res = backup_paths(ctx, "dotfiles", ITEMS, NullProgress())
    assert res.status == "ok" and res.items == 2
    out = ctx.dest / "dotfiles"
    assert (out / ".zshrc").read_text() == "zsh"
    assert (out / ".config" / "fish" / "config.fish").exists()
    assert not (out / ".config" / "fish" / "fish_variables").exists()
    assert not (out / ".config" / "fish" / "cache").exists()
    assert json.loads((out / "_entries.json").read_text()) == [".zshrc", ".config/fish"]

    (ctx.home / ".zshrc").write_text("old")
    res2 = restore_paths(ctx, "dotfiles", NullProgress())
    assert res2.status == "ok" and res2.items == 2
    assert (ctx.home / ".zshrc").read_text() == "zsh"
    assert (ctx.pre_restore_dir / ".zshrc").read_text() == "old"


def test_restore_without_backup_is_skipped(ctx: Context) -> None:
    assert restore_paths(ctx, "dotfiles", NullProgress()).status == "skipped"


def test_backup_dry_run_writes_nothing(ctx: Context) -> None:
    _seed(ctx)
    ctx.dry_run = True
    res = backup_paths(ctx, "dotfiles", ITEMS, NullProgress())
    assert res.status == "ok"
    assert not (ctx.dest / "dotfiles").exists()


def test_restore_skips_entry_missing_from_backup(ctx: Context) -> None:
    _seed(ctx)
    backup_paths(ctx, "dotfiles", ITEMS, NullProgress())
    (ctx.dest / "dotfiles" / ".zshrc").unlink()  # sauvegarde tronquée
    res = restore_paths(ctx, "dotfiles", NullProgress())
    assert res.status == "ok" and res.items == 1
    assert res.warnings == ["absent de la sauvegarde, ignoré : dotfiles/.zshrc"]
    assert (ctx.home / ".zshrc").read_text() == "zsh"
    assert not (ctx.pre_restore_dir / ".zshrc").exists()


def test_backup_continues_after_unreadable_entry(ctx: Context) -> None:
    _seed(ctx)
    gemini = ctx.home / ".gemini"
    gemini.mkdir()
    unreadable = gemini / "secret"
    unreadable.write_text("nope")
    unreadable.chmod(0o000)
    try:
        res = backup_paths(
            ctx, "dotfiles", [PathItem(".gemini"), PathItem(".zshrc")], NullProgress()
        )
        assert res.status == "ok"
        assert len(res.warnings) == 1 and ".gemini" in res.warnings[0]
        out = ctx.dest / "dotfiles"
        assert json.loads((out / "_entries.json").read_text()) == [".zshrc"]
        assert (out / ".zshrc").read_text() == "zsh"
    finally:
        unreadable.chmod(0o600)


def test_backup_fails_when_no_entry_can_be_copied(ctx: Context) -> None:
    gemini = ctx.home / ".gemini"
    gemini.mkdir()
    unreadable = gemini / "secret"
    unreadable.write_text("nope")
    unreadable.chmod(0o000)
    try:
        res = backup_paths(ctx, "dotfiles", [PathItem(".gemini")], NullProgress())
        assert res.status == "failed"
        assert res.items == 0 and len(res.warnings) == 1
    finally:
        unreadable.chmod(0o600)


def test_backup_ignores_entries_that_escape_the_destination(ctx: Context) -> None:
    """`out / "/etc/x"` EST `/etc/x` : sans garde, remove_path effacerait le vrai fichier."""
    outside = ctx.dest.parent / "precieux.txt"
    outside.write_text("à ne pas toucher")
    _seed(ctx)
    res = backup_paths(
        ctx,
        "dotfiles",
        [PathItem(str(outside)), PathItem("../precieux.txt"), PathItem(".zshrc")],
        NullProgress(),
    )
    assert outside.read_text() == "à ne pas toucher"
    assert res.status == "ok" and res.items == 1
    assert len(res.warnings) == 2
    assert json.loads((ctx.dest / "dotfiles" / "_entries.json").read_text()) == [".zshrc"]


def test_restore_ignores_entries_that_escape_home(ctx: Context) -> None:
    _seed(ctx)
    backup_paths(ctx, "dotfiles", [PathItem(".zshrc")], NullProgress())
    outside = ctx.home.parent / "hors-home.txt"
    outside.write_text("intact")
    entries = ctx.dest / "dotfiles" / "_entries.json"
    entries.write_text(json.dumps([str(outside), "../hors-home.txt", ".zshrc"]))
    res = restore_paths(ctx, "dotfiles", NullProgress())
    assert outside.read_text() == "intact"
    assert res.status == "ok" and res.items == 1
    assert len(res.warnings) == 2
