"""Integration tests for cross-session job resume via ``fastmcp.Client``.

Proves the R004 obligation: "Job resume across sessions — in-process
``fastmcp.Client`` test: start job, drop client, new client resumes, step log
shows both agents."

Cross-session simulation strategy
----------------------------------
Two logical sessions are simulated by:

1. Opening a ``fastmcp.Client`` against the real ``mcp`` server object,
   performing actions as agent-A, then closing the client.
2. Monkeypatching ``_AGENT_ID`` to a different value (agent-B), opening a
   **new** ``fastmcp.Client``, and continuing the job.

Between sessions, the ``fastmcp.Client`` is fully closed and a new one is
constructed — proving the job state is durable on disk and survives client
teardown.

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_packer_prompt.py``.
"""

from __future__ import annotations

import json
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import types
    from pathlib import Path

    import pytest

import mcp_vm_blackbox.job_tools as job_tools_mod
from mcp_vm_blackbox.job_store import JobStore


class TestJobLifecycle:
    """Integration tests exercising the full MCP job lifecycle through JSON-RPC."""

    async def test_cross_session_resume(
        self, server_module: types.ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """Start a job as agent-A, pause, then resume and complete as agent-B.

        Exercises the full status transition chain
        ``active → paused → active → completed`` through two independent
        ``fastmcp.Client`` sessions, proving file-backed persistence survives
        client close/reopen.

        Assertions:
        - Step log has ≥3 entries
        - Early steps attributed to agent-A
        - Later steps attributed to agent-B
        - Final status is "completed"
        - Job JSON file exists on disk at the expected path
        """
        from fastmcp import Client

        # Redirect store to tmp_path for isolation
        store = JobStore(tmp_path)
        monkeypatch.setattr(job_tools_mod, "default_job_store", store)

        vm_name = "test-vm"

        # ── Session 1: agent-A starts, steps, pauses ──────────────
        monkeypatch.setattr(job_tools_mod, "_AGENT_ID", "agent-A")

        async with Client(server_module.mcp) as client:
            # Start the 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"

            # 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"

            # Pause the job
            pause_result = await client.call_tool("vm_job_pause", {"job_id": job_id, "vm_name": vm_name})
            pause_data = json.loads(pause_result.content[0].text)
            assert pause_data["status"] == "paused"

        # Client is now closed — session 1 ended

        # ── Session 2: agent-B finds, resumes, steps, completes ───
        monkeypatch.setattr(job_tools_mod, "_AGENT_ID", "agent-B")

        async with Client(server_module.mcp) as client:
            # List jobs to find the paused job
            list_result = await client.call_tool("vm_job_list", {"vm_name": vm_name, "status": "paused"})
            list_data = json.loads(list_result.content[0].text)
            assert len(list_data) == 1, f"Expected 1 paused job, got {list_data}"
            assert list_data[0]["job_id"] == job_id
            assert list_data[0]["status"] == "paused"

            # Resume the job
            resume_result = await client.call_tool("vm_job_resume", {"job_id": job_id, "vm_name": vm_name})
            resume_data = json.loads(resume_result.content[0].text)
            assert resume_data["status"] == "active"

            # Append a step as agent-B
            step2_result = await client.call_tool(
                "vm_job_step",
                {
                    "job_id": job_id,
                    "vm_name": vm_name,
                    "action": "finish installer wizard",
                    "outcome": "installation complete",
                },
            )
            step2_data = json.loads(step2_result.content[0].text)
            assert step2_data["status"] == "step_appended"

            # 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"

        # Client is now closed — session 2 ended

        # ── Post-session assertions ───────────────────────────────

        # 1. Job file exists on disk
        job_path = tmp_path / "jobs" / vm_name / f"{job_id}.json"
        assert job_path.exists(), f"Job file missing at {job_path}"

        # 2. Load the record and verify step log
        record = await store.load(job_id, vm_name)
        assert record.status == "completed"
        assert len(record.steps) >= 3, f"Expected ≥3 steps, got {len(record.steps)}: {[s.action for s in record.steps]}"

        # 3. Agent attribution: agent-A in early steps, agent-B in later
        agent_ids = [s.agent_id for s in record.steps]
        assert "agent-A" in agent_ids, f"agent-A not found in step agents: {agent_ids}"
        assert "agent-B" in agent_ids, f"agent-B not found in step agents: {agent_ids}"

        # agent-A should appear before agent-B's first occurrence
        first_a = agent_ids.index("agent-A")
        first_b = agent_ids.index("agent-B")
        assert first_a < first_b, (
            f"agent-A (index {first_a}) should appear before agent-B (index {first_b}) in step log"
        )

        # 4. Verify the raw JSON on disk matches the loaded record
        raw_data = json.loads(job_path.read_text(encoding="utf-8"))
        assert raw_data["status"] == "completed"
        assert raw_data["job_id"] == job_id
        assert raw_data["vm_name"] == vm_name

    async def test_tools_discoverable(self, server_module: types.ModuleType) -> None:
        """All six ``vm_job_*`` tools must be discoverable via ``list_tools``.

        Catches import/discovery wiring regressions — if a tool is defined but
        not picked up by the server's ``FileSystemProvider``, it will be absent.
        """
        from fastmcp import Client

        expected_tools = {
            "vm_job_start",
            "vm_job_step",
            "vm_job_pause",
            "vm_job_resume",
            "vm_job_complete",
            "vm_job_list",
        }

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

        tool_names = {t.name for t in tools}
        missing = expected_tools - tool_names
        assert not missing, (
            f"Missing vm_job_* tools in list_tools: {sorted(missing)}. Available tools: {sorted(tool_names)}"
        )
