"""The conformance-suite runner.

Each suite is a callable bundle of scenarios. Adapters get a single
entry point (``hypervisor_conformance_suite(adapter)``) returning a
suite object whose ``run_all()`` method executes the catalog and
raises ``AssertionError`` on the first failure (consistent with
pytest's default expectation).

Individual scenarios can also be run by ID:
``suite.run_one("CONF-H-01")`` — useful when debugging a single
failure.

Adding a new scenario:
    1. Implement the function in ``hypervisor.py`` / ``storage.py`` /
       ``device.py`` / ``cross.py``.
    2. Register it in the corresponding ``_*_CATALOG`` dict below.
    3. The suite's ``run_all`` and ``run_one`` pick it up
       automatically.
"""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass, field
from functools import partial
from typing import TYPE_CHECKING

from mgf.vm.core.conformance import device as _device_scenarios
from mgf.vm.core.conformance import disk as _disk_scenarios
from mgf.vm.core.conformance import guest_agent as _guest_agent_scenarios
from mgf.vm.core.conformance import hypervisor as _hypervisor_scenarios
from mgf.vm.core.conformance import metrics as _metrics_scenarios
from mgf.vm.core.conformance import network as _network_scenarios
from mgf.vm.core.conformance import storage as _storage_scenarios

if TYPE_CHECKING:
    from collections.abc import Mapping

    from mgf.vm.core.contracts import (
        DeviceBackend,
        GuestAgentBackend,
        HostDiskBackend,
        HypervisorBackend,
        MetricsBackend,
        NetworkBackend,
        StorageBackend,
    )
    from mgf.vm.core.ids import DomainId


# A scenario is a callable that takes the adapter and either runs
# its assertions (success) or raises (failure).
_Scenario = Callable[..., None]


@dataclass(slots=True)
class ConformanceResult:
    """Outcome of running a single scenario."""

    scenario_id: str
    passed: bool
    skipped: bool = False
    skip_reason: str = ""
    error: BaseException | None = None


@dataclass(slots=True)
class ConformanceSuite:
    """Base class for the per-Protocol suites.

    Wraps a catalog of scenarios + an adapter under test.
    """

    adapter: object
    catalog: dict[str, _Scenario]
    results: list[ConformanceResult] = field(default_factory=list)

    def run_one(self, scenario_id: str) -> ConformanceResult:
        """Run a single scenario by ID; record + return the result."""
        if scenario_id not in self.catalog:
            msg = f"unknown scenario {scenario_id!r}; known: {sorted(self.catalog)}"
            raise KeyError(msg)
        scenario = self.catalog[scenario_id]
        try:
            scenario(self.adapter)
        except _SkipScenario as skip:
            result = ConformanceResult(
                scenario_id=scenario_id,
                passed=True,
                skipped=True,
                skip_reason=str(skip),
            )
        except BaseException as exc:
            result = ConformanceResult(
                scenario_id=scenario_id,
                passed=False,
                error=exc,
            )
        else:
            result = ConformanceResult(scenario_id=scenario_id, passed=True)
        self.results.append(result)
        return result

    def run_all(self) -> list[ConformanceResult]:
        """Run the whole catalog. Raises the first failure's exception
        once the full catalog has been recorded — so a maintainer can
        always see ``self.results`` post-mortem."""
        first_failure: BaseException | None = None
        for scenario_id in sorted(self.catalog):
            result = self.run_one(scenario_id)
            if not result.passed and first_failure is None:
                first_failure = result.error
        if first_failure is not None:
            raise first_failure
        return list(self.results)

    def summary(self) -> str:
        """One-line human summary."""
        passed = sum(1 for r in self.results if r.passed and not r.skipped)
        skipped = sum(1 for r in self.results if r.skipped)
        failed = sum(1 for r in self.results if not r.passed)
        return f"{passed} passed, {skipped} skipped, {failed} failed ({len(self.results)} total)"


class _SkipScenario(Exception):
    """Raised by a scenario when the adapter's capabilities don't
    support the feature under test. Catalog scenarios use this via
    the ``skip_unless_capability`` helper in
    ``mgf.vm.core.conformance._helpers``.
    """


# ---------------------------------------------------------------------------
# Per-Protocol catalogs
# ---------------------------------------------------------------------------

_HYPERVISOR_CATALOG: dict[str, _Scenario] = {
    "CONF-H-01": _hypervisor_scenarios.define_start_stop_undefine_round_trip,
    "CONF-H-02": _hypervisor_scenarios.define_idempotency,
    "CONF-H-03": _hypervisor_scenarios.start_idempotency,
    "CONF-H-11": _hypervisor_scenarios.network_observation,
    "CONF-H-12": _hypervisor_scenarios.rename_round_trip,
}

_STORAGE_CATALOG: dict[str, _Scenario] = {
    "CONF-S-01": _storage_scenarios.create_list_delete_volume_round_trip,
    "CONF-S-03": _storage_scenarios.volume_with_snapshots_refuses_delete,
}

_DEVICE_CATALOG: dict[str, _Scenario] = {
    "CONF-D-01": _device_scenarios.claim_assignment_release_round_trip,
    "CONF-D-04": _device_scenarios.fenced_lease_assignment_raises,
}

# The metrics catalog is built per-suite (it needs a known DomainId bound
# into the scenario) — see ``metrics_conformance_suite``.

