import os
import time
from pathlib import Path

import pytest

from agentmux.utils.symlink import (
    _MAX_BACKUPS,
    _cleanup_backups,
    create_symlink,
    sanitize_component_name,
    write_file,
)


def test_backup_cleanup_keeps_only_last_n(tmp_path: Path) -> None:
    target = tmp_path / "file.txt"
    # Write and update the file more than _MAX_BACKUPS times to accumulate backups
    for i in range(_MAX_BACKUPS + 2):
        target.write_text(f"version {i}")
        time.sleep(0.01)  # ensure distinct timestamps
        write_file(f"version {i + 1}", target)

    backups = sorted(tmp_path.glob("file.txt.bak.*"))
    assert len(backups) <= _MAX_BACKUPS


def test_write_file_creates_new(tmp_path: Path) -> None:
    target = tmp_path / "new.txt"
    action, backup_path = write_file("hello", target)
    assert action == "created"
    assert backup_path is None
    assert target.read_text() == "hello"


def test_write_file_skips_identical(tmp_path: Path) -> None:
    target = tmp_path / "same.txt"
    target.write_text("same")
    action, backup_path = write_file("same", target)
    assert action == "skipped"
    assert backup_path is None


def test_write_file_updates_changed(tmp_path: Path) -> None:
    target = tmp_path / "file.txt"
    target.write_text("old")
    action, backup_path = write_file("new", target)
    assert action == "updated"
    assert target.read_text() == "new"
    assert backup_path is not None
    assert backup_path.exists()
    assert backup_path.read_text() == "old"


# ── create_symlink ────────────────────────────────────────────────────────────


def test_create_symlink_creates_new_link(tmp_path: Path) -> None:
    source = tmp_path / "source.txt"
    source.write_text("content")
    target = tmp_path / "link.txt"

    action, backup_path = create_symlink(source, target)
    assert action in ("created", "copied")  # "copied" on Windows
    assert backup_path is None
    assert target.read_text() == "content"


def test_create_symlink_skips_identical(tmp_path: Path) -> None:
    source = tmp_path / "source.txt"
    source.write_text("content")
    target = tmp_path / "link.txt"
    os.symlink(source, target)

    action, backup_path = create_symlink(source, target)
    assert action == "skipped"
    assert backup_path is None


def test_create_symlink_replaces_stale_symlink(tmp_path: Path) -> None:
    old_source = tmp_path / "old.txt"
    old_source.write_text("old")
    new_source = tmp_path / "new.txt"
    new_source.write_text("new")
    target = tmp_path / "link.txt"
    os.symlink(old_source, target)

    action, backup_path = create_symlink(new_source, target)
    assert action in ("created", "copied")
    assert backup_path is None  # symlink replaced — no backup of a symlink
    assert target.read_text() == "new"


def test_create_symlink_backs_up_existing_file(tmp_path: Path) -> None:
    source = tmp_path / "source.txt"
    source.write_text("from source")
    target = tmp_path / "existing.txt"
    target.write_text("original content")

    action, backup_path = create_symlink(source, target)
    assert action in ("created", "copied")
    assert target.read_text() == "from source"
    assert backup_path is not None
    assert backup_path.exists()
    assert backup_path.read_text() == "original content"


def test_create_symlink_creates_parent_dirs(tmp_path: Path) -> None:
    source = tmp_path / "source.txt"
    source.write_text("content")
    target = tmp_path / "sub" / "dir" / "link.txt"

    action, backup_path = create_symlink(source, target)
    assert action in ("created", "copied")
    assert backup_path is None
    assert target.read_text() == "content"


def test_create_symlink_falls_back_to_copy_on_oserror(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """create_symlink copies the file when os.symlink raises OSError (Windows-like env)."""
    source = tmp_path / "source.txt"
    source.write_text("fallback content")
    target = tmp_path / "link.txt"

    def _raise_oserror(src: object, dst: object) -> None:
        raise OSError("symlinks not supported on this filesystem")

    monkeypatch.setattr(os, "symlink", _raise_oserror)

    action, backup = create_symlink(source, target)

    assert action == "copied"
    assert not target.is_symlink()
    assert target.read_text() == "fallback content"


def test_create_symlink_falls_back_to_copytree_for_directory(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """create_symlink copies a directory tree when os.symlink raises OSError."""
    source = tmp_path / "source_dir"
    source.mkdir()
    (source / "file.txt").write_text("dir content")
    target = tmp_path / "link_dir"

    def _raise_oserror(src: object, dst: object) -> None:
        raise OSError("symlinks not supported")

    monkeypatch.setattr(os, "symlink", _raise_oserror)

    action, backup = create_symlink(source, target, is_directory=True)

    assert action == "copied"
    assert target.is_dir()
    assert (target / "file.txt").read_text() == "dir content"


# ── write_file: symlink target ────────────────────────────────────────────────


def test_write_file_replaces_symlink_target(tmp_path: Path) -> None:
    """write_file must replace a symlink at the target with a regular file."""
    real = tmp_path / "real.txt"
    real.write_text("old content")
    target = tmp_path / "link.txt"
    os.symlink(real, target)

    action, backup = write_file("new content", target)

    assert action == "created"
    assert backup is None
    assert not target.is_symlink()
    assert target.read_text() == "new content"


# ── sanitize_component_name edge cases ───────────────────────────────────────


def test_sanitize_component_name_empty_raises() -> None:
    """sanitize_component_name must raise ValueError for empty input."""
    with pytest.raises(ValueError, match="must not be empty"):
        sanitize_component_name("")


def test_sanitize_component_name_only_dots_raises() -> None:
    """sanitize_component_name must raise ValueError when the name reduces to empty."""
    with pytest.raises(ValueError, match="reduces to an empty string"):
        sanitize_component_name(" . ")


def test_sanitize_component_name_replaces_unsafe_chars() -> None:
    """Unsafe characters are replaced with underscores."""
    result = sanitize_component_name("my/unsafe:name")
    assert "/" not in result
    assert ":" not in result


# ── _cleanup_backups: directory removal ──────────────────────────────────────


def test_cleanup_backups_removes_oldest_directory_backups(tmp_path: Path) -> None:
    """_cleanup_backups removes directory-type backups beyond the retention limit."""
    for i in range(_MAX_BACKUPS + 2):
        bak_dir = tmp_path / f"mydir.bak.{i:08d}_000000_000000"
        bak_dir.mkdir()
        (bak_dir / "file.txt").write_text(f"content {i}")

    _cleanup_backups(tmp_path, "mydir")

    remaining = sorted(tmp_path.glob("mydir.bak.*"))
    assert len(remaining) <= _MAX_BACKUPS
