"""Tests for proof-gate validation.

The proof-gate is the single authoritative entry point for validating that
the Windows proof workflow can be trusted. These tests verify the gate
behaves correctly in both pass and fail states.

Run with:
    uv run pytest packages/mcp_vm_blackbox/tests/test_proof_gate.py -v --no-cov
"""

from __future__ import annotations

import builtins
from pathlib import Path
from typing import TYPE_CHECKING
from unittest.mock import MagicMock

import pytest

from mcp_vm_blackbox.proof_gate import (
    PROOF_CRITICAL_FILES,
    BlockingCategory,
    ProofGateResult,
    check_integrity,
    check_live_preconditions,
    check_proof_gate,
    check_readiness_semantics,
)

if TYPE_CHECKING:
    from collections.abc import Mapping, Sequence

    from pytest_mock import MockerFixture


# ---------------------------------------------------------------------------
# ProofGateResult tests
# ---------------------------------------------------------------------------


class TestProofGateResult:
    """Tests for ProofGateResult dataclass."""

    def test_passed_result_is_truthy(self) -> None:
        """Passed results evaluate to True via __bool__."""
        result = ProofGateResult(status="passed", checks_run=3)
        assert bool(result) is True
        assert result.status == "passed"

    def test_blocked_result_is_falsy(self) -> None:
        """Blocked results evaluate to False via __bool__."""
        result = ProofGateResult(
            status="blocked", checks_run=1, blocking_category=BlockingCategory.INTEGRITY, blocking_reason="Test failure"
        )
        assert bool(result) is False
        assert result.status == "blocked"

    def test_passed_result_has_no_blocking_category(self) -> None:
        """Passed results have no blocking category."""
        result = ProofGateResult(status="passed", checks_run=3)
        assert result.blocking_category is None
        assert result.blocking_reason is None


# ---------------------------------------------------------------------------
# Integrity check tests
# ---------------------------------------------------------------------------


class TestCheckIntegrity:
    """Tests for check_integrity function."""

    def test_clean_files_pass(self, tmp_path: Path) -> None:
        """Clean proof-critical files pass integrity check."""
        # Create proof-critical files with clean content
        for rel_path in PROOF_CRITICAL_FILES:
            file_path = tmp_path / rel_path
            file_path.parent.mkdir(parents=True, exist_ok=True)
            file_path.write_text("# Clean Python file\n\ndef foo():\n    pass\n")

        result = check_integrity(tmp_path)

        assert result.status == "passed"
        assert result.checks_run == len(PROOF_CRITICAL_FILES)

    def test_merge_conflict_markers_block(self, tmp_path: Path) -> None:
        """Merge conflict markers in proof-critical files block the gate."""
        # Create ALL proof-critical files (one with conflict)
        for rel_path in PROOF_CRITICAL_FILES:
            file_path = tmp_path / rel_path
            file_path.parent.mkdir(parents=True, exist_ok=True)
            if rel_path == PROOF_CRITICAL_FILES[0]:
                file_path.write_text(
                    "# File with merge conflict\n"
                    "<<<<<<< HEAD\n"
                    "from foo import bar\n"
                    "=======\n"
                    "from baz import qux\n"
                    ">>>>>>> other\n"
                )
            else:
                file_path.write_text("# Clean file\n")

        result = check_integrity(tmp_path)

        assert result.status == "blocked"
        assert result.blocking_category == BlockingCategory.INTEGRITY
        assert "merge conflict" in (result.blocking_reason or "").lower()

    def test_missing_file_blocks(self, tmp_path: Path) -> None:
        """Missing proof-critical files block the gate."""
        # Don't create any files
        result = check_integrity(tmp_path)

        assert result.status == "blocked"
        assert result.blocking_category == BlockingCategory.INTEGRITY
        assert "missing" in (result.blocking_reason or "").lower()

    def test_multiple_conflict_markers_counted(self, tmp_path: Path) -> None:
        """Multiple conflict markers are detected and counted."""
        # Create ALL proof-critical files (one with conflicts)
        for rel_path in PROOF_CRITICAL_FILES:
            file_path = tmp_path / rel_path
            file_path.parent.mkdir(parents=True, exist_ok=True)
            if rel_path == PROOF_CRITICAL_FILES[0]:
                file_path.write_text(
                    "<<<<<<< HEAD\n"
                    "code1\n"
                    "=======\n"
                    "code2\n"
                    ">>>>>>> branch1\n"
                    "<<<<<<< HEAD\n"
                    "code3\n"
                    "=======\n"
                    "code4\n"
                    ">>>>>>> branch2\n"
                )
            else:
                file_path.write_text("# Clean file\n")

        result = check_integrity(tmp_path)

        assert result.status == "blocked"
        assert result.blocking_category == BlockingCategory.INTEGRITY
        assert "merge conflict" in (result.blocking_reason or "").lower()
        # Should find conflict markers in details
        assert "conflict" in result.details.get("errors", "").lower()

    def test_blocking_details_explain_issue(self, tmp_path: Path) -> None:
        """Blocking result includes detailed error information."""
        # Create ALL proof-critical files (one with conflict)
        for rel_path in PROOF_CRITICAL_FILES:
            file_path = tmp_path / rel_path
            file_path.parent.mkdir(parents=True, exist_ok=True)
            if rel_path == PROOF_CRITICAL_FILES[0]:
                file_path.write_text("<<<<<<< HEAD\nconflict\n=======\n>>>>>>> other\n")
            else:
                file_path.write_text("# Clean file\n")

        result = check_integrity(tmp_path)

        assert result.status == "blocked"
        assert "errors" in result.details
        assert PROOF_CRITICAL_FILES[0] in result.details["errors"]