# The host-disk catalog is built per-suite too (it needs the seed source
# images + a workdir bound in) — see ``host_disk_conformance_suite``.

# The network catalog is built per-suite too (it needs the observing
# HypervisorBackend bound in) — see ``network_conformance_suite``.


# ---------------------------------------------------------------------------
# Per-Protocol suite subclasses (typed-adapter aliases)
# ---------------------------------------------------------------------------


class HypervisorConformanceSuite(ConformanceSuite):
    """Conformance suite over a ``HypervisorBackend`` instance."""


class StorageConformanceSuite(ConformanceSuite):
    """Conformance suite over a ``StorageBackend`` instance."""


class DeviceConformanceSuite(ConformanceSuite):
    """Conformance suite over a ``DeviceBackend`` instance."""


class MetricsConformanceSuite(ConformanceSuite):
    """Conformance suite over an OPTIONAL ``MetricsBackend`` instance (D-G13)."""


class GuestAgentConformanceSuite(ConformanceSuite):
    """Conformance suite over an OPTIONAL ``GuestAgentBackend`` instance (D-G19)."""


class HostDiskConformanceSuite(ConformanceSuite):
    """Conformance suite over an OPTIONAL ``HostDiskBackend`` instance (D-G18)."""


class NetworkConformanceSuite(ConformanceSuite):
    """Conformance suite over an OPTIONAL ``NetworkBackend`` instance (D-G15)."""


# ---------------------------------------------------------------------------
# Factory functions — the public entry points
# ---------------------------------------------------------------------------


def hypervisor_conformance_suite(
    adapter: HypervisorBackend,
) -> HypervisorConformanceSuite:
    """Build a conformance suite for the given hypervisor adapter."""
    return HypervisorConformanceSuite(adapter=adapter, catalog=_HYPERVISOR_CATALOG)


def storage_conformance_suite(adapter: StorageBackend) -> StorageConformanceSuite:
    """Build a conformance suite for the given storage adapter."""
    return StorageConformanceSuite(adapter=adapter, catalog=_STORAGE_CATALOG)


def device_conformance_suite(adapter: DeviceBackend) -> DeviceConformanceSuite:
    """Build a conformance suite for the given device adapter."""
    return DeviceConformanceSuite(adapter=adapter, catalog=_DEVICE_CATALOG)


def metrics_conformance_suite(
    adapter: MetricsBackend,
    *,
    known_domain: DomainId,
) -> MetricsConformanceSuite:
    """Build a conformance suite for the given OPTIONAL metrics adapter.

    ``known_domain`` is a domain the adapter can sample — the caller arranges
    it (the Fake registers it; a real adapter passes a live domain). It is
    bound into the CONF-M-01 scenario so the runner's uniform
    ``scenario(adapter)`` call still applies.
    """
    catalog: dict[str, _Scenario] = {
        "CONF-M-01": partial(
            _metrics_scenarios.sample_metrics_observation,
            known=known_domain,
        ),
    }
    return MetricsConformanceSuite(adapter=adapter, catalog=catalog)


def guest_agent_conformance_suite(
    adapter: GuestAgentBackend,
    *,
    known_domain: DomainId,
) -> GuestAgentConformanceSuite:
    """Build a conformance suite for the given OPTIONAL guest-agent adapter.

    ``known_domain`` is a domain whose in-guest agent the adapter can reach —
    the caller arranges it (the Fake seeds it reachable; a real adapter passes
    a live agent-running domain). It is bound into the CONF-G-01 scenario so
    the runner's uniform ``scenario(adapter)`` call still applies.
    """
    catalog: dict[str, _Scenario] = {
        "CONF-G-01": partial(
            _guest_agent_scenarios.guest_observation,
            known=known_domain,
        ),
    }
    return GuestAgentConformanceSuite(adapter=adapter, catalog=catalog)


def host_disk_conformance_suite(
    adapter: HostDiskBackend,
    *,
    sources: Mapping[str, str],
    workdir: str,
) -> HostDiskConformanceSuite:
    """Build a conformance suite for the given OPTIONAL host-disk adapter.

    ``sources`` maps guest target → host path of pre-existing images the
    adapter can read (the Fake seeds them; a real adapter arranges them on a
    host). ``workdir`` is a directory prefix the scenario writes its copies /
    overlays / archives under. Both are bound into the CONF-O-01 scenario so
    the runner's uniform ``scenario(adapter)`` call still applies.
    """
    catalog: dict[str, _Scenario] = {
        "CONF-O-01": partial(
            _disk_scenarios.disk_image_round_trip,
            sources=sources,
            workdir=workdir,
        ),
    }
    return HostDiskConformanceSuite(adapter=adapter, catalog=catalog)


def network_conformance_suite(
    adapter: NetworkBackend,
    *,
    observer: HypervisorBackend,
) -> NetworkConformanceSuite:
    """Build a conformance suite for the given OPTIONAL network adapter.

    ``observer`` is the :class:`HypervisorBackend` the management backend shares
    an engine connection with (the Fake's hypervisor; a real adapter's
    ``LibvirtHypervisorBackend`` over the same connection) — CONF-N-01 verifies
    each management op by observing its effect through ``observer.list_networks``.
    """
    catalog: dict[str, _Scenario] = {
        "CONF-N-01": partial(
            _network_scenarios.network_management_round_trip,
            observer=observer,
        ),
    }
    return NetworkConformanceSuite(adapter=adapter, catalog=catalog)
