import os
from pathlib import Path

import pytest

from osx_backup.fsutil import (
    DEPENDENCY_DIRS,
    copy_tree,
    format_size,
    is_excluded,
    move_aside,
    tree_size,
)


def test_is_excluded_by_part_and_glob() -> None:
    assert is_excluded(Path("a/node_modules/b"), ["**/node_modules"])
    assert is_excluded(Path("x/app.log"), ["**/*.log"])
    assert not is_excluded(Path("x/app.py"), ["**/*.log"])
    assert is_excluded(Path("plugins/cache/x"), ["cache"])
    assert is_excluded(Path(".env.local"), [".env.*"])
    assert not is_excluded(Path("src/main.py"), DEPENDENCY_DIRS)


def test_is_excluded_by_relative_path_prefix() -> None:
    assert is_excluded(Path("plugins/cache"), ["plugins/cache"])
    assert is_excluded(Path("plugins/cache/deep/file"), ["plugins/cache"])
    assert not is_excluded(Path("other/cache"), ["plugins/cache"])


def test_is_excluded_single_segment_matches_any_depth() -> None:
    assert is_excluded(Path("cache"), ["cache"])
    assert is_excluded(Path("plugins/cache"), ["cache"])
    assert is_excluded(Path("plugins/foo/cache"), ["cache"])
    assert is_excluded(Path("cache/deep/file"), ["cache"])
    assert not is_excluded(Path("plugins/caches"), ["cache"])


def test_is_excluded_double_star_sequence_matches_any_depth() -> None:
    assert is_excluded(Path("foo/bar"), ["**/foo/bar"])
    assert is_excluded(Path("x/foo/bar"), ["**/foo/bar"])
    assert is_excluded(Path("x/y/foo/bar"), ["**/foo/bar"])
    assert is_excluded(Path("x/foo/bar/deep/file"), ["**/foo/bar"])
    assert not is_excluded(Path("x/foo/baz"), ["**/foo/bar"])
    assert not is_excluded(Path("foo"), ["**/foo/bar"])


def test_is_excluded_anchored_sequence_only_matches_from_root() -> None:
    assert is_excluded(Path("plugins/cache"), ["plugins/cache"])
    assert not is_excluded(Path("x/plugins/cache"), ["plugins/cache"])
    assert not is_excluded(Path("plugins"), ["plugins/cache"])


def _make_tree(root: Path) -> None:
    (root / "src").mkdir(parents=True)
    (root / "src" / "main.py").write_text("print(1)")
    (root / "node_modules" / "pkg").mkdir(parents=True)
    (root / "node_modules" / "pkg" / "index.js").write_text("x")
    (root / "app.log").write_text("log")
    os.symlink("src/main.py", root / "link.py")


def test_copy_tree_applies_excludes_and_keeps_symlinks(tmp_path: Path) -> None:
    src, dst = tmp_path / "src", tmp_path / "dst"
    _make_tree(src)
    copied = copy_tree(src, dst, ["**/node_modules", "**/*.log"])
    assert (dst / "src" / "main.py").read_text() == "print(1)"
    assert not (dst / "node_modules").exists()
    assert not (dst / "app.log").exists()
    assert (dst / "link.py").is_symlink() and os.readlink(dst / "link.py") == "src/main.py"
    assert copied == len("print(1)")


def test_copy_tree_single_file(tmp_path: Path) -> None:
    f = tmp_path / "a.txt"
    f.write_text("abc")
    assert copy_tree(f, tmp_path / "out" / "b.txt") == 3
    assert (tmp_path / "out" / "b.txt").read_text() == "abc"


def test_copy_tree_missing_source_raises(tmp_path: Path) -> None:
    with pytest.raises(FileNotFoundError):
        copy_tree(tmp_path / "nope", tmp_path / "out")


def test_tree_size(tmp_path: Path) -> None:
    _make_tree(tmp_path / "t")
    files, size = tree_size(tmp_path / "t", ["**/node_modules"])
    assert files == 3 and size == len("print(1)") + len("log")


def test_move_aside(tmp_path: Path) -> None:
    home = tmp_path / "home"
    target = home / ".config" / "fish"
    target.mkdir(parents=True)
    (target / "config.fish").write_text("x")
    aside = tmp_path / "aside"
    moved = move_aside(target, aside, home)
    assert moved == aside / ".config" / "fish"
    assert (moved / "config.fish").exists() and not target.exists()
    assert move_aside(target, aside, home) is None


def test_move_aside_never_overwrites_a_previous_copy(tmp_path: Path) -> None:
    home = tmp_path / "home"
    home.mkdir()
    aside = tmp_path / "aside"
    target = home / "f"
    target.write_text("original")
    assert move_aside(target, aside, home) == aside / "f"
    target.write_text("replacement")
    assert move_aside(target, aside, home) == aside / "f.1"
    assert (aside / "f").read_text() == "original"
    assert (aside / "f.1").read_text() == "replacement"


def test_move_aside_never_nests_directories(tmp_path: Path) -> None:
    home = tmp_path / "home"
    aside = tmp_path / "aside"
    d = home / "d"
    d.mkdir(parents=True)
    (d / "x").write_text("original")
    move_aside(d, aside, home)
    d.mkdir()
    (d / "y").write_text("replacement")
    assert move_aside(d, aside, home) == aside / "d.1"
    assert (aside / "d" / "x").read_text() == "original"
    assert (aside / "d.1" / "y").read_text() == "replacement"
    assert not (aside / "d" / "d").exists()


def test_format_size() -> None:
    assert format_size(512) == "512 B"
    assert format_size(1536) == "1.5 KB"
    assert format_size(3 * 1024**3) == "3.0 GB"
