"""Tests for the mgf-cloud CLI (Pass 4), driven against the FakeProvider."""

from __future__ import annotations

import json
from typing import TYPE_CHECKING

import pytest

from mgf.cloud.__main__ import main
from mgf.cloud.exceptions import CredentialsError
from mgf.cloud.providers.fake import FakeProvider

if TYPE_CHECKING:
    from pathlib import Path


@pytest.fixture
def fake(monkeypatch: pytest.MonkeyPatch) -> FakeProvider:
    """A single shared FakeProvider so a create→list sequence sees state."""
    provider = FakeProvider()
    monkeypatch.setattr(
        "mgf.cloud.cli._app.get_provider",
        lambda name, credentials=None: provider,
    )
    return provider


def run(*argv: str) -> int:
    if argv and argv[0] == "version":
        return main(["version"])
    return main([*argv, "--provider", "fake"])


def test_version(capsys: pytest.CaptureFixture[str]) -> None:
    assert main(["version"]) == 0
    assert "mgf-cloud" in capsys.readouterr().out


def test_provider_list(capsys: pytest.CaptureFixture[str]) -> None:
    assert main(["provider", "list"]) == 0
    out = capsys.readouterr().out
    assert "fake" in out
    assert "infomaniak" in out


def test_provider_check_json(fake: FakeProvider, capsys: pytest.CaptureFixture[str]) -> None:
    assert run("provider", "check", "--json") == 0
    data = json.loads(capsys.readouterr().out)
    assert data["ok"] is True
    assert data["provider"] == "fake"


def test_vm_create_list_show_delete(fake: FakeProvider, capsys: pytest.CaptureFixture[str]) -> None:
    assert (
        run(
            "vm",
            "create",
            "web",
            "--flavor",
            "a2-ram2-disk20",
            "--image",
            "Ubuntu 24.04 LTS",
            "--floating-ip",
            "--secgroup",
            "default",
            "--owner",
            "federation",
        )
        == 0
    )
    capsys.readouterr()
    assert run("vm", "list", "--json") == 0
    servers = json.loads(capsys.readouterr().out)
    assert servers[0]["name"] == "web"
    assert servers[0]["floating_ips"]
    assert servers[0]["metadata"]["mgf:owner"] == "federation"

    assert run("vm", "show", "web") == 0
    capsys.readouterr()

    assert run("vm", "delete", "web") == 2  # refused without --yes
    assert run("vm", "delete", "web", "--yes") == 0
    assert fake.list_servers() == []


def test_vm_dry_run_creates_nothing(fake: FakeProvider, capsys: pytest.CaptureFixture[str]) -> None:
    assert run("vm", "create", "db", "--flavor", "a2-ram2-disk20", "--image", "x", "--dry-run") == 0
    assert "[dry-run]" in capsys.readouterr().out
    assert fake.list_servers() == []


def test_cost_reports_rate_and_breakdown(
    fake: FakeProvider, capsys: pytest.CaptureFixture[str]
) -> None:
    assert run("vm", "create", "web", "--flavor", "a2-ram4-disk40", "--image", "x") == 0
    capsys.readouterr()
    assert run("cost") == 0
    out = capsys.readouterr().out
    assert "cost" in out.lower()
    assert "/month" in out and "web" in out
    assert "verify against your invoice" in out  # the estimate disclaimer


def test_cost_json_has_rates(fake: FakeProvider, capsys: pytest.CaptureFixture[str]) -> None:
    assert run("vm", "create", "web", "--flavor", "a2-ram4-disk40", "--image", "x") == 0
    capsys.readouterr()
    assert run("cost", "--json") == 0
    data = json.loads(capsys.readouterr().out)
    assert data["currency"] == "EUR"
    assert data["rate_hourly"] > 0
    # monthly is the hourly run-rate projected over a month (values are rounded
    # for display, so allow a small tolerance).
    assert data["rate_monthly"] == pytest.approx(data["rate_hourly"] * 730.5, rel=1e-2)
    assert any(line["kind"] == "server" for line in data["lines"])


def test_cost_no_resources(fake: FakeProvider, capsys: pytest.CaptureFixture[str]) -> None:
    assert run("cost", "--compute-only") == 0
    assert "no billable resources" in capsys.readouterr().out.lower()


def test_vm_reboot_resize_snapshot(fake: FakeProvider, capsys: pytest.CaptureFixture[str]) -> None:
    run("vm", "create", "h", "--flavor", "a2-ram2-disk20", "--image", "Ubuntu 24.04 LTS")
    capsys.readouterr()
    assert run("vm", "reboot", "h", "--hard") == 0
    assert run("vm", "resize", "h", "--flavor", "a2-ram4-disk40") == 0
    assert fake.get_server("h").flavor == "a2-ram4-disk40"
    assert run("vm", "snapshot", "h", "h-snap") == 0


def test_vm_show_missing_returns_1(fake: FakeProvider, capsys: pytest.CaptureFixture[str]) -> None:
    assert run("vm", "show", "ghost") == 1
    assert "mgf-cloud:" in capsys.readouterr().err


def test_flavor_and_image_list(fake: FakeProvider, capsys: pytest.CaptureFixture[str]) -> None:
    assert run("flavor", "list", "--json") == 0
    assert any(f["name"] == "a2-ram2-disk20" for f in json.loads(capsys.readouterr().out))
    assert run("image", "list", "--json") == 0
    assert any(i["name"] == "Ubuntu 24.04 LTS" for i in json.loads(capsys.readouterr().out))


