from datetime import datetime

import pytest

from machine_backup.cli import resolve_snapshot, rotate_snapshots, run_backup
from machine_backup.config import Config
from machine_backup.console import Reporter
from machine_backup.manifest import read_manifest
from machine_backup.modules.base import Problem

from conftest import FakeModule, needs_bins


def _cfg(tmp_path, recipient, identity, **kw):
    return Config(
        destination=tmp_path / "backups",
        keep_snapshots=kw.pop("keep_snapshots", 7),
        age_recipient=recipient,
        age_identity=identity,
        **kw,
    )


def _reporter():
    return Reporter(mode="linear")


@needs_bins
def test_backup_complete(tmp_path, age_keys):
    recipient, identity = age_keys
    code = run_backup(
        [FakeModule()],
        _cfg(tmp_path, recipient, identity),
        _reporter(),
        now=datetime(2026, 8, 5, 14, 30, 12),
    )
    snap = tmp_path / "backups" / "2026-08-05T14-30-12"
    assert code == 0
    assert (snap / "snapshot.tar.zst.age").exists()

    m = read_manifest(snap / "manifest.json")
    assert m["status"] == "complete"
    assert "archive" in m
    assert m["modules"]["fake"]["artifacts"][0]["path"] == "fake/data.txt"
    # Le repertoire de travail ne doit pas survivre au run.
    assert not (snap / "tmp").exists()


@needs_bins
def test_manifest_inside_archive_has_no_archive_block(tmp_path, age_keys):
    from machine_backup.crypto import stream_member_hashes

    recipient, identity = age_keys
    run_backup([FakeModule()], _cfg(tmp_path, recipient, identity), _reporter())
    snap = resolve_snapshot(tmp_path / "backups", "latest")
    members = stream_member_hashes(snap / "snapshot.tar.zst.age", identity)
    assert "manifest.json" in members
    assert "fake/data.txt" in members


@needs_bins
def test_failed_module_gives_partial_exit_1_others_continue(tmp_path, age_keys):
    recipient, identity = age_keys
    mods = [FakeModule(name="boom", fail_collect=True), FakeModule(name="ok")]
    code = run_backup(mods, _cfg(tmp_path, recipient, identity), _reporter())

    snap = resolve_snapshot(tmp_path / "backups", "latest")
    m = read_manifest(snap / "manifest.json")
    assert code == 1
    assert m["status"] == "partial"
    assert m["modules"]["boom"]["status"] == "failed"
    # L'echec d'un module n'annule pas les autres.
    assert m["modules"]["ok"]["status"] == "ok"


@needs_bins
def test_fatal_problem_marks_skipped(tmp_path, age_keys):
    recipient, identity = age_keys
    mod = FakeModule(problems=[Problem("fake", "pg_dump introuvable")])
    run_backup([mod], _cfg(tmp_path, recipient, identity), _reporter())

    snap = resolve_snapshot(tmp_path / "backups", "latest")
    m = read_manifest(snap / "manifest.json")
    assert m["modules"]["fake"]["status"] == "skipped"
    assert "pg_dump" in m["modules"]["fake"]["error"]


@needs_bins
def test_non_fatal_problem_does_not_skip(tmp_path, age_keys):
    recipient, identity = age_keys
    mod = FakeModule(problems=[Problem("fake", "mas absent", fatal=False)])
    code = run_backup([mod], _cfg(tmp_path, recipient, identity), _reporter())
    snap = resolve_snapshot(tmp_path / "backups", "latest")
    assert code == 0
    assert read_manifest(snap / "manifest.json")["modules"]["fake"]["status"] == "ok"


@needs_bins
def test_progress_is_passed_to_modules(tmp_path, age_keys):
    """La barre par unite de travail exige que collect recoive le canal."""
    recipient, identity = age_keys

    seen = {}

    class ProgressAwareModule(FakeModule):
        def collect(self, dest, progress=None):
            seen["got_progress"] = progress is not None
            return super().collect(dest, progress=progress)

    run_backup(
        [ProgressAwareModule()], _cfg(tmp_path, recipient, identity), _reporter()
    )
    assert seen["got_progress"] is True


def test_backup_without_recipient_exits_2(tmp_path):
    cfg = Config(destination=tmp_path / "backups", age_recipient=None)
    assert run_backup([FakeModule()], cfg, _reporter()) == 2
    assert not (tmp_path / "backups").exists()


@needs_bins
def test_encryption_failure_leaves_no_partial_snapshot(tmp_path, age_keys):
    _, identity = age_keys
    cfg = _cfg(tmp_path, "pas-une-cle-valide", identity)
    code = run_backup([FakeModule()], cfg, _reporter())
    assert code == 1
    assert list((tmp_path / "backups").iterdir()) == []


def test_rotation_keeps_n_and_ignores_foreign_dirs(tmp_path):
    dest = tmp_path / "b"
    dest.mkdir()
    for i in range(9):
        d = dest / f"2026-08-0{i % 9 + 1}T00-00-0{i}"
        d.mkdir()
        (d / "manifest.json").write_text("{}")
    (dest / "pas-un-snapshot").mkdir()
    (dest / "2026-08-01T00-00-99-sans-manifest").mkdir()

    removed = rotate_snapshots(dest, keep=7)
    assert len(removed) == 2
    assert (dest / "pas-un-snapshot").exists()
    assert (dest / "2026-08-01T00-00-99-sans-manifest").exists()
    assert len(list(dest.glob("2026-*T*"))) == 8  # 7 snapshots + le dossier sans manifest


def test_rotation_noop_when_under_limit(tmp_path):
    dest = tmp_path / "b"
    dest.mkdir()
    d = dest / "2026-08-01T00-00-00"
    d.mkdir()
    (d / "manifest.json").write_text("{}")
    assert rotate_snapshots(dest, keep=7) == []


def test_resolve_snapshot_latest_exact_and_missing(tmp_path):
    dest = tmp_path / "b"
    for s in ["2026-08-04T10-00-00", "2026-08-05T10-00-00"]:
        (dest / s).mkdir(parents=True)
        (dest / s / "manifest.json").write_text("{}")

    assert resolve_snapshot(dest, "latest").name == "2026-08-05T10-00-00"
    assert resolve_snapshot(dest, "2026-08-04T10-00-00").name == "2026-08-04T10-00-00"
    assert resolve_snapshot(dest, str(dest / "2026-08-04T10-00-00")).name == (
        "2026-08-04T10-00-00"
    )
    with pytest.raises(FileNotFoundError):
        resolve_snapshot(dest, "2020-01-01T00-00-00")


def test_resolve_snapshot_latest_on_empty_destination(tmp_path):
    with pytest.raises(FileNotFoundError):
        resolve_snapshot(tmp_path / "vide", "latest")
