"""Behaviour tests for ``mgf.vm.core.lifecycle.power``.

Drives the pure orchestration with the first-class
:class:`~mgf.vm.core.fakes.hypervisor.FakeHypervisorBackend`, plus a
deliberately-broken backend to assert the typed error translation.
"""

from __future__ import annotations

import pytest

from mgf.vm.core.cancellation import CancellationEvent
from mgf.vm.core.common.progress import RecordingReporter
from mgf.vm.core.contracts import VmSpec
from mgf.vm.core.exceptions import (
    DomainNotFound,
    HypervisorTransientError,
    OperationCancelled,
    OperationError,
)
from mgf.vm.core.fakes.hypervisor import FakeHypervisorBackend
from mgf.vm.core.ids import DomainId
from mgf.vm.core.lifecycle import power


def _defined(backend: FakeHypervisorBackend, name: str = "vm1") -> DomainId:
    return backend.define(VmSpec(name=name, cpu=2, memory_bytes=1 << 30))


class _BrokenBackend:
    """Minimal backend whose power methods raise a transient provider error."""

    def __init__(self) -> None:
        self.raise_not_found = False

    def _boom(self) -> None:
        if self.raise_not_found:
            raise DomainNotFound("gone")
        raise HypervisorTransientError("engine hiccup")

    def start(self, d: DomainId, *, token: object | None = None) -> None:
        self._boom()

    def stop(self, d: DomainId, *, force: bool = False, token: object | None = None) -> None:
        self._boom()

    def pause(self, d: DomainId) -> None:
        self._boom()

    def resume(self, d: DomainId) -> None:
        self._boom()

    def reboot(self, d: DomainId, *, force: bool = False, token: object | None = None) -> None:
        self._boom()


def test_start_marks_domain_running_and_reports() -> None:
    backend = FakeHypervisorBackend()
    d = _defined(backend)
    reporter = RecordingReporter()

    power.start(backend, d, reporter=reporter)

    assert backend.state[d].is_running is True
    msgs = [c[0] for c in reporter.calls]
    assert msgs == [f"starting {d.value}", f"{d.value} started"]


def test_start_is_idempotent() -> None:
    backend = FakeHypervisorBackend()
    d = _defined(backend)
    power.start(backend, d)
    power.start(backend, d)
    assert backend.state[d].is_running is True


def test_stop_graceful_stops_running_domain() -> None:
    backend = FakeHypervisorBackend()
    d = _defined(backend)
    power.start(backend, d)

    power.stop(backend, d)

    assert backend.state[d].is_running is False


def test_stop_graceful_unresponsive_translated_to_operation_error() -> None:
    backend = FakeHypervisorBackend(graceful_stop_succeeds=False)
    d = _defined(backend)
    power.start(backend, d)

    with pytest.raises(OperationError) as ei:
        power.stop(backend, d, force=False)
    assert "could not stop" in str(ei.value)


def test_stop_force_succeeds_when_graceful_would_fail() -> None:
    backend = FakeHypervisorBackend(graceful_stop_succeeds=False)
    d = _defined(backend)
    power.start(backend, d)

    power.stop(backend, d, force=True)

    assert backend.state[d].is_running is False


def test_pause_then_resume_round_trips() -> None:
    backend = FakeHypervisorBackend()
    d = _defined(backend)
    power.start(backend, d)

    power.pause(backend, d)
    assert backend.state[d].is_paused is True

    power.resume(backend, d)
    assert backend.state[d].is_paused is False


def test_pause_non_running_translated() -> None:
    backend = FakeHypervisorBackend()
    d = _defined(backend)  # not started

    with pytest.raises(OperationError) as ei:
        power.pause(backend, d)
    assert "could not pause" in str(ei.value)


def test_reboot_running_domain_reports() -> None:
    backend = FakeHypervisorBackend()
    d = _defined(backend)
    power.start(backend, d)
    reporter = RecordingReporter()

    power.reboot(backend, d, reporter=reporter)

    msgs = [c[0] for c in reporter.calls]
    assert msgs == [f"rebooting {d.value}", f"{d.value} rebooted"]


def test_reboot_force_message() -> None:
    backend = FakeHypervisorBackend()
    d = _defined(backend)
    power.start(backend, d)
    reporter = RecordingReporter()

    power.reboot(backend, d, force=True, reporter=reporter)

    assert reporter.calls[0][0] == f"force-rebooting {d.value}"


def test_reboot_non_running_translated() -> None:
    backend = FakeHypervisorBackend()
    d = _defined(backend)

    with pytest.raises(OperationError):
        power.reboot(backend, d)


def test_cancellation_token_propagates_to_start() -> None:
    backend = FakeHypervisorBackend()
    d = _defined(backend)
    token = CancellationEvent()
    token.cancel()

    with pytest.raises(OperationCancelled):
        power.start(backend, d, token=token)


def test_cancellation_token_propagates_to_stop() -> None:
    backend = FakeHypervisorBackend()
    d = _defined(backend)
    power.start(backend, d)
    token = CancellationEvent()
    token.cancel()

    with pytest.raises(OperationCancelled):
        power.stop(backend, d, token=token)


def test_cancellation_token_propagates_to_reboot() -> None:
    backend = FakeHypervisorBackend()
    d = _defined(backend)
    power.start(backend, d)
    token = CancellationEvent()
    token.cancel()

    with pytest.raises(OperationCancelled):
        power.reboot(backend, d, token=token)


@pytest.mark.parametrize("op", ["start", "stop", "pause", "resume", "reboot"])
def test_domain_not_found_passes_through_unwrapped(op: str) -> None:
    backend = _BrokenBackend()
    backend.raise_not_found = True
    d = DomainId("missing")
    fn = getattr(power, op)

    with pytest.raises(DomainNotFound):
        fn(backend, d)


@pytest.mark.parametrize("op", ["start", "stop", "pause", "resume", "reboot"])
def test_provider_error_translated_to_operation_error(op: str) -> None:
    backend = _BrokenBackend()
    d = DomainId("vm1")
    fn = getattr(power, op)

    with pytest.raises(OperationError) as ei:
        fn(backend, d)
    assert "engine hiccup" in str(ei.value)
