"""Client surface — construction, parity, and dispatch.

Asserts both clients dispatch through their transport (the sync surface against
a sync fake, the async surface against an async fake), the few genuinely-unwired
sync reads still raise, and that the sync/async surfaces are parallel (same
method names + parameters).
"""

from __future__ import annotations

import inspect
from collections.abc import AsyncIterator, Awaitable, Iterator
from datetime import UTC, datetime
from typing import TYPE_CHECKING

import pytest
from mgf.vm.core.contracts import OperationHandle, OperationKind
from mgf.vm.core.ids import (
    DeviceId,
    LeaseId,
    OperationId,
    SnapshotId,
    VmId,
    VolumeId,
)
from tests.conftest import FakeGrpcTransport

from mgf.vm.sdk.client import AsyncVmManagerClient, VmManagerClient

if TYPE_CHECKING:
    from mgf.vm.core.contracts import EventGap, VmEvent

# (method_name, args, kwargs) — drives the stub-dispatch assertions for both
# the sync client and (via the same names) the async client.
READ_CALLS = [
    ("get_vm", (VmId("vm-1"),), {}),
    ("list_vms", (), {}),
    ("get_volume", (VolumeId("vol-1"),), {}),
    ("list_volumes", ("pool-1",), {}),
    ("get_pool", ("pool-1",), {}),
    ("refresh_pool", ("pool-1",), {}),
    ("list_pools", (), {}),
    ("list_snapshots", (VmId("vm-1"),), {}),
    ("list_volume_snapshots", (VolumeId("vol-1"),), {}),
    ("get_device", (DeviceId("dev-1"),), {}),
    ("list_devices", (), {}),
    ("list_leases", (), {}),
    ("get_operation", (OperationId("op-1"),), {}),
    ("list_operations", (), {}),
    ("capabilities", (), {}),
    ("storage_capabilities", (), {}),
    ("device_capabilities", (), {}),
]

MUTATION_CALLS = [
    ("create_vm", (object(),), {}),
    ("redefine_vm", (VmId("vm-1"), object()), {}),
    ("delete_vm", (VmId("vm-1"),), {"delete_storage": True}),
    ("start_vm", (VmId("vm-1"),), {}),
    ("stop_vm", (VmId("vm-1"),), {"force": True}),
    ("pause_vm", (VmId("vm-1"),), {}),
    ("resume_vm", (VmId("vm-1"),), {}),
    ("reboot_vm", (VmId("vm-1"),), {"force": False}),
    ("migrate_vm", (VmId("vm-1"), object()), {"plan": object()}),
    ("snapshot_vm", (VmId("vm-1"), "snap"), {"include_memory": True}),
    ("revert_snapshot", (VmId("vm-1"), SnapshotId("s-1")), {}),
    ("delete_snapshot", (VmId("vm-1"), SnapshotId("s-1")), {}),
    ("attach_device", (VmId("vm-1"), DeviceId("dev-1")), {"live": True}),
    ("detach_device", (VmId("vm-1"), DeviceId("dev-1")), {"live": False}),
    ("create_volume", (object(),), {}),
    ("resize_volume", (VolumeId("vol-1"), 1024), {}),
    ("delete_volume", (VolumeId("vol-1"),), {}),
    ("snapshot_volume", (VolumeId("vol-1"), "snap"), {}),
    ("revert_volume_snapshot", (VolumeId("vol-1"), SnapshotId("s-1")), {}),
    ("delete_volume_snapshot", (SnapshotId("s-1"),), {}),
    ("convert_format", (VolumeId("vol-1"), "raw", object()), {}),
    ("create_pool", (object(),), {}),
    ("delete_pool", ("pool-1",), {"force": True}),
    ("claim_device", (DeviceId("dev-1"),), {}),
    ("release_device", (LeaseId("lease-1"),), {}),
    ("transfer_lease", (LeaseId("lease-1"), "host-2"), {}),
]

ALL_CALLS = READ_CALLS + MUTATION_CALLS

# B1-B6 wired the ENTIRE sync surface (lifecycle, snapshots, storage, devices,
# plus B6's migrate_vm / get_operation / list_operations / watch_vm). Nothing on
# the sync client raises NotImplementedError any more, and B7 did the same for
# the async client. The dispatch tests below drive every method against a fake
# transport and assert the call actually reached the seam.


# -- fake async transport (mirrors conftest.FakeGrpcTransport) ------------