# ---------------------------------------------------------------------------
# Readiness semantics tests
# ---------------------------------------------------------------------------


class TestCheckReadinessSemantics:
    """Tests for check_readiness_semantics function."""

    def test_correct_semantics_pass(self) -> None:
        """Correct readiness semantics pass the check."""
        result = check_readiness_semantics()

        assert result.status == "passed"
        assert result.checks_run >= 1

    def test_missing_blocking_reason_field_blocks(self, mocker: MockerFixture) -> None:
        """Missing blocking_reason field in VmReadyResult blocks the gate."""
        # Patch the import location - check_readiness_semantics imports from models
        mock_model = MagicMock()
        mock_model.model_fields = {"status": MagicMock(), "vm_name": MagicMock()}
        mocker.patch("mcp_vm_blackbox.models.VmReadyResult", mock_model)

        result = check_readiness_semantics()

        assert result.status == "blocked"
        assert result.blocking_category == BlockingCategory.READINESS

    def test_import_error_blocks(self, mocker: MockerFixture) -> None:
        """Import errors block the gate."""
        original_import = builtins.__import__

        def mock_import(
            name: str,
            globals_: Mapping[str, object] | None = None,
            locals_: Mapping[str, object] | None = None,
            fromlist: Sequence[str] | None = (),
            level: int = 0,
        ) -> object:
            if name == "mcp_vm_blackbox.models" or name.startswith("mcp_vm_blackbox.models."):
                raise ImportError("Module not found")
            return original_import(name, globals_, locals_, fromlist, level)

        mocker.patch("builtins.__import__", side_effect=mock_import)

        result = check_readiness_semantics()

        assert result.status == "blocked"
        assert result.blocking_category == BlockingCategory.READINESS


# ---------------------------------------------------------------------------
# Live preconditions tests
# ---------------------------------------------------------------------------


