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

from __future__ import annotations

from typing import TYPE_CHECKING

import pytest

import mgf.vm.core.lifecycle.adopt as adopt_mod
from mgf.vm.core.common.progress import RecordingReporter
from mgf.vm.core.contracts import DomainInfo
from mgf.vm.core.domain import VMStatus
from mgf.vm.core.exceptions import (
    DomainNotFound,
    HypervisorTransientError,
    OperationError,
)
from mgf.vm.core.ids import DomainId
from mgf.vm.core.lifecycle.adopt import DiskExistenceCheck, DomainResolver
from mgf.vm.core.monitor.state import StateStore

if TYPE_CHECKING:
    from collections.abc import Sequence


class _FakeResolver:
    """In-memory by-name resolver + disk-path enumeration."""

    def __init__(
        self,
        *,
        info: DomainInfo | None = None,
        disks: Sequence[str] = (),
        resolve_error: Exception | None = None,
        disk_error: Exception | None = None,
    ) -> None:
        self._info = info
        self._disks = list(disks)
        self._resolve_error = resolve_error
        self._disk_error = disk_error

    def resolve_by_name(self, name: str) -> tuple[DomainId, DomainInfo]:
        if self._resolve_error is not None:
            raise self._resolve_error
        assert self._info is not None
        return DomainId(f"id-{name}"), self._info

    def disk_paths(self, domain: DomainId) -> Sequence[str]:
        if self._disk_error is not None:
            raise self._disk_error
        return self._disks


class _FakeExistence:
    """Existence check where ``present`` lists the paths that exist."""

    def __init__(self, present: Sequence[str]) -> None:
        self._present = set(present)
        self.checked: list[str] = []

    def disk_exists(self, path: str) -> bool:
        self.checked.append(path)
        return path in self._present


def _info(name: str = "vm1", *, running: bool = False) -> DomainInfo:
    return DomainInfo(
        domain_id=DomainId(f"id-{name}"),
        name=name,
        cpu=2,
        memory_bytes=1 << 30,
        is_running=running,
    )


def test_seams_are_structural() -> None:
    resolver = _FakeResolver(info=_info())
    existence = _FakeExistence([])
    assert isinstance(resolver, DomainResolver)
    assert isinstance(existence, DiskExistenceCheck)


def test_adopt_registers_in_store_all_disks_present() -> None:
    resolver = _FakeResolver(info=_info("vm1"), disks=["/a.qcow2", "/b.qcow2"])
    existence = _FakeExistence(["/a.qcow2", "/b.qcow2"])
    store = StateStore()
    reporter = RecordingReporter()

    report = adopt_mod.adopt(resolver, existence, store, "vm1", reporter=reporter)

    assert report.missing_disks == []
    assert report.disk_paths == ["/a.qcow2", "/b.qcow2"]
    assert report.handle.name == "vm1"
    assert report.handle.uuid == "id-vm1"
    assert report.handle.state is VMStatus.STOPPED
    assert store.get("vm1") is not None
    assert reporter.calls[-1][0] == "vm1: adopted"


def test_adopt_running_domain_handle_state() -> None:
    resolver = _FakeResolver(info=_info("vm1", running=True), disks=[])
    existence = _FakeExistence([])
    store = StateStore()

    report = adopt_mod.adopt(resolver, existence, store, "vm1")

    assert report.handle.state is VMStatus.RUNNING


def test_adopt_reports_missing_disks_but_still_succeeds() -> None:
    resolver = _FakeResolver(info=_info("vm1"), disks=["/a.qcow2", "/gone.qcow2"])
    existence = _FakeExistence(["/a.qcow2"])
    store = StateStore()
    reporter = RecordingReporter()

    report = adopt_mod.adopt(resolver, existence, store, "vm1", reporter=reporter)

    assert report.missing_disks == ["/gone.qcow2"]
    assert store.get("vm1") is not None
    assert reporter.calls[-1][0] == "vm1: adopted with 1 missing disk file(s)"


def test_adopt_missing_domain_raises_not_found() -> None:
    resolver = _FakeResolver(resolve_error=DomainNotFound("ghost"))
    existence = _FakeExistence([])

    with pytest.raises(DomainNotFound):
        adopt_mod.adopt(resolver, existence, StateStore(), "ghost")


def test_adopt_resolve_provider_error_translated() -> None:
    resolver = _FakeResolver(resolve_error=HypervisorTransientError("lookup failed"))
    existence = _FakeExistence([])

    with pytest.raises(OperationError) as ei:
        adopt_mod.adopt(resolver, existence, StateStore(), "vm1")
    assert "could not look up" in str(ei.value)


def test_adopt_disk_paths_provider_error_translated() -> None:
    resolver = _FakeResolver(
        info=_info("vm1"), disk_error=HypervisorTransientError("disk read failed")
    )
    existence = _FakeExistence([])

    with pytest.raises(OperationError) as ei:
        adopt_mod.adopt(resolver, existence, StateStore(), "vm1")
    assert "could not read disk paths" in str(ei.value)


def test_adopt_inserts_handle_into_store_idempotently() -> None:
    resolver = _FakeResolver(info=_info("vm1"), disks=[])
    existence = _FakeExistence([])
    store = StateStore()

    adopt_mod.adopt(resolver, existence, store, "vm1")
    adopt_mod.adopt(resolver, existence, store, "vm1")  # second adopt = update

    assert len(store) == 1
