from collections.abc import Sequence
from pathlib import Path
from typing import Any

from osx_backup.rsync import (
    build_command,
    estimate,
    find_gnu_rsync,
    parse_progress2,
    parse_stats,
)
from osx_backup.runner import CommandResult, RecordingRunner

GNU = "rsync  version 3.4.1  protocol version 32\n"
OPENRSYNC = "openrsync: protocol version 29\n"
# Sortie réelle de /usr/bin/rsync sur macOS 15 : il s'annonce « compatible » 2.6.9.
MACOS_OPENRSYNC = "openrsync: protocol version 29\nrsync version 2.6.9 compatible\n"


def test_find_gnu_rsync_prefers_homebrew() -> None:
    r = RecordingRunner()
    r.add(("/opt/homebrew/bin/rsync", "--version"), stdout=GNU)
    assert find_gnu_rsync(r) == "/opt/homebrew/bin/rsync"


def test_find_gnu_rsync_rejects_openrsync() -> None:
    r = RecordingRunner(which_map={"rsync": "/usr/bin/rsync"})
    r.add(("/opt/homebrew/bin/rsync", "--version"), returncode=127)
    r.add(("/usr/local/bin/rsync", "--version"), returncode=127)
    r.add(("/usr/bin/rsync", "--version"), stdout=OPENRSYNC)
    assert find_gnu_rsync(r) is None
    assert all(c[1] == "--version" for c in r.calls)
    assert r.mutations == []


def _openrsync_runner(version_output: str) -> RecordingRunner:
    r = RecordingRunner(which_map={"rsync": "/usr/bin/rsync"})
    r.add(("/opt/homebrew/bin/rsync", "--version"), returncode=127)
    r.add(("/usr/local/bin/rsync", "--version"), returncode=127)
    r.add(("/usr/bin/rsync", "--version"), stdout=version_output)
    return r


def test_find_gnu_rsync_rejects_real_macos_rsync() -> None:
    """Sortie réelle de /usr/bin/rsync : « rsync version 2.6.9 compatible »."""
    assert find_gnu_rsync(_openrsync_runner(MACOS_OPENRSYNC)) is None


def test_find_gnu_rsync_rejects_openrsync_claiming_v3() -> None:
    """Un openrsync qui s'annoncerait compatible 3.x reste un openrsync (pas de progress2)."""
    assert find_gnu_rsync(_openrsync_runner(MACOS_OPENRSYNC.replace("2.6.9", "3.4.1"))) is None


def test_build_command() -> None:
    cmd = build_command("/r", Path("/src"), Path("/dst"), ["node_modules", ".env"], delete=True)
    assert cmd == [
        "/r",
        "-a",
        "--no-inc-recursive",
        "--info=progress2",
        "--delete",
        "--exclude",
        "node_modules",
        "--exclude",
        ".env",
        "/src/",
        "/dst/",
    ]
    cmd2 = build_command("/r", Path("/s"), Path("/d"), [], delete=False, dry_run=True, stats=True)
    assert "--dry-run" in cmd2 and "--stats" in cmd2 and "--delete" not in cmd2


def test_parse_progress2() -> None:
    assert parse_progress2("  1,234,567  45%   12.34MB/s    0:00:10") == (1234567, 45)
    assert parse_progress2("sending incremental file list") is None


def test_parse_stats() -> None:
    text = "Number of files: 1,234 (reg: 1,000, dir: 234)\nTotal file size: 9,876,543 bytes\n"
    assert parse_stats(text) == (1234, 9876543)
    assert parse_stats("") == (0, 0)


def test_estimate_is_readonly() -> None:
    r = RecordingRunner()
    r.add(("/r",), stdout="Number of files: 3\nTotal file size: 42 bytes\n")
    assert estimate(r, "/r", Path("/s"), Path("/d"), []) == (3, 42, 0)
    assert r.mutations == []
    assert "--dry-run" in r.calls[0]


def test_estimate_reports_the_failure_code() -> None:
    r = RecordingRunner()
    r.add(("/r",), returncode=23, stderr="rsync: opendir failed: Permission denied (13)\n")
    assert estimate(r, "/r", Path("/s"), Path("/d"), []) == (0, 0, 23)


def test_parse_progress2_human_readable_keeps_the_percent() -> None:
    """Avec --human-readable (popt de l'utilisateur) les octets ne sont pas chiffrés."""
    assert parse_progress2("      1.23G  45%   12.34MB/s    0:00:10") == (0, 45)


class _MissingBinaryRunner(RecordingRunner):
    """RealRunner lève FileNotFoundError quand le binaire candidat n'existe pas."""

    def run(self, cmd: Sequence[str], **kwargs: Any) -> CommandResult:
        if cmd[0] in ("/opt/homebrew/bin/rsync", "/usr/local/bin/rsync"):
            raise FileNotFoundError(2, "No such file or directory", cmd[0])
        return super().run(cmd, **kwargs)


def test_find_gnu_rsync_survives_missing_candidate_binary() -> None:
    r = _MissingBinaryRunner(which_map={"rsync": "/usr/bin/rsync"})
    r.add(("/usr/bin/rsync", "--version"), stdout=MACOS_OPENRSYNC)
    assert find_gnu_rsync(r) is None

    r2 = _MissingBinaryRunner(which_map={"rsync": "/usr/bin/rsync"})
    r2.add(("/usr/bin/rsync", "--version"), stdout=GNU)
    assert find_gnu_rsync(r2) == "/usr/bin/rsync"


# Bloc --stats complet d'un rsync GNU 3.x : les lignes voisines ne doivent pas être prises
# pour « Number of files » ni « Total file size ».
GNU_STATS = """sending incremental file list

Number of files: 1,234 (reg: 1,000, dir: 234)
Number of created files: 12 (reg: 10, dir: 2)
Number of deleted files: 0
Number of regular files transferred: 10
Total file size: 9,876,543 bytes
Total transferred file size: 4,096 bytes
Literal data: 4,096 bytes
Matched data: 0 bytes
File list size: 0
Total bytes sent: 5,120
Total bytes received: 1,024
"""

GNU_PROGRESS = """sending incremental file list
        32,768   0%   31.25MB/s    0:00:00 (xfr#1, to-chk=1233/1234)
   9,876,543  100%   45.67MB/s    0:00:12 (xfr#10, to-chk=0/1234)
"""


def test_parse_stats_on_full_gnu_block() -> None:
    assert parse_stats(GNU_STATS) == (1234, 9876543)


def test_parse_progress2_on_full_gnu_stream() -> None:
    parsed = [parse_progress2(line) for line in GNU_PROGRESS.splitlines()]
    assert parsed == [None, (32768, 0), (9876543, 100)]


def test_build_command_backup_dir() -> None:
    cmd = build_command(
        "/r", Path("/s"), Path("/d"), [], delete=False, backup_dir=Path("/aside/Documents")
    )
    assert "--backup" in cmd and "--backup-dir=/aside/Documents" in cmd