class FakeAsyncTransport:
    """Async stand-in mirroring :class:`conftest.FakeGrpcTransport`.

    Records submitted ops / reads and returns a terminal ``SUCCEEDED`` handle
    from :meth:`watch_op`, so the async two-step collapse has something to
    unpack. Satisfies the ``AsyncTransport`` Protocol structurally.
    """

    def __init__(self, *, canned_result: object = None) -> None:
        self.submitted: list[tuple[OperationKind, object, str]] = []
        self.reads: list[object] = []
        self.closed = False
        self._canned_result = canned_result
        self._counter = 0

    def _next_op_id(self) -> OperationId:
        self._counter += 1
        return OperationId(f"op-async-fake-{self._counter}")

    async def submit_op(
        self,
        kind: OperationKind,
        payload: object,
        *,
        client_op_id: str,
        token: object | None = None,
    ) -> OperationHandle:
        self.submitted.append((kind, payload, client_op_id))
        return OperationHandle.pending(self._next_op_id(), kind, datetime.now(UTC))

    async def get_op(self, op_id: OperationId) -> OperationHandle:
        now = datetime.now(UTC)
        return OperationHandle.pending(op_id, OperationKind.VM_CREATE, now).started()

    async def watch_op(self, op_id: OperationId) -> AsyncIterator[OperationHandle]:
        now = datetime.now(UTC)
        pending = OperationHandle.pending(op_id, OperationKind.VM_CREATE, now)
        yield pending.started()
        yield pending.started().succeeded(self._canned_result, finished_at=now)

    async def read(self, query: object) -> object:
        self.reads.append(query)
        return self._canned_result

    async def watch_stream(self, subject: object) -> AsyncIterator[VmEvent | EventGap]:
        return
        yield  # pragma: no cover - empty async generator

    async def aclose(self) -> None:
        self.closed = True


# -- construction + lifecycle ---------------------------------------------


def test_sync_client_constructs_and_closes() -> None:
    transport = FakeGrpcTransport()
    with VmManagerClient(transport, client_id="me") as client:
        assert client.client_id == "me"
    assert transport.closed


def test_sync_client_default_client_id() -> None:
    client = VmManagerClient(FakeGrpcTransport())
    assert client.client_id  # non-empty UUID


async def test_async_client_constructs_and_closes() -> None:
    transport = FakeGrpcTransport()

    class _AsyncFake(FakeGrpcTransport):
        async def aclose(self) -> None:  # type: ignore[override]
            self.closed = True

    atransport = _AsyncFake()
    async with AsyncVmManagerClient(atransport, client_id="me") as client:
        assert client.client_id == "me"
    assert atransport.closed
    assert not transport.closed  # untouched


# -- dispatch: sync (every method reaches the seam; none raise) -----------


@pytest.mark.parametrize(("name", "args", "kwargs"), ALL_CALLS)
def test_sync_methods_dispatch_to_transport(
    name: str,
    args: tuple[object, ...],
    kwargs: dict[str, object],
) -> None:
    transport = FakeGrpcTransport()
    client = VmManagerClient(transport)
    method = getattr(client, name)
    # No method raises NotImplementedError any more — each one dispatches.
    result = method(*args, **kwargs)
    if name == "get_operation":
        # get_operation reads the op registry via transport.get_op (no record).
        assert result is not None
    else:
        assert transport.submitted or transport.reads


def test_sync_watch_vm_returns_iterator() -> None:
    transport = FakeGrpcTransport()
    client = VmManagerClient(transport)
    stream = client.watch_vm(VmId("vm-1"))
    assert isinstance(stream, Iterator)
    assert list(stream) == []
    # watch_vm with no argument subscribes to all domains.
    assert list(client.watch_vm()) == []


# -- dispatch: async (every method reaches the seam; none raise) ----------


@pytest.mark.parametrize(("name", "args", "kwargs"), ALL_CALLS)
async def test_async_methods_dispatch_to_transport(
    name: str,
    args: tuple[object, ...],
    kwargs: dict[str, object],
) -> None:
    transport = FakeAsyncTransport()
    client = AsyncVmManagerClient(transport)
    method = getattr(client, name)
    # The async surface is fully implemented (B7) — nothing raises here.
    result = await method(*args, **kwargs)
    if name == "get_operation":
        assert result is not None
    else:
        assert transport.submitted or transport.reads


async def test_async_watch_vm_returns_async_iterator() -> None:
    transport = FakeAsyncTransport()
    client = AsyncVmManagerClient(transport)
    stream = client.watch_vm(VmId("vm-1"))
    assert isinstance(stream, AsyncIterator)
    assert [ev async for ev in stream] == []


# -- sync/async surface parity --------------------------------------------


def _public_methods(cls: type) -> set[str]:
    return {n for n, _ in inspect.getmembers(cls, predicate=callable) if not n.startswith("_")}


def test_surfaces_are_parallel() -> None:
    sync = _public_methods(VmManagerClient)
    asy = _public_methods(AsyncVmManagerClient)
    # Lifecycle differs by design (close vs aclose); everything else matches.
    sync_ops = sync - {"close"}
    async_ops = asy - {"aclose"}
    assert sync_ops == async_ops


def test_signatures_match_modulo_async() -> None:
    for name in _public_methods(VmManagerClient) - {"close"}:
        sync_sig = inspect.signature(getattr(VmManagerClient, name))
        async_sig = inspect.signature(getattr(AsyncVmManagerClient, name))
        assert list(sync_sig.parameters) == list(async_sig.parameters), name


def test_method_count_covers_surface() -> None:
    # ~39 methods: 17 reads + 26 mutations + watch_vm on the surface.
    assert len(ALL_CALLS) == 43
    assert len(READ_CALLS) == 17
    assert len(MUTATION_CALLS) == 26


def test_async_mutations_return_awaitables() -> None:
    client = AsyncVmManagerClient(FakeGrpcTransport())  # type: ignore[arg-type]
    coro = client.start_vm(VmId("vm-1"))
    assert isinstance(coro, Awaitable)
    coro.close()  # avoid un-awaited warning under filterwarnings=error