def test_volume_lifecycle(fake: FakeProvider, capsys: pytest.CaptureFixture[str]) -> None:
    run("vm", "create", "vmx", "--flavor", "a2-ram2-disk20", "--image", "Ubuntu 24.04 LTS")
    assert run("volume", "create", "data", "--size", "50") == 0
    assert run("volume", "attach", "data", "--server", "vmx") == 0
    assert fake.get_volume("data").attached_server_id is not None
    assert run("volume", "detach", "data", "--server", "vmx") == 0
    assert run("volume", "snapshot", "data", "data-snap") == 0
    assert run("volume", "list", "--json") == 0
    capsys.readouterr()
    assert run("volume", "delete", "data") == 2  # no --yes
    assert run("volume", "delete", "data", "--yes") == 0


def test_floating_ip_lifecycle(fake: FakeProvider, capsys: pytest.CaptureFixture[str]) -> None:
    run("vm", "create", "vmf", "--flavor", "a2-ram2-disk20", "--image", "Ubuntu 24.04 LTS")
    capsys.readouterr()
    assert run("floating-ip", "create", "--network", "ext-net1", "--json") == 0
    addr = json.loads(capsys.readouterr().out)["address"]
    assert run("floating-ip", "attach", addr, "--server", "vmf") == 0
    assert run("floating-ip", "list") == 0
    assert run("floating-ip", "detach", addr) == 0
    assert run("floating-ip", "release", addr) == 2  # no --yes
    assert run("floating-ip", "release", addr, "--yes") == 0


def test_secgroup_and_keypair(
    fake: FakeProvider, tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
    assert run("secgroup", "create", "ci", "--description", "ci", "--rule", "tcp:22", "--json") == 0
    group = json.loads(capsys.readouterr().out)
    assert group["rules"][0]["port_min"] == 22
    assert run("secgroup", "list") == 0
    assert run("secgroup", "delete", "ci", "--yes") == 0

    keyfile = tmp_path / "id.pub"
    keyfile.write_text("ssh-ed25519 AAAA test\n", encoding="utf-8")
    assert run("keypair", "create", "k1", "--public-key-file", str(keyfile)) == 0
    assert run("keypair", "list") == 0
    assert run("keypair", "delete", "k1", "--yes") == 0


def test_object_storage_roundtrip(
    fake: FakeProvider, tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
    src = tmp_path / "a.txt"
    src.write_bytes(b"hello")
    assert run("object", "mkcontainer", "backups") == 0
    assert run("object", "put", "backups", "a.txt", "--file", str(src)) == 0
    capsys.readouterr()
    assert run("object", "ls", "backups", "--json") == 0
    assert json.loads(capsys.readouterr().out)[0]["name"] == "a.txt"
    dest = tmp_path / "out.txt"
    assert run("object", "get", "backups", "a.txt", "--file", str(dest)) == 0
    assert dest.read_bytes() == b"hello"
    assert run("object", "containers", "--json") == 0
    assert run("object", "rm", "backups", "a.txt", "--yes") == 0
    assert run("object", "rmcontainer", "backups", "--yes") == 0


def test_unknown_provider_errors(capsys: pytest.CaptureFixture[str]) -> None:
    # not using the fake fixture: real resolution path → ProviderNotFoundError → rc 1
    assert main(["vm", "list", "--provider", "bogus"]) == 1
    assert "mgf-cloud:" in capsys.readouterr().err


def test_cloud_flag_threads_to_config_load(monkeypatch: pytest.MonkeyPatch) -> None:
    # --cloud picks the clouds.yaml entry (Infomaniak names it PCP-<id>-<region>,
    # not 'openstack'), so it must reach config.load(cloud=...).
    seen: dict[str, object] = {}

    def fake_load(clouds_yaml: object = None, cloud: str = "openstack", **_: object) -> object:
        seen["clouds_yaml"] = clouds_yaml
        seen["cloud"] = cloud
        # what config.load really raises when the entry is absent; the CLI maps it to rc 1.
        raise CredentialsError("stop after capture")

    monkeypatch.setattr("mgf.cloud.cli._app.config.load", fake_load)
    rc = main(
        [
            "vm",
            "list",
            "--provider",
            "infomaniak",
            "--clouds-yaml",
            "/tmp/clouds.yaml",
            "--cloud",
            "PCP-HOESFJV-dc3-a",
        ]
    )
    assert rc == 1  # the RuntimeError is caught + reported as a failure
    assert seen == {"clouds_yaml": "/tmp/clouds.yaml", "cloud": "PCP-HOESFJV-dc3-a"}


def test_untyped_error_routed_to_crash_report(
    monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
    """EH-04: an untyped bug is routed to report_now + an actionable message and a
    non-zero exit — never a raw traceback."""
    captured: dict[str, object] = {}

    def fake_report(exc: BaseException, *, source: str) -> str:
        captured["exc"] = exc
        captured["source"] = source
        return "/tmp/crash.json"

    def boom(name: str, credentials: object = None) -> object:
        raise ValueError("an untyped bug")

    monkeypatch.setattr("mgf.cloud.cli._app.report_now", fake_report)
    monkeypatch.setattr("mgf.cloud.cli._app.get_provider", boom)
    rc = main(["vm", "list", "--provider", "fake"])
    assert rc == 1
    assert isinstance(captured["exc"], ValueError)
    assert "unexpected error" in capsys.readouterr().err
