"""Tests for the goal state verification engine (verification.py).

Covers requirements: GOAL-06, MCP-03.

test_ssh_port_check is a real implementation test against mcp_vm_blackbox.ssh_exec.
All other tests exercise vm_verify_goal_state and the per-criterion dispatch helpers.
"""

from __future__ import annotations

from contextlib import asynccontextmanager
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from mcp_vm_blackbox.models import (
    Criterion,
    CriterionKind,
    GoalState,
    HostProtocol,
    HostRosterEntry,
    VerificationReport,
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_goal(
    *,
    protocol: HostProtocol = "WinRM",
    criterion_type: CriterionKind = "port_open",
    params: dict | None = None,
    fail_fast: bool = False,
    extra_criteria: list[Criterion] | None = None,
) -> GoalState:
    """Build a minimal GoalState for testing."""
    host = HostRosterEntry(name="win-host", ip="192.168.56.10", protocol=protocol, port=5985, username="vagrant")
    criteria = [
        Criterion(criterion_id="crit-001", host_name="win-host", criterion_type=criterion_type, params=params or {})
    ]
    if extra_criteria:
        criteria.extend(extra_criteria)
    return GoalState(
        vm_name="DSR-QA-Windows", scenario_name="install-dsr", fail_fast=fail_fast, hosts=[host], criteria=criteria
    )


def _mock_winrm_session(stdout_bytes: bytes, stderr_bytes: bytes = b"", status_code: int = 0) -> MagicMock:
    """Return a mock winrm.Session whose run_ps() returns a synthetic result."""
    result = MagicMock()
    result.std_out = stdout_bytes
    result.std_err = stderr_bytes
    result.status_code = status_code
    session = MagicMock()
    session.run_ps = MagicMock(return_value=result)
    return session


# ---------------------------------------------------------------------------
# GOAL-06: port_open criterion — WinRM
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_port_open_pass(mock_ctx: MagicMock) -> None:
    """GOAL-06: port_open criterion passes when WinRM reports the port is listening."""
    from mcp_vm_blackbox.verification import vm_verify_goal_state

    goal = _make_goal(criterion_type="port_open", params={"host": "192.168.56.10", "port": "8080"})
    mock_ctx.get_state = AsyncMock(
        side_effect=lambda key: "completed" if key.startswith("run_status:") else goal.model_dump()
    )

    session = _mock_winrm_session(b"OPEN\n")
    with patch("mcp_vm_blackbox.winrm_client.winrm.Session", return_value=session):
        result_dict = await vm_verify_goal_state("DSR-QA-Windows", "install-dsr", mock_ctx)

    report = VerificationReport.model_validate(result_dict)
    assert report.passed is True
    assert len(report.results) == 1
    assert report.results[0].passed is True
    assert report.results[0].actual == "OPEN"


@pytest.mark.asyncio
async def test_port_open_fail(mock_ctx: MagicMock) -> None:
    """GOAL-06: port_open criterion fails when port is not listening."""
    from mcp_vm_blackbox.verification import vm_verify_goal_state

    goal = _make_goal(criterion_type="port_open", params={"host": "192.168.56.10", "port": "8080"})
    mock_ctx.get_state = AsyncMock(
        side_effect=lambda key: "completed" if key.startswith("run_status:") else goal.model_dump()
    )

    session = _mock_winrm_session(b"CLOSED\n")
    with patch("mcp_vm_blackbox.winrm_client.winrm.Session", return_value=session):
        result_dict = await vm_verify_goal_state("DSR-QA-Windows", "install-dsr", mock_ctx)

    report = VerificationReport.model_validate(result_dict)
    assert report.passed is False
    assert report.results[0].passed is False
    assert report.results[0].actual == "CLOSED"


# ---------------------------------------------------------------------------
# GOAL-06: service_running criterion — WinRM
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_service_running_pass(mock_ctx: MagicMock) -> None:
    """GOAL-06: service_running criterion passes when service status is Running."""
    from mcp_vm_blackbox.verification import vm_verify_goal_state

    goal = _make_goal(criterion_type="service_running", params={"service_name": "DSR Service"})
    mock_ctx.get_state = AsyncMock(
        side_effect=lambda key: "completed" if key.startswith("run_status:") else goal.model_dump()
    )

    session = _mock_winrm_session(b"Running\n")
    with patch("mcp_vm_blackbox.winrm_client.winrm.Session", return_value=session):
        result_dict = await vm_verify_goal_state("DSR-QA-Windows", "install-dsr", mock_ctx)

    report = VerificationReport.model_validate(result_dict)
    assert report.passed is True
    assert report.results[0].actual == "Running"


@pytest.mark.asyncio
async def test_service_running_fail(mock_ctx: MagicMock) -> None:
    """GOAL-06: service_running criterion fails when service status is Stopped."""
    from mcp_vm_blackbox.verification import vm_verify_goal_state

    goal = _make_goal(criterion_type="service_running", params={"service_name": "DSR Service"})
    mock_ctx.get_state = AsyncMock(
        side_effect=lambda key: "completed" if key.startswith("run_status:") else goal.model_dump()
    )

    session = _mock_winrm_session(b"Stopped\n")
    with patch("mcp_vm_blackbox.winrm_client.winrm.Session", return_value=session):
        result_dict = await vm_verify_goal_state("DSR-QA-Windows", "install-dsr", mock_ctx)

    report = VerificationReport.model_validate(result_dict)
    assert report.passed is False
    assert report.results[0].actual == "Stopped"


# ---------------------------------------------------------------------------
# GOAL-06: SSH transport helper
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_ssh_port_check(caplog: pytest.LogCaptureFixture) -> None:
    """GOAL-06: _ssh_exec() connects via asyncssh, logs a warning, returns (stdout, stderr, exit_code)."""
    mock_result = MagicMock()
    mock_result.stdout = "hello"
    mock_result.stderr = ""
    mock_result.returncode = 0

    mock_conn = AsyncMock()
    mock_conn.run = AsyncMock(return_value=mock_result)

    @asynccontextmanager
    async def _fake_connect(**kwargs):  # noqa: RUF029
        yield mock_conn

    from mcp_vm_blackbox.ssh_exec import _ssh_exec

    with (
        patch("mcp_vm_blackbox.ssh_exec.asyncssh.connect", _fake_connect),
        caplog.at_level("WARNING", logger="mcp_vm_blackbox.ssh_exec"),
    ):
        stdout, stderr, exit_code = await _ssh_exec(
            host="192.168.56.10", port=22, username="vagrant", command="echo hello"
        )

    assert stdout == "hello"
    assert stderr == ""
    assert exit_code == 0
    assert any("192.168.56.10" in r.message and "22" in r.message for r in caplog.records), (
        f"Expected warning with host/port not found in: {[r.message for r in caplog.records]}"
    )


# ---------------------------------------------------------------------------
# GOAL-06: fail_fast halts after first failure
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_fail_fast_halts(mock_ctx: MagicMock) -> None:
    """GOAL-06: When fail_fast=True, verification stops after the first failing criterion."""
    from mcp_vm_blackbox.verification import vm_verify_goal_state

    # Two criteria — first will fail (CLOSED), second would pass (OPEN) but must not run
    second_criterion = Criterion(
        criterion_id="crit-002",
        host_name="win-host",
        criterion_type="port_open",
        params={"host": "192.168.56.10", "port": "9090"},
    )
    goal = _make_goal(
        criterion_type="port_open",
        params={"host": "192.168.56.10", "port": "8080"},
        fail_fast=True,
        extra_criteria=[second_criterion],
    )
    mock_ctx.get_state = AsyncMock(
        side_effect=lambda key: "completed" if key.startswith("run_status:") else goal.model_dump()
    )

    # First call returns CLOSED (fail), second would return OPEN but must not be called
    result1 = MagicMock()
    result1.std_out = b"CLOSED\n"
    result1.std_err = b""
    result1.status_code = 0

    result2 = MagicMock()
    result2.std_out = b"OPEN\n"
    result2.std_err = b""
    result2.status_code = 0

    session = MagicMock()
    session.run_ps = MagicMock(side_effect=[result1, result2])

    with patch("mcp_vm_blackbox.winrm_client.winrm.Session", return_value=session):
        result_dict = await vm_verify_goal_state("DSR-QA-Windows", "install-dsr", mock_ctx)

    report = VerificationReport.model_validate(result_dict)
    assert report.fail_fast_triggered is True
    assert report.passed is False
    # Only one criterion result — halt after first failure
    assert len(report.results) == 1
    assert report.results[0].passed is False
    # Second criterion was never dispatched
    assert session.run_ps.call_count == 1


# ---------------------------------------------------------------------------
# MCP-03: verification gate
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_verify_gate_blocked_pre_run(mock_ctx: MagicMock) -> None:
    """MCP-03: vm_verify_goal_state raises ToolError when called before any run completes."""
    from fastmcp.exceptions import ToolError

    from mcp_vm_blackbox.verification import vm_verify_goal_state

    # run_status returns None (not "completed")
    mock_ctx.get_state = AsyncMock(return_value=None)

    with pytest.raises(ToolError) as exc_info:
        await vm_verify_goal_state("DSR-QA-Windows", "install-dsr", mock_ctx)

    msg = str(exc_info.value)
    assert "Verification gate not open for DSR-QA-Windows/install-dsr" in msg
    assert "vm-scenario-runner" in msg


@pytest.mark.asyncio
async def test_verify_gate_opens_after_run(mock_ctx: MagicMock) -> None:
    """MCP-03: vm_verify_goal_state returns a VerificationReport after a successful scenario run."""
    from mcp_vm_blackbox.verification import vm_verify_goal_state

    goal = _make_goal(criterion_type="port_open", params={"host": "192.168.56.10", "port": "8080"})
    mock_ctx.get_state = AsyncMock(
        side_effect=lambda key: "completed" if key.startswith("run_status:") else goal.model_dump()
    )

    session = _mock_winrm_session(b"OPEN\n")
    with patch("mcp_vm_blackbox.winrm_client.winrm.Session", return_value=session):
        result_dict = await vm_verify_goal_state("DSR-QA-Windows", "install-dsr", mock_ctx)

    report = VerificationReport.model_validate(result_dict)
    assert report.vm_name == "DSR-QA-Windows"
    assert report.scenario_name == "install-dsr"
    assert isinstance(report.passed, bool)
    assert len(report.results) == 1
