"""Integration tests proving fleet status and destroy guard through JSON-RPC.

Exercises the complete lifecycle:
  registry populate → job start → step → fleet status (R005) →
  vagrant_destroy blocked (R006) → job complete → destroy succeeds.

Deferred imports
----------------
``fastmcp`` is intentionally **not** imported at module level.  Importing it
at collection time (before conftest patches are in place) causes
``ModuleNotFoundError`` for ``rich.logging`` in pytest-xdist workers.  All
fastmcp symbols are accessed via local imports inside test bodies — matching
the pattern established in ``test_job_lifecycle.py``.
"""

from __future__ import annotations

import json
from typing import TYPE_CHECKING
from unittest.mock import AsyncMock

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


import pytest

import mcp_vm_blackbox.fleet_tools as fleet_tools_mod
import mcp_vm_blackbox.job_store as job_store_mod
import mcp_vm_blackbox.job_tools as job_tools_mod
import mcp_vm_blackbox.vagrant_tools as vagrant_tools_mod
from mcp_vm_blackbox.job_store import JobStore
from mcp_vm_blackbox.models import VmListEntry
from mcp_vm_blackbox.targets import TargetConfig
from mcp_vm_blackbox.vm_registry import VMRegistry


class TestFleetLifecycle:
    """Integration test exercising R005 + R006 end-to-end through fastmcp.Client."""

    async def test_fleet_lifecycle(
        self, server_module: types.ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """Full lifecycle: populate → start → step → fleet status → destroy blocked → complete → destroy ok.

        Assertions:
        - ``vm_fleet_status`` returns FleetEntry with active job data
        - ``vagrant_destroy`` returns is_error=True when an active job exists
        - ``vagrant_destroy`` succeeds after the job is completed
        - Job file persisted on disk
        """
        from fastmcp import Client

        # --- Isolated stores ---
        store = JobStore(tmp_path)
        registry = VMRegistry(tmp_path)

        # Monkeypatch job_store on all modules that reference it
        monkeypatch.setattr(job_tools_mod, "default_job_store", store)
        monkeypatch.setattr(fleet_tools_mod, "default_job_store", store)
        monkeypatch.setattr(job_store_mod, "default_job_store", store)

        # Monkeypatch registry on fleet_tools
        monkeypatch.setattr(fleet_tools_mod, "default_registry", registry)

        # Monkeypatch agent ID
        monkeypatch.setattr(job_tools_mod, "_AGENT_ID", "test-agent")

        # Mock vagrant internals so vagrant_destroy doesn't need real Vagrant
        local_cfg = TargetConfig(ssh_host="")
        monkeypatch.setattr(vagrant_tools_mod, "_resolve", lambda _target: local_cfg)

        mock_run = AsyncMock(return_value=("", "", 0, 0.1))
        monkeypatch.setattr(vagrant_tools_mod, "_vagrant_run", mock_run)

        mock_run_detached = AsyncMock()
        monkeypatch.setattr(vagrant_tools_mod, "_vagrant_run_detached", mock_run_detached)

        # --- Populate registry with a test VM ---
        vm_name = "fleet-test-vm"
        await registry.populate([VmListEntry(name=vm_name, uuid="uuid-fleet-1", running=True)])

        async with Client(server_module.mcp) as client:
            # ── 1. Start a job ──────────────────────────────────
            start_result = await client.call_tool(
                "vm_job_start", {"vm_name": vm_name, "scenario_name": "install-os", "step": "boot VM"}
            )
            start_data = json.loads(start_result.content[0].text)
            job_id = start_data["job_id"]
            assert start_data["status"] == "active"

            # ── 2. Append a step ────────────────────────────────
            step_result = await client.call_tool(
                "vm_job_step",
                {"job_id": job_id, "vm_name": vm_name, "action": "run installer wizard", "outcome": "page 1 complete"},
            )
            step_data = json.loads(step_result.content[0].text)
            assert step_data["status"] == "step_appended"

            # ── 3. Call vm_fleet_status (R005) ──────────────────
            fleet_result = await client.call_tool("vm_fleet_status", {})
            fleet_data = json.loads(fleet_result.content[0].text)

            assert len(fleet_data) == 1, f"Expected 1 VM, got {len(fleet_data)}"
            entry = fleet_data[0]
            assert entry["vm_name"] == vm_name
            assert entry["uuid"] == "uuid-fleet-1"
            assert entry["running"] is True
            assert len(entry["jobs"]) == 1

            job_summary = entry["jobs"][0]
            assert job_summary["job_id"] == job_id
            assert job_summary["status"] == "active"
            assert job_summary["scenario_name"] == "install-os"
            assert job_summary["last_agent_id"] == "test-agent"
            assert job_summary["last_step"] == "run installer wizard"

            # ── 4. vagrant_destroy should be BLOCKED (R006) ─────
            #    Use raise_on_error=False so the ToolError surfaces as
            #    is_error on the CallToolResult instead of propagating.
            destroy_result = await client.call_tool("vagrant_destroy", {"vm": vm_name}, raise_on_error=False)
            assert destroy_result.is_error is True, f"Expected isError=True from destroy guard, got: {destroy_result}"
            error_text = destroy_result.content[0].text
            assert f"Cannot destroy VM '{vm_name}'" in error_text
            assert job_id in error_text
            assert "install-os" in error_text

            # ── 5. Complete the job ─────────────────────────────
            complete_result = await client.call_tool("vm_job_complete", {"job_id": job_id, "vm_name": vm_name})
            complete_data = json.loads(complete_result.content[0].text)
            assert complete_data["status"] == "completed"

            # ── 6. vagrant_destroy should now SUCCEED ───────────
            destroy_ok_result = await client.call_tool("vagrant_destroy", {"vm": vm_name})
            assert destroy_ok_result.is_error is not True, (
                f"Expected destroy to succeed after job completion, got error: {destroy_ok_result.content[0].text}"
            )

        # ── Post-session: verify file persistence ──────────────
        job_path = tmp_path / "jobs" / vm_name / f"{job_id}.json"
        assert job_path.exists(), f"Job file missing at {job_path}"

        raw = json.loads(job_path.read_text(encoding="utf-8"))
        assert raw["status"] == "completed"
        assert raw["job_id"] == job_id


class TestFleetStatusDiscoverable:
    """Verify ``vm_fleet_status`` is discoverable via ``list_tools``."""

    async def test_fleet_status_discoverable(self, server_module: types.ModuleType) -> None:
        """``vm_fleet_status`` must appear in the tool list."""
        from fastmcp import Client

        async with Client(server_module.mcp) as client:
            tools = await client.list_tools()

        tool_names = {t.name for t in tools}
        assert "vm_fleet_status" in tool_names, (
            f"vm_fleet_status not found in list_tools. Available tools: {sorted(tool_names)}"
        )


class TestFleetStatusNewFields:
    """Integration tests proving recording and pilot_context_usage fields appear in JSON-RPC responses."""

    @pytest.fixture(autouse=True)
    def _reset_db(self) -> Generator[None, None, None]:
        """Reset the CoordinationDB singleton before and after each test.

        Prevents the session-scoped server_module fixture from leaking DB state
        between tests that use different tmp_path-backed stores.  Without this,
        the first call to get_db() wins and all subsequent tmp_path stores share
        the same SQLite database.
        """
        from mcp_vm_blackbox.coordination_db import _reset_db_instance_for_testing

        _reset_db_instance_for_testing()
        yield
        _reset_db_instance_for_testing()

    async def test_fleet_status_recording_fields_present_in_response(
        self, server_module: types.ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """FleetEntry recording fields and JobSummary.pilot_context_usage appear in vm_fleet_status JSON.

        Tests: New fields added in feature #46 are serialized and present in MCP tool response
        How: Populate registry with a VM (recording active), start a job, call vm_fleet_status
             via fastmcp.Client, assert recording/recording_path/recording_started_at and
             pilot_context_usage keys exist with correct values in the JSON response.
        Why: Confirms the end-to-end path: DB -> VMRegistryEntry -> FleetEntry -> JSON-RPC.
             Catches missing field wiring that unit tests of isolated constructors might miss.
        """
        from fastmcp import Client

        # --- Isolated stores ---
        store = JobStore(tmp_path)
        registry = VMRegistry(tmp_path)

        monkeypatch.setattr(fleet_tools_mod, "default_job_store", store)
        monkeypatch.setattr(fleet_tools_mod, "default_registry", registry)

        # --- Populate registry with a VM in active recording state ---
        vm_name = "fleet-rec-test-vm"
        await registry.populate([VmListEntry(name=vm_name, uuid="uuid-fleet-rec-1", running=True)])
        await registry.update(
            vm_name,
            recording=True,
            recording_path="/evidence/session.webm",
            recording_started_at="2026-03-28T09:00:00+00:00",
        )

        # --- Start a job (pilot_context_usage will be NULL) ---
        await store.start(vm_name, "recording-scenario", "agent-rec", "begin test")

        async with Client(server_module.mcp) as client:
            fleet_result = await client.call_tool("vm_fleet_status", {})

        fleet_data = json.loads(fleet_result.content[0].text)

        assert len(fleet_data) == 1
        entry = fleet_data[0]

        # --- Verify recording fields on FleetEntry ---
        assert entry["recording"] is True, f"recording should be True, got: {entry.get('recording')}"
        assert entry["recording_path"] == "/evidence/session.webm"
        assert entry["recording_started_at"] == "2026-03-28T09:00:00+00:00"

        # --- Verify pilot_context_usage on JobSummary (NULL -> None -> JSON null) ---
        assert len(entry["jobs"]) == 1
        job_summary = entry["jobs"][0]
        assert "pilot_context_usage" in job_summary, (
            f"pilot_context_usage key missing from job summary. Keys: {list(job_summary.keys())}"
        )
        assert job_summary["pilot_context_usage"] is None
