"""Tests for the async VMRegistry facade over CoordinationDB."""

from __future__ import annotations

import importlib
import re
from typing import TYPE_CHECKING

import pytest

from mcp_vm_blackbox.coordination_db import _reset_db_instance_for_testing
from mcp_vm_blackbox.models import VmListEntry
from mcp_vm_blackbox.vm_registry import VMRegistry

if TYPE_CHECKING:
    from collections.abc import Generator
    from pathlib import Path


def _make_entries(count: int) -> list[VmListEntry]:
    """Create *count* synthetic VmListEntry objects."""
    return [
        VmListEntry(name=f"vm-{i}", uuid=f"00000000-0000-0000-0000-{i:012d}", running=i % 2 == 0)
        for i in range(1, count + 1)
    ]


@pytest.fixture(autouse=True)
def reset_db_singleton() -> Generator[None, None, None]:
    """Reset the CoordinationDB singleton before and after each test to prevent state leakage."""
    _reset_db_instance_for_testing()
    yield
    _reset_db_instance_for_testing()


class TestPopulate:
    """Tests for VMRegistry.populate()."""

    async def test_populate_persists_entries(self, tmp_path: Path) -> None:
        """populate() upserts VMs into the SQLite registry."""
        store = VMRegistry(root=tmp_path)
        entries = _make_entries(2)
        await store.populate(entries)

        result = await store.list_all()
        assert len(result) == 2
        names = {e.vm_name for e in result}
        assert names == {"vm-1", "vm-2"}

    async def test_populate_sets_last_refreshed(self, tmp_path: Path) -> None:
        """populate() sets a non-empty last_refreshed timestamp on each entry."""
        store = VMRegistry(root=tmp_path)
        await store.populate(_make_entries(1))

        entry = await store.get("vm-1")
        assert entry is not None
        assert entry.last_refreshed


class TestGet:
    """Tests for VMRegistry.get()."""

    async def test_get_returns_matching_entry(self, tmp_path: Path) -> None:
        """get() returns the correct VMRegistryEntry for a known name."""
        store = VMRegistry(root=tmp_path)
        await store.populate(_make_entries(3))

        result = await store.get("vm-1")
        assert result is not None
        assert type(result).__name__ == "VMRegistryEntry"
        assert result.vm_name == "vm-1"
        assert result.uuid == "00000000-0000-0000-0000-000000000001"

    async def test_get_returns_none_for_missing(self, tmp_path: Path) -> None:
        """get() returns None when the VM name is not in the registry."""
        store = VMRegistry(root=tmp_path)
        await store.populate(_make_entries(2))

        assert await store.get("nonexistent") is None


class TestListAll:
    """Tests for VMRegistry.list_all()."""

    async def test_list_all_returns_all_entries(self, tmp_path: Path) -> None:
        """list_all() returns every persisted entry."""
        store = VMRegistry(root=tmp_path)
        await store.populate(_make_entries(3))

        result = await store.list_all()
        assert len(result) == 3

    async def test_list_all_empty_when_no_entries(self, tmp_path: Path) -> None:
        """list_all() returns an empty list on a fresh store with no entries."""
        store = VMRegistry(root=tmp_path)
        assert await store.list_all() == []


class TestPersistence:
    """Tests for cross-instance persistence via shared SQLite DB."""

    async def test_persistence_survives_new_instance(self, tmp_path: Path) -> None:
        """Data written by one VMRegistry instance is visible to another sharing the same root."""
        store1 = VMRegistry(root=tmp_path)
        await store1.populate(_make_entries(2))

        store2 = VMRegistry(root=tmp_path)
        result = await store2.list_all()
        assert len(result) == 2
        names = {e.vm_name for e in result}
        assert names == {"vm-1", "vm-2"}


