"""Tests for vm_fleet_status MCP tool.

Uses monkeypatch to replace ``default_registry`` and ``default_job_store``
so tests run against isolated in-memory / tmp_path-backed stores.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import pytest
from fastmcp.tools import ToolResult
from mcp.types import TextContent

import mcp_vm_blackbox.fleet_tools as fleet_tools_mod
from mcp_vm_blackbox.coordination_db import _reset_db_instance_for_testing
from mcp_vm_blackbox.fleet_tools import vm_fleet_status
from mcp_vm_blackbox.job_store import JobStore
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 _content_text(result: ToolResult) -> str:
    """Extract the text from the first content item of a ToolResult.

    Args:
        result: A ``ToolResult`` whose first content item must be ``TextContent``.

    Returns:
        The text string from the first content item.

    Raises:
        AssertionError: If the first content item is not ``TextContent``.
    """
    assert isinstance(result.content[0], TextContent), f"Expected TextContent, got {type(result.content[0])}"
    return result.content[0].text


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


class TestVmFleetStatus:
    """Unit tests for the vm_fleet_status tool."""

    @pytest.fixture(autouse=True)
    def _isolated_stores(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """Replace module-level singletons with tmp_path-backed stores."""
        self.registry = VMRegistry(tmp_path)
        self.job_store = JobStore(tmp_path)
        monkeypatch.setattr(fleet_tools_mod, "default_registry", self.registry)
        monkeypatch.setattr(fleet_tools_mod, "default_job_store", self.job_store)

    @pytest.mark.anyio
    async def test_empty_registry_returns_no_vms_text(self) -> None:
        """Empty registry produces the canonical 'No VMs registered.' message."""
        result = await vm_fleet_status()
        assert _content_text(result) == "No VMs registered."

    @pytest.mark.anyio
    async def test_vms_with_mixed_jobs(self) -> None:
        """Two VMs — one with 2 jobs (active + completed), one with none."""

        # Populate registry with 2 VMs
        await self.registry.populate([
            VmListEntry(name="vm-alpha", uuid="uuid-aaa", running=True),
            VmListEntry(name="vm-beta", uuid="uuid-bbb", running=False),
        ])

        # Create 2 jobs on vm-alpha
        job1 = await self.job_store.start("vm-alpha", "install", "agent-1", "begin install")
        job2 = await self.job_store.start("vm-alpha", "smoke", "agent-2", "start smoke test")
        await self.job_store.set_status(job2, "vm-alpha", "completed")

        result = await vm_fleet_status()
        text = _content_text(result)

        # Both VMs appear in the text output
        assert isinstance(text, str)
        assert "vm-alpha" in text
        assert "vm-beta" in text

        # Fleet summary line reflects 2 VMs
        assert "Fleet: 2 VMs" in text

        # vm-alpha is running; vm-beta is stopped
        assert "Running" in text
        assert "Stopped" in text

        # Active job's scenario appears for vm-alpha (active job preferred over completed)
        assert "install" in text

        # Verify underlying data via direct store access
        alpha_jobs = await self.job_store.list_by_vm("vm-alpha")
        assert len(alpha_jobs) == 2
        job_ids = {j.job_id for j in alpha_jobs}
        assert job1 in job_ids
        assert job2 in job_ids

        active_job = next(j for j in alpha_jobs if j.job_id == job1)
        assert active_job.status == "active"
        assert active_job.scenario_name == "install"
        assert active_job.last_agent_id == "agent-1"
        assert active_job.updated_at is not None

        completed_job = next(j for j in alpha_jobs if j.job_id == job2)
        assert completed_job.status == "completed"

        beta_jobs = await self.job_store.list_by_vm("vm-beta")
        assert beta_jobs == []

    @pytest.mark.anyio
    async def test_vm_with_no_job_directory(self) -> None:
        """VM exists in registry but no jobs directory — row appears with empty job columns."""

        # Populate registry with 1 VM — no jobs created at all
        await self.registry.populate([VmListEntry(name="vm-lonely", uuid="uuid-ccc", running=True)])

        result = await vm_fleet_status()
        text = _content_text(result)

        assert isinstance(text, str)
        assert "vm-lonely" in text
        assert "Fleet: 1 VMs" in text

    @pytest.mark.anyio
    async def test_recording_fields_default_to_false_none_none(self) -> None:
        """FleetEntry recording fields default to False/None/None when no recording is active.

        Tests: recording field passthrough defaults in FleetEntry
        How: Populate registry with a VM (no recording state set), call vm_fleet_status,
             assert the text table shows 'No' for the Recording column.
        Why: VMRegistryEntry defaults recording=False and both path fields to None;
             fleet_tools.py must pass them through unchanged rather than dropping them.
        """
        # Arrange — VM with default recording state (no recording active)
        await self.registry.populate([VmListEntry(name="vm-norecord", uuid="uuid-nr1", running=True)])

        # Act
        result = await vm_fleet_status()
        text = _content_text(result)

        # Assert — recording shows 'No' in the markdown table row
        assert isinstance(text, str)
        assert "vm-norecord" in text
        # The text table renders recording=False as 'No'
        assert "| No |" in text

    @pytest.mark.anyio
    async def test_recording_fields_flow_through_to_fleet_entry(self) -> None:
        """All three recording fields pass through from VMRegistryEntry to FleetEntry.

        Tests: recording field passthrough when recording is active
        How: Populate registry with a VM, then update its recording fields via
             registry.update(), call vm_fleet_status, assert the text table shows 'Yes'.
        Why: The fleet_tools.py loop assigns recording, recording_path, and
             recording_started_at from the VMRegistryEntry to the FleetEntry constructor.
             This test proves all three assignments carry correct values.
        """
        # Arrange — populate VM, then set it to active recording state
        vm_name = "vm-recording"
        await self.registry.populate([VmListEntry(name=vm_name, uuid="uuid-rec1", running=True)])
        await self.registry.update(
            vm_name,
            recording=True,
            recording_path="/tmp/session.webm",
            recording_started_at="2026-03-28T10:00:00+00:00",
        )

        # Act
        result = await vm_fleet_status()
        text = _content_text(result)

        # Assert — recording shows 'Yes' in the markdown table row
        assert isinstance(text, str)
        assert vm_name in text
        assert "| Yes |" in text

    @pytest.mark.anyio
    async def test_job_summary_pilot_context_usage_defaults_to_none(self) -> None:
        """pilot_context_usage=None renders as 'N/A' in the fleet text output.

        Tests: pilot_context_usage default in JobSummary
        How: Start a job (no pilot_context_usage written), call vm_fleet_status,
             assert the text table shows 'N/A' for the Context column.
        Why: The jobs table column is REAL and nullable; new jobs always have NULL until
             the write path is implemented. The model and passthrough must preserve None.
        """
        # Arrange — VM with one job, no pilot_context_usage written
        await self.registry.populate([VmListEntry(name="vm-ctx", uuid="uuid-ctx1", running=True)])
        await self.job_store.start("vm-ctx", "smoke-test", "agent-x", "initial boot")

        # Act
        result = await vm_fleet_status()
        text = _content_text(result)

        # Assert — None renders as 'N/A'
        assert isinstance(text, str)
        assert "N/A" in text

    @pytest.mark.anyio
    async def test_pilot_context_usage_flows_through_from_job_record(self) -> None:
        """pilot_context_usage flows from JobRecord through JobSummary and renders as percentage.

        Tests: pilot_context_usage passthrough from DB -> JobRecord -> JobSummary -> text
        How: Start a job, directly update pilot_context_usage in the DB via
             coordination_db, call vm_fleet_status, assert '75%' appears in the text.
        Why: fleet_tools.py must pass r.pilot_context_usage to the JobSummary
             constructor. Without this test, a missing keyword arg would silently
             drop the value and tests would not catch the regression.
        """
        from mcp_vm_blackbox.coordination_db import get_db

        # Arrange — VM with one job, then write pilot_context_usage directly to DB
        vm_name = "vm-pcu"
        await self.registry.populate([VmListEntry(name=vm_name, uuid="uuid-pcu1", running=True)])
        job_id = await self.job_store.start(vm_name, "context-test", "agent-pcu", "start")

        # Write pilot_context_usage to the jobs table directly (no write-path MCP tool yet)
        db = await get_db(self.registry._root)
        await db._c.execute(
            "UPDATE jobs SET pilot_context_usage = ? WHERE job_id = ? AND vm_name = ?", (0.75, job_id, vm_name)
        )
        await db._c.commit()

        # Act
        result = await vm_fleet_status()
        text = _content_text(result)

        # Assert — 0.75 renders as '75%'
        assert isinstance(text, str)
        assert "75%" in text
