import hashlib
import re
import shutil
import subprocess
from dataclasses import dataclass, field
from pathlib import Path

import pytest

from machine_backup.modules.base import (
    Action,
    ActionState,
    Artifact,
    ModuleResult,
    ModuleStatus,
    Problem,
    Progress,
)

# Les tests qui manipulent de vraies archives ont besoin de la chaine complete.
needs_bins = pytest.mark.skipif(
    shutil.which("age") is None
    or shutil.which("age-keygen") is None
    or shutil.which("zstd") is None,
    reason="age / age-keygen / zstd absents",
)


@pytest.fixture
def age_keys(tmp_path):
    """Paire de cles age jetable, propre a chaque test."""
    identity = tmp_path / "identity.txt"
    proc = subprocess.run(
        ["age-keygen", "-o", str(identity)], capture_output=True, text=True
    )
    match = re.search(r"age1[0-9a-z]+", proc.stderr)
    assert match, f"cle publique introuvable dans : {proc.stderr}"
    return match.group(0), identity


class RecordingProgress:
    """Progression factice : enregistre ce qu'un module lui annonce."""

    def __init__(self) -> None:
        self.total: int | None = None
        self.steps: list[str] = []

    def set_total(self, total: int) -> None:
        self.total = total

    def step(self, label: str = "") -> None:
        self.steps.append(label)


@dataclass
class FakeModule:
    name: str = "fake"
    problems: list = field(default_factory=list)
    fail_collect: bool = False
    fail_restore: bool = False
    restored: list = field(default_factory=list)

    def check(self) -> list[Problem]:
        return self.problems

    def collect(self, dest: Path, progress: Progress | None = None) -> ModuleResult:
        if self.fail_collect:
            raise RuntimeError("boom")
        if progress is not None:
            progress.set_total(1)
        dest.mkdir(parents=True, exist_ok=True)
        data = b"payload"
        (dest / "data.txt").write_bytes(data)
        if progress is not None:
            progress.step("data.txt")
        art = Artifact(
            path=f"{self.name}/data.txt",
            sha256=hashlib.sha256(data).hexdigest(),
            size=len(data),
        )
        return ModuleResult(self.name, ModuleStatus.OK, artifacts=[art], duration_s=0.01)

    def plan_restore(self, src: Path) -> list[Action]:
        return [
            Action(self.name, ActionState.SATISFIED, "deja conforme"),
            Action(self.name, ActionState.ADDITIVE, "recreer data.txt"),
            Action(
                self.name,
                ActionState.DESTRUCTIVE,
                "ecraser existing.txt",
                reason="diverge",
            ),
        ]

    def restore(self, src: Path, actions, force: bool) -> ModuleResult:
        if self.fail_restore:
            raise RuntimeError("restore boom")
        for a in actions:
            if a.state is ActionState.ADDITIVE or (
                a.state is ActionState.DESTRUCTIVE and force
            ):
                self.restored.append(a.description)
        return ModuleResult(self.name, ModuleStatus.OK)


def make_runner(responses: dict | None = None):
    """Runner factice : enregistre les argv et rejoue des sorties preparees.

    La cle est le nom du binaire (`psql`, `pg_dump`, `brew`…), ce qui evite de
    dependre du chemin absolu resolu sur la machine de test.
    """
    from machine_backup.logs import CommandResult

    responses = responses or {}
    calls: list[list[str]] = []

    def runner(argv, **kwargs):
        argv = [str(a) for a in argv]
        calls.append(argv)
        key = Path(argv[0]).name
        canned = responses.get(key)
        if canned is None:
            return CommandResult(argv=argv, returncode=0)
        return CommandResult(
            argv=argv,
            returncode=canned.returncode,
            stdout=canned.stdout,
            stderr=canned.stderr,
        )

    runner.calls = calls  # type: ignore[attr-defined]
    return runner


@pytest.fixture
def fake_module():
    return FakeModule()


@pytest.fixture
def recording_progress():
    return RecordingProgress()
