from pathlib import Path

import pytest

from osx_backup.context import Context
from osx_backup.modules.base import NullProgress, get_module
from osx_backup.modules.documents import DocumentsModule
from osx_backup.modules.github import GithubModule
from osx_backup.rsync import RSYNC_MISSING
from osx_backup.runner import RecordingRunner

GNU = "rsync  version 3.4.1  protocol version 32\n"


def _runner() -> RecordingRunner:
    r = RecordingRunner()
    r.add(("/opt/homebrew/bin/rsync", "--version"), stdout=GNU)
    return r


def test_registered() -> None:
    assert isinstance(get_module("github"), GithubModule)
    assert isinstance(get_module("documents"), DocumentsModule)


def test_unavailable_without_source(ctx: Context) -> None:
    ctx.runner = _runner()
    inv = GithubModule().inventory(ctx)
    assert not inv.available and "Github" in inv.reason


def test_unavailable_without_gnu_rsync(ctx: Context) -> None:
    (ctx.home / "Github").mkdir()
    ctx.runner = RecordingRunner(which_map={})
    inv = GithubModule().inventory(ctx)
    assert not inv.available and inv.reason == RSYNC_MISSING


def test_inventory_counts_non_git_dirs(ctx: Context) -> None:
    gh = ctx.home / "Github"
    (gh / "a" / ".git").mkdir(parents=True)
    (gh / "b").mkdir()
    r = _runner()
    r.add(
        ("/opt/homebrew/bin/rsync", "-a"), stdout="Number of files: 5\nTotal file size: 100 bytes\n"
    )
    ctx.runner = r
    inv = GithubModule().inventory(ctx)
    assert inv.available and inv.items == 5 and inv.size_bytes == 100
    assert inv.warnings == ["1 dossier(s) sans dépôt git : b"]


def test_backup_streams_progress_and_uses_delete(ctx: Context) -> None:
    (ctx.home / "Github").mkdir()
    r = _runner()
    r.add(("/opt/homebrew/bin/rsync", "-a"), stdout="  500  50%  1MB/s\n  1,000  100%  1MB/s\n")
    ctx.runner = r
    p = NullProgress()
    res = GithubModule().backup(ctx, p)
    assert res.status == "ok" and res.size_bytes == 1000
    cmd = r.mutations[-1]
    assert "--delete" in cmd and "--exclude" in cmd and "node_modules" in cmd
    assert cmd[-2:] == [f"{ctx.home}/Github/", f"{ctx.dest}/github/"]
    assert p.tasks[0].completed == 100 and p.tasks[0].finished


def test_restore_never_deletes(ctx: Context) -> None:
    (ctx.dest / "documents").mkdir()
    r = _runner()
    ctx.runner = r
    res = DocumentsModule().restore(ctx, NullProgress())
    assert res.status == "ok"
    cmd = r.mutations[-1]
    assert "--delete" not in cmd
    assert cmd[-2:] == [f"{ctx.dest}/documents/", f"{ctx.home}/Documents/"]


def test_restore_without_backup_skipped(ctx: Context) -> None:
    ctx.runner = _runner()
    assert DocumentsModule().restore(ctx, NullProgress()).status == "skipped"


def test_backup_failure_reported(ctx: Context) -> None:
    (ctx.home / "Documents").mkdir()
    r = _runner()
    r.add(("/opt/homebrew/bin/rsync", "-a"), returncode=23)
    ctx.runner = r
    res = DocumentsModule().backup(ctx, NullProgress())
    assert res.status == "failed" and "23" in res.message


def test_backup_without_gnu_rsync_fails(ctx: Context) -> None:
    (ctx.home / "Documents").mkdir()
    ctx.runner = RecordingRunner(which_map={})
    res = DocumentsModule().backup(ctx, NullProgress())
    assert res.status == "failed" and res.message == RSYNC_MISSING
    assert ctx.runner.mutations == []


def test_dry_run_backup_writes_nothing(ctx: Context) -> None:
    (ctx.home / "Documents").mkdir()
    ctx.dry_run = True
    ctx.runner = _runner()
    assert DocumentsModule().backup(ctx, NullProgress()).status == "ok"
    assert not (ctx.dest / "documents").exists()


def test_backup_tolerates_vanished_files(ctx: Context) -> None:
    """Code 24 : des fichiers ont disparu pendant la copie, le miroir reste exploitable."""
    (ctx.home / "Documents").mkdir()
    r = _runner()
    r.add(("/opt/homebrew/bin/rsync", "-a"), stdout="  700  100%  1MB/s\n", returncode=24)
    ctx.runner = r
    res = DocumentsModule().backup(ctx, NullProgress())
    assert res.status == "ok" and res.size_bytes == 700
    assert res.warnings == ["des fichiers ont disparu pendant la copie (rsync code 24)"]


def test_backup_failure_message_carries_stderr(ctx: Context) -> None:
    (ctx.home / "Documents").mkdir()
    ctx.log_path = ctx.dest / "log.txt"
    r = _runner()
    r.add(
        ("/opt/homebrew/bin/rsync", "-a"),
        returncode=23,
        stderr=(
            'rsync: send_files failed to open "/x": Permission denied (13)\n'
            "rsync error: some files could not be transferred (code 23)\n"
        ),
    )
    ctx.runner = r
    res = DocumentsModule().backup(ctx, NullProgress())
    assert res.status == "failed"
    assert "code 23" in res.message
    assert "rsync error: some files could not be transferred (code 23)" in res.message
    assert "Permission denied (13)" in ctx.log_path.read_text()


def test_inventory_warns_when_estimate_fails(ctx: Context) -> None:
    (ctx.home / "Documents").mkdir()
    r = _runner()
    r.add(("/opt/homebrew/bin/rsync", "-a"), returncode=23)
    ctx.runner = r
    inv = DocumentsModule().inventory(ctx)
    assert inv.available and inv.items == 0
    assert inv.warnings == ["estimation rsync impossible (code 23)"]


def test_inventory_survives_unreadable_source(
    ctx: Context, monkeypatch: pytest.MonkeyPatch
) -> None:
    (ctx.home / "Github").mkdir()
    r = _runner()
    r.add(
        ("/opt/homebrew/bin/rsync", "-a"), stdout="Number of files: 5\nTotal file size: 100 bytes\n"
    )
    ctx.runner = r

    def boom(self: Path) -> None:
        raise PermissionError(13, "Permission denied")

    monkeypatch.setattr(Path, "iterdir", boom)
    inv = GithubModule().inventory(ctx)
    assert inv.available and inv.items == 5
    assert inv.warnings == ["impossible de lister ~/Github : Permission denied"]


def test_restore_keeps_a_copy_of_overwritten_files(ctx: Context) -> None:
    """`rsync -a` écrase les fichiers modifiés : --backup-dir les met de côté comme place()."""
    (ctx.dest / "documents").mkdir()
    r = _runner()
    ctx.runner = r
    assert DocumentsModule().restore(ctx, NullProgress()).status == "ok"
    cmd = r.mutations[-1]
    assert "--backup" in cmd
    assert f"--backup-dir={ctx.pre_restore_dir / 'Documents'}" in cmd


def test_backup_never_uses_backup_dir(ctx: Context) -> None:
    (ctx.home / "Documents").mkdir()
    r = _runner()
    ctx.runner = r
    DocumentsModule().backup(ctx, NullProgress())
    assert not any(c.startswith("--backup") for c in r.mutations[-1])