class TestCheckLivePreconditions:
    """Tests for check_live_preconditions function."""

    def test_vboxmanage_not_found_blocks(self, mocker: MockerFixture) -> None:
        """Missing VBoxManage blocks the gate."""
        mocker.patch("shutil.which", return_value=None)

        result = check_live_preconditions("test-vm")

        assert result.status == "blocked"
        assert result.blocking_category == BlockingCategory.LIVE_PRECONDITION
        assert "VBoxManage" in result.details.get("errors", "")
        assert "precondition" in (result.blocking_reason or "").lower()

    def test_vm_not_found_blocks(self, mocker: MockerFixture) -> None:
        """Missing VM blocks the gate."""
        mocker.patch("shutil.which", return_value="/usr/bin/VBoxManage")
        mock_run = mocker.patch("subprocess.run")
        mock_run.return_value = MagicMock(returncode=1, stderr="VM not found")

        result = check_live_preconditions("nonexistent-vm")

        assert result.status == "blocked"
        assert result.blocking_category == BlockingCategory.LIVE_PRECONDITION
        assert "not found" in result.details.get("errors", "").lower()
        assert "precondition" in (result.blocking_reason or "").lower()

    def test_vm_exists_passes(self, mocker: MockerFixture) -> None:
        """Existing VM passes the check."""
        mocker.patch("shutil.which", return_value="/usr/bin/VBoxManage")
        mock_run = mocker.patch("subprocess.run")
        mock_run.return_value = MagicMock(returncode=0, stdout='VMState="powered off"\n')

        result = check_live_preconditions("test-vm")

        assert result.status == "passed"
        assert result.checks_run >= 1

    def test_custom_vm_name(self, mocker: MockerFixture) -> None:
        """Custom VM name is passed to VBoxManage."""
        mocker.patch("shutil.which", return_value="/usr/bin/VBoxManage")
        mock_run = mocker.patch("subprocess.run")
        mock_run.return_value = MagicMock(returncode=0, stdout='VMState="running"\n')

        result = check_live_preconditions("my-custom-vm")

        assert result.status == "passed"
        # Verify the VM name was passed
        call_args = mock_run.call_args[0][0]
        assert "my-custom-vm" in call_args


# ---------------------------------------------------------------------------
# Full proof-gate tests
# ---------------------------------------------------------------------------


class TestCheckProofGate:
    """Tests for check_proof_gate function (full gate)."""

    def test_full_gate_passes_with_all_checks(self, tmp_path: Path, mocker: MockerFixture) -> None:
        """Full gate passes when all checks succeed."""
        # Create clean proof-critical files
        for rel_path in PROOF_CRITICAL_FILES:
            file_path = tmp_path / rel_path
            file_path.parent.mkdir(parents=True, exist_ok=True)
            file_path.write_text("# Clean file\n")

        # Mock check_readiness_semantics to pass (it imports from real package)
        mocker.patch(
            "mcp_vm_blackbox.proof_gate.check_readiness_semantics",
            return_value=ProofGateResult(status="passed", checks_run=1),
        )

        # Mock VBoxManage and VM existence
        mocker.patch("shutil.which", return_value="/usr/bin/VBoxManage")
        mock_run = mocker.patch("subprocess.run")
        mock_run.return_value = MagicMock(returncode=0, stdout='VMState="running"\n')

        result = check_proof_gate(repo_root=tmp_path, vm_name="test-vm")

        assert result.status == "passed", f"Expected passed, got blocked: {result.blocking_reason}"
        assert result.checks_run >= len(PROOF_CRITICAL_FILES)

    def test_gate_stops_at_integrity_failure(self, tmp_path: Path) -> None:
        """Gate stops at first failure (integrity)."""
        # Create a file with merge conflict
        file_path = tmp_path / PROOF_CRITICAL_FILES[0]
        file_path.parent.mkdir(parents=True, exist_ok=True)
        file_path.write_text("<<<<<<< HEAD\nconflict\n=======\n>>>>>>> other\n")

        result = check_proof_gate(repo_root=tmp_path)

        assert result.status == "blocked"
        assert result.blocking_category == BlockingCategory.INTEGRITY
        # Should not have run readiness or live checks
        assert result.checks_run == len(PROOF_CRITICAL_FILES)

    def test_gate_stops_at_readiness_failure(self, tmp_path: Path, mocker: MockerFixture) -> None:
        """Gate stops at first failure (readiness)."""
        # Create clean proof-critical files
        for rel_path in PROOF_CRITICAL_FILES:
            file_path = tmp_path / rel_path
            file_path.parent.mkdir(parents=True, exist_ok=True)
            file_path.write_text("# Clean file\n")

        # Mock broken readiness semantics - patch at the module that's imported
        mock_model = MagicMock()
        mock_model.model_fields = {}  # Missing required fields
        mocker.patch("mcp_vm_blackbox.models.VmReadyResult", mock_model)

        result = check_proof_gate(repo_root=tmp_path)

        assert result.status == "blocked"
        assert result.blocking_category == BlockingCategory.READINESS

    def test_gate_stops_at_live_precondition_failure(self, tmp_path: Path, mocker: MockerFixture) -> None:
        """Gate stops at first failure (live precondition)."""
        # Create clean proof-critical files
        for rel_path in PROOF_CRITICAL_FILES:
            file_path = tmp_path / rel_path
            file_path.parent.mkdir(parents=True, exist_ok=True)
            file_path.write_text("# Clean file\n")

        # Mock VBoxManage not found
        mocker.patch("shutil.which", return_value=None)

        result = check_proof_gate(repo_root=tmp_path, vm_name="test-vm")

        assert result.status == "blocked"
        assert result.blocking_category == BlockingCategory.LIVE_PRECONDITION

    def test_skip_live_preconditions(self, tmp_path: Path, mocker: MockerFixture) -> None:
        """Gate passes when skipping live preconditions (offline CI)."""
        # Create clean proof-critical files
        for rel_path in PROOF_CRITICAL_FILES:
            file_path = tmp_path / rel_path
            file_path.parent.mkdir(parents=True, exist_ok=True)
            file_path.write_text("# Clean file\n")

        # Even without VBoxManage, should pass when skipping live checks
        mocker.patch("shutil.which", return_value=None)

        result = check_proof_gate(repo_root=tmp_path, vm_name="test-vm", skip_live_preconditions=True)

        assert result.status == "passed"

    def test_default_repo_root_is_cwd(self, mocker: MockerFixture) -> None:
        """Default repo_root uses current working directory."""
        # Mock integrity check to verify the path
        captured_path: Path | None = None

        def capture_path(repo_root: Path) -> ProofGateResult:
            nonlocal captured_path
            captured_path = repo_root
            return ProofGateResult(status="passed", checks_run=1)

        mocker.patch("mcp_vm_blackbox.proof_gate.check_integrity", side_effect=capture_path)

        result = check_proof_gate(skip_live_preconditions=True)

        assert result.status == "passed"
        assert captured_path == Path.cwd()


