from pathlib import Path

import pytest
from rich.console import Console

from osx_backup.context import Context
from osx_backup.modules.base import Inventory, Module, ModuleResult, ProgressReporter
from osx_backup.ui import (
    RichProgress,
    checklist,
    confirm,
    inventory_table,
    plan_table,
    summary_table,
)


class Fake(Module):
    name = "fake"
    description = "module factice"
    order = 1

    def inventory(self, ctx: Context) -> Inventory:
        return Inventory()

    def backup(self, ctx: Context, progress: ProgressReporter) -> ModuleResult:
        return ModuleResult.ok()

    def restore(self, ctx: Context, progress: ProgressReporter) -> ModuleResult:
        return ModuleResult.ok()


class Other(Fake):
    name = "other"
    description = "autre"
    order = 2


class Broken(Fake):
    name = "broken"
    description = "cassé"
    order = 3


MODULES = [Fake(), Other(), Broken()]
INVENTORIES = {
    "fake": Inventory(items=3, size_bytes=2048),
    "other": Inventory(items=1, size_bytes=10, warnings=["un avertissement"]),
    "broken": Inventory(available=False, reason="binaire absent"),
}


def test_checklist_returns_in_module_order_and_disables_unavailable() -> None:
    captured = {}

    def ask(choices):
        captured["choices"] = choices
        return ["other", "fake"]

    assert checklist(MODULES, INVENTORIES, None, ask) == ["fake", "other"]
    by_value = {c.value: c for c in captured["choices"]}
    assert by_value["broken"].disabled == "binaire absent"
    assert by_value["fake"].checked and by_value["other"].checked
    assert "2.0 KB" in by_value["fake"].title


def test_checklist_preselection() -> None:
    captured = {}

    def ask(choices):
        captured["choices"] = choices
        return []

    checklist(MODULES, INVENTORIES, {"other"}, ask)
    by_value = {c.value: c for c in captured["choices"]}
    assert not by_value["fake"].checked and by_value["other"].checked


def test_checklist_cancel_raises() -> None:
    with pytest.raises(KeyboardInterrupt):
        checklist(MODULES, INVENTORIES, None, lambda _c: None)


def test_confirm_injectable() -> None:
    assert confirm("ok ?", lambda _m: True) is True
    assert confirm("ok ?", lambda _m: False) is False


def test_confirm_ctrl_c_raises() -> None:
    """Ctrl-C au prompt n'est pas un « non » : même contrat que checklist."""
    with pytest.raises(KeyboardInterrupt):
        confirm("ok ?", lambda _m: None)


def _render(renderable) -> str:
    console = Console(record=True, width=160)
    console.print(renderable)
    return console.export_text()


def test_inventory_table() -> None:
    text = _render(inventory_table(MODULES, INVENTORIES))
    assert "fake" in text and "2.0 KB" in text
    assert "binaire absent" in text and "un avertissement" in text


def test_plan_table() -> None:
    text = _render(plan_table("backup", ["fake"], INVENTORIES, Path("/Volumes/X")))
    assert "/Volumes/X/fake" in text and "sauvegarde" in text
    text2 = _render(plan_table("restore", ["fake"], INVENTORIES, Path("/Volumes/X")))
    assert "restauration" in text2


def test_summary_table() -> None:
    results = {
        "fake": ModuleResult.ok("bien", duration_s=1.5, size_bytes=100),
        "other": ModuleResult.failed("boom", warnings=["w1", "w2"]),
        "broken": ModuleResult.skipped("rien"),
    }
    text = _render(summary_table(results))
    assert "ok" in text and "failed" in text and "skipped" in text
    assert "boom" in text and "2" in text


def test_rich_progress_tasks() -> None:
    console = Console(record=True, width=80)
    with RichProgress(console) as progress:
        task = progress.task("copie", total=4)
        task.advance()
        task.advance(2)
        task.set_completed(4)
        task.finish()
        spinner = progress.task("brew bundle", total=None)
        spinner.set_total(1)
        spinner.finish()
    assert progress.progress.tasks[0].completed == 4
    assert progress.progress.tasks[1].finished


def test_summary_table_escapes_external_markup() -> None:
    results = {
        "fake": ModuleResult.failed(
            "stderr : [/bin/x] not found",
            warnings=["rsync: [sender] send_files failed"],
        )
    }
    text = _render(summary_table(results))
    assert "stderr : [/bin/x] not found" in text
    assert "rsync: [sender] send_files failed" in text


def test_inventory_table_escapes_external_markup() -> None:
    inventories = {
        "fake": Inventory(available=False, reason="binaire [/usr/bin/x] absent"),
        "other": Inventory(items=1, warnings=["rsync: [sender] send_files failed"]),
    }
    text = _render(inventory_table([Fake(), Other()], inventories))
    assert "binaire [/usr/bin/x] absent" in text
    assert "rsync: [sender] send_files failed" in text


def test_rich_task_finish_after_another_task_removed() -> None:
    console = Console(record=True, width=80)
    with RichProgress(console) as progress:
        progress.task("premier", total=2)
        second = progress.task("second", total=3)
        progress.progress.remove_task(progress.progress.task_ids[0])
        second.advance()
        second.finish()
    assert progress.progress.tasks[0].finished
    assert progress.progress.tasks[0].completed == 3


def test_plan_table_escapes_destination_path() -> None:
    text = _render(plan_table("backup", ["fake"], INVENTORIES, Path("/Volumes/[X]")))
    assert "/Volumes/[X]/fake" in text
