"""Tests for build_orchestration MCP tools.

Tests: build_start, build_watch, build_status.
How: Mock _ssh_run, subprocess.run so tests run offline without real builds.
Why: CI environments lack Packer; tools must return correct Pydantic models.
"""

from __future__ import annotations

import asyncio
import contextlib
from typing import TYPE_CHECKING
from unittest.mock import AsyncMock, MagicMock

import pytest

from mcp_vm_blackbox.targets import TargetConfig

if TYPE_CHECKING:
    from pathlib import Path

    from pytest_mock import MockerFixture


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def mock_resolve(mocker: MockerFixture) -> MagicMock:
    """Patch _resolve to return a local TargetConfig."""
    cfg = TargetConfig(ssh_host="")
    return mocker.patch("mcp_vm_blackbox.build_orchestration._resolve", return_value=cfg)


@pytest.fixture
def mock_ssh_run(mocker: MockerFixture) -> AsyncMock:
    """Patch _ssh_run for build_status (async)."""
    proc = MagicMock()
    proc.stdout = ""
    proc.stderr = ""
    proc.returncode = 0
    return mocker.patch("mcp_vm_blackbox.build_orchestration._ssh_run", new_callable=AsyncMock, return_value=proc)


# ---------------------------------------------------------------------------
# build_start
# ---------------------------------------------------------------------------


class TestBuildStart:
    """Tests for build_start tool."""

    async def test_build_start_unknown_build_returns_error(self, mock_resolve: MagicMock) -> None:
        """build_start returns error for unknown build alias."""
        from mcp_vm_blackbox.build_orchestration import build_start

        result = await build_start(build="unknown-build", target="ci")

        assert result.error is not None
        assert "Unknown build" in result.error
        assert result.pid is None

    async def test_build_start_success(
        self, mock_resolve: MagicMock, mock_ssh_run: AsyncMock, mocker: MockerFixture
    ) -> None:
        """build_start returns BuildStartResult with pid and log_path on success."""
        mock_ssh_run.return_value.stdout = "12345\n"
        mock_ssh_run.return_value.stderr = ""
        mock_ssh_run.return_value.returncode = 0
        mocker.patch.dict(
            "mcp_vm_blackbox.build_orchestration.BUILD_SCRIPTS", {"windows-vm": "/path/to/script.sh"}, clear=False
        )

        from mcp_vm_blackbox.build_orchestration import build_start

        result = await build_start(build="windows-vm", target="ci", log_dir="/tmp")

        assert result.pid == "12345"
        assert result.log_path is not None
        assert "packer-windows-vm" in result.log_path
        assert result.message is not None
        assert "build_watch" in result.message
        mock_ssh_run.assert_called_once()


# ---------------------------------------------------------------------------
# build_watch (async)
# ---------------------------------------------------------------------------


class TestBuildWatch:
    """Tests for build_watch tool."""

    async def test_build_watch_local_pattern_match(
        self, mock_resolve: MagicMock, mocker: MockerFixture, tmp_path: Path
    ) -> None:
        """build_watch returns early on pattern match when new content contains break_on."""
        mocker.patch("mcp_vm_blackbox.build_orchestration.LOG_POLL_INTERVAL_S", 0.05)
        log_file = tmp_path / "build.log"
        log_file.write_text("line1\n")  # Start with partial content

        from mcp_vm_blackbox.build_orchestration import build_watch

        async def append_success() -> None:
            await asyncio.sleep(0.15)  # Append after first poll
            log_file.write_text(log_file.read_text() + "Build successful\n")

        append_task = asyncio.create_task(append_success())
        try:
            result = await build_watch(log_path=str(log_file), target="local", watch_s=5, break_on=["Build successful"])
        finally:
            append_task.cancel()
            with contextlib.suppress(asyncio.CancelledError):
                await append_task

        assert result.reason == "pattern_match"
        assert result.matched == "Build successful"
        assert "Build successful" in (result.matched_line or "")

    async def test_build_watch_local_timeout(
        self, mock_resolve: MagicMock, mocker: MockerFixture, tmp_path: Path
    ) -> None:
        """build_watch returns timeout when no pattern matches within watch_s."""
        mocker.patch("mcp_vm_blackbox.build_orchestration.LOG_POLL_INTERVAL_S", 0.1)
        log_file = tmp_path / "build.log"
        log_file.write_text("line1\nline2")

        from mcp_vm_blackbox.build_orchestration import build_watch

        result = await build_watch(log_path=str(log_file), target="local", watch_s=1, break_on=["NONEXISTENT_PATTERN"])

        assert result.reason == "timeout"
        assert result.matched is None


# ---------------------------------------------------------------------------
# build_status
# ---------------------------------------------------------------------------


class TestBuildStatus:
    """Tests for build_status tool."""

    async def test_build_status_running(self, mock_resolve: MagicMock, mock_ssh_run: AsyncMock) -> None:
        """build_status sets running=True when no terminal marker in last lines."""
        mock_ssh_run.return_value.stdout = "Building...\nStill going..."
        mock_ssh_run.return_value.returncode = 0

        from mcp_vm_blackbox.build_orchestration import build_status

        result = await build_status(log_path="/tmp/build.log", target="local", tail_lines=20)

        assert result.running is True
        assert len(result.last_lines) > 0
        assert result.log_path == "/tmp/build.log"

    async def test_build_status_finished(self, mock_resolve: MagicMock, mock_ssh_run: AsyncMock) -> None:
        """build_status sets running=False when Build successful in last lines."""
        mock_ssh_run.return_value.stdout = "Packer finished\nBuild successful"
        mock_ssh_run.return_value.returncode = 0

        from mcp_vm_blackbox.build_orchestration import build_status

        result = await build_status(log_path="/tmp/build.log", target="local")

        assert result.running is False

    async def test_build_status_log_not_found(self, mock_resolve: MagicMock, mock_ssh_run: AsyncMock) -> None:
        """build_status returns empty last_lines when log file not found."""
        mock_ssh_run.return_value.stdout = ""
        mock_ssh_run.return_value.returncode = 1

        from mcp_vm_blackbox.build_orchestration import build_status

        result = await build_status(log_path="/nonexistent/build.log", target="local")

        assert result.last_lines == []
        assert result.running is True