# ---------------------------------------------------------------------------
# Blocking category tests
# ---------------------------------------------------------------------------


class TestBlockingCategory:
    """Tests for BlockingCategory enum."""

    def test_categories_exist(self) -> None:
        """All expected blocking categories exist."""
        assert BlockingCategory.INTEGRITY.value == "integrity"
        assert BlockingCategory.READINESS.value == "readiness"
        assert BlockingCategory.LIVE_PRECONDITION.value == "live_precondition"

    def test_category_in_result_details(self, tmp_path: Path) -> None:
        """Blocking category is properly set in failed results."""
        # Create a file with merge conflict
        file_path = tmp_path / PROOF_CRITICAL_FILES[0]
        file_path.parent.mkdir(parents=True, exist_ok=True)
        file_path.write_text("<<<<<<< HEAD\nconflict\n=======\n>>>>>>> other\n")

        result = check_proof_gate(repo_root=tmp_path)

        assert result.blocking_category == BlockingCategory.INTEGRITY
        assert result.details is not None
        assert "errors" in result.details


# ---------------------------------------------------------------------------
# Integration tests (require real environment)
# ---------------------------------------------------------------------------


class TestProofGateIntegration:
    """Integration tests that verify real behavior."""

    @pytest.mark.integration
    def test_real_proof_gate_with_repo_root(self) -> None:
        """Verify proof-gate works with actual repo root.

        This test uses the real repository files and environment.
        """
        repo_root = Path(__file__).parent.parent.parent.parent

        # Run with skip_live_preconditions for CI environments
        result = check_proof_gate(repo_root=repo_root, skip_live_preconditions=True)

        # Should pass integrity and readiness checks
        assert result.status == "passed"
        assert result.checks_run >= len(PROOF_CRITICAL_FILES) + 1  # integrity + readiness