class TestUpdate:
    """Tests for VMRegistry.update()."""

    async def test_update_modifies_recording_fields(self, tmp_path: Path) -> None:
        """update() sets recording fields on an existing entry and persists.

        Tests: Recording state persistence via update().
        How: Populate registry with one entry; call update() with recording=True
             and recording_path; reload and verify the fields.
        Why: vm_recording_start must persist recording state so other tools and
             agents can read it back from a fresh registry instance.
        """
        store = VMRegistry(root=tmp_path)
        await store.populate(_make_entries(1))

        result = await store.update(
            "vm-1",
            recording=True,
            recording_path="/tmp/vm-1-screen0.webm",
            recording_started_at="2026-01-01T12:00:00+00:00",
        )

        assert result is not None
        assert result.recording is True
        assert result.recording_path == "/tmp/vm-1-screen0.webm"
        assert result.recording_started_at == "2026-01-01T12:00:00+00:00"

        reloaded = await store.get("vm-1")
        assert reloaded is not None
        assert reloaded.recording is True
        assert reloaded.recording_path == "/tmp/vm-1-screen0.webm"

    async def test_update_returns_none_for_unknown_vm(self, tmp_path: Path) -> None:
        """update() returns None when the VM name is not in the registry.

        Tests: update() missing VM path.
        How: Populate registry with vm-1; call update() for 'unknown-vm'.
        Why: The caller must handle None to know no update was applied.
        """
        store = VMRegistry(root=tmp_path)
        await store.populate(_make_entries(1))

        result = await store.update("unknown-vm", recording=True)

        assert result is None

    async def test_update_validates_field_names(self, tmp_path: Path) -> None:
        """update() raises ValueError when an invalid field name is passed.

        Tests: Field name validation in VMRegistry.update().
        How: Populate registry with one VM; call update() with a key that does
             not exist on VMRegistryEntry and assert ValueError is raised.
        Why: Silent acceptance of unknown field names would drop typo'd updates.
        """
        store = VMRegistry(root=tmp_path)
        await store.populate(_make_entries(1))

        with pytest.raises(ValueError, match="nonexistent_field"):
            await store.update("vm-1", nonexistent_field="bad_value")

    async def test_update_clears_recording_started_at(self, tmp_path: Path) -> None:
        """update() can clear recording_started_at by passing None.

        Tests: Clearing recording timestamp via update().
        How: Set recording_started_at via update(); then clear it with None;
             verify the persisted value is None.
        Why: vm_recording_stop must clear recording_started_at so elapsed_s
             is not calculated for stopped recordings.
        """
        store = VMRegistry(root=tmp_path)
        await store.populate(_make_entries(1))

        await store.update("vm-1", recording=True, recording_started_at="2026-01-01T12:00:00+00:00")
        await store.update("vm-1", recording=False, recording_started_at=None)

        entry = await store.get("vm-1")
        assert entry is not None
        assert entry.recording is False
        assert entry.recording_started_at is None


class TestRefresh:
    """Tests for VMRegistry.refresh()."""

    async def test_refresh_returns_current_entries(self, tmp_path: Path) -> None:
        """refresh() returns the current list of persisted entries."""
        store = VMRegistry(root=tmp_path)
        await store.populate(_make_entries(2))

        result = await store.refresh()
        assert len(result) == 2


class TestAgentID:
    """Tests for the _AGENT_ID constant in server.py."""

    def test_agent_id_default_format(self) -> None:
        """_AGENT_ID defaults to hostname:pid pattern."""
        from mcp_vm_blackbox.server import _AGENT_ID

        assert re.match(r"^.+:\d+$", _AGENT_ID), f"_AGENT_ID should match 'hostname:pid', got {_AGENT_ID!r}"

    def test_agent_id_env_override(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """_AGENT_ID reads from MCP_VM_BLACKBOX_AGENT_ID env var on reload."""
        import mcp_vm_blackbox.server as server_mod

        monkeypatch.setenv("MCP_VM_BLACKBOX_AGENT_ID", "custom-agent-7")
        importlib.reload(server_mod)
        try:
            assert server_mod._AGENT_ID == "custom-agent-7"
        finally:
            # Restore the module to default state for other tests
            monkeypatch.delenv("MCP_VM_BLACKBOX_AGENT_ID", raising=False)
            importlib.reload(server_mod)
