"""End-to-end integration tests for video-enabled proof demo chain.

These tests exercise the complete build_proof_demo(video_enabled=True) chain
with real filesystem evidence, mocking only subprocess.run (the Node/render.ts
boundary). This proves the pieces from S01-S03 assemble correctly:
evidence validation → render_staging validation → render invocation with
correct job-keyed paths → video_output_path population on the manifest.
"""

from __future__ import annotations

import json
from typing import TYPE_CHECKING, TypedDict
from unittest.mock import Mock, patch

import pytest

from mcp_vm_blackbox.proof_demo import build_proof_demo

if TYPE_CHECKING:
    from pathlib import Path


class VideoChainEvidence(TypedDict):
    """Typed fixture return for video chain evidence."""

    jobs_root: Path
    evidence_root: Path
    job_id: str
    vm_name: str
    job_path: Path
    proof_file: Path
    screenshots_dir: Path
    render_staging_dir: Path


@pytest.fixture
def video_chain_evidence(tmp_path: Path) -> VideoChainEvidence:
    """Create realistic evidence structure for video-enabled proof demo.

    Creates:
    - jobs/{vm_name}/{job_id}.json with valid job record
    - evidence/{vm_name}/{job_id}/proof.txt with thumbs-up content
    - evidence/{vm_name}/{job_id}/screenshots/ with at least one .png
    - evidence/{vm_name}/{job_id}/render_staging/ with dummy frame

    Returns dict with paths and identifiers for test use.
    """
    jobs_root = tmp_path
    evidence_root = tmp_path / "evidence"

    job_id = "abc123def456"
    vm_name = "Win11-Pro-Test"

    # Create job record at jobs/{vm_name}/{job_id}.json
    job_dir = jobs_root / "jobs" / vm_name
    job_dir.mkdir(parents=True)
    job_path = job_dir / f"{job_id}.json"
    job_record = {
        "job_id": job_id,
        "vm_name": vm_name,
        "scenario_name": "proof-demo-scenario",
        "created_by": "test-agent",
        "last_agent_id": "test-agent",
        "status": "completed",
        "steps": [
            {
                "agent_id": "test-agent",
                "action": "initial_step",
                "timestamp": "2025-01-01T00:00:00Z",
                "outcome": "success",
            }
        ],
        "created_at": "2025-01-01T00:00:00Z",
        "updated_at": "2025-01-01T00:00:00Z",
    }
    job_path.write_text(json.dumps(job_record), encoding="utf-8")

    # Create evidence structure
    job_evidence_dir = evidence_root / vm_name / job_id
    job_evidence_dir.mkdir(parents=True)

    # Proof file with all required fields
    proof_file = job_evidence_dir / "proof.txt"
    proof_content = """Proof Run Results
================

Calculated value: 42.5
Status: 👍

hostname: WIN11-PRO-TEST
username: TestUser
timestamp: 2025-01-01T12:00:00Z
"""
    proof_file.write_text(proof_content, encoding="utf-8")

    # Screenshots directory with matching naming convention
    screenshots_dir = job_evidence_dir / "screenshots"
    screenshots_dir.mkdir(parents=True)
    screenshot_path = screenshots_dir / f"{vm_name}-001.png"
    screenshot_path.write_bytes(b"\x89PNG\r\n\x1a\nfake png header")

    # Render staging directory with dummy frame
    render_staging_dir = job_evidence_dir / "render_staging"
    render_staging_dir.mkdir(parents=True)
    frame_path = render_staging_dir / "frame_001.png"
    frame_path.write_bytes(b"\x89PNG\r\n\x1a\nfake frame png")

    return VideoChainEvidence(
        jobs_root=jobs_root,
        evidence_root=evidence_root,
        job_id=job_id,
        vm_name=vm_name,
        job_path=job_path,
        proof_file=proof_file,
        screenshots_dir=screenshots_dir,
        render_staging_dir=render_staging_dir,
    )


class TestFullChainVideoEnabled:
    """Tests for complete video-enabled proof demo chain."""

    async def test_video_enabled_full_chain_success(self, video_chain_evidence: VideoChainEvidence) -> None:
        """Full chain with video_enabled=True populates video_output_path.

        This test:
        1. Patches subprocess.run so the side_effect creates proof_video.mp4
        2. Calls build_proof_demo(job_id, vm_name, evidence_root, video_enabled=True)
        3. Asserts manifest.video_output_path is not None and ends with proof_video.mp4
        4. Asserts manifest.render_staging_path is populated
        5. Asserts subprocess.run was called once with args containing render.ts, --manifest, --output
        """
        job_id = video_chain_evidence["job_id"]
        vm_name = video_chain_evidence["vm_name"]
        evidence_root = video_chain_evidence["evidence_root"]
        render_staging_dir = video_chain_evidence["render_staging_dir"]

        # Expected output path
        expected_mp4 = render_staging_dir / "proof_video.mp4"

        mock_result = Mock()
        mock_result.returncode = 0
        mock_result.stderr = ""

        # Patch subprocess.run in render_video module (where it's actually used)
        with patch("mcp_vm_blackbox.render_video.subprocess.run") as mock_run:

            def side_effect_create_video(*args, **kwargs):
                """Simulate render.ts creating the output file."""
                expected_mp4.write_bytes(b"fake mp4 video content")
                return mock_result

            mock_run.side_effect = side_effect_create_video

            manifest = await build_proof_demo(
                job_id=job_id,
                vm_name=vm_name,
                evidence_root=evidence_root,
                jobs_root=video_chain_evidence["jobs_root"],
                video_enabled=True,
            )

        # Assert video_output_path is populated correctly
        assert manifest.video_output_path is not None
        assert manifest.video_output_path.endswith("proof_video.mp4")

        # Assert render_staging_path is populated with job-keyed path
        assert manifest.render_staging_path is not None
        assert job_id in manifest.render_staging_path
        assert "render_staging" in manifest.render_staging_path

        # Assert subprocess.run was called once with correct CLI args
        mock_run.assert_called_once()
        call_args = mock_run.call_args[0][0]  # First positional argument is the command list

        # Verify CLI structure contains required elements
        assert "npx" in call_args
        assert "tsx" in call_args
        # render.ts path includes mcp_vm_video
        assert any("render.ts" in arg for arg in call_args)
        assert "--manifest" in call_args
        assert "--output" in call_args

        # Verify manifest was passed (temp file path)
        manifest_idx = call_args.index("--manifest") + 1
        assert call_args[manifest_idx].endswith(".json")

        # Verify output path was passed
        output_idx = call_args.index("--output") + 1
        assert "proof_video.mp4" in call_args[output_idx]

    async def test_video_disabled_skips_render(self, video_chain_evidence: VideoChainEvidence) -> None:
        """When video_enabled=False, render should not be invoked.

        This test:
        1. Uses same evidence setup
        2. Calls build_proof_demo(video_enabled=False)
        3. Asserts manifest.video_output_path is None
        4. Asserts subprocess.run was NOT called
        """
        job_id = video_chain_evidence["job_id"]
        vm_name = video_chain_evidence["vm_name"]
        evidence_root = video_chain_evidence["evidence_root"]

        # Patch subprocess.run in render_video module
        with patch("mcp_vm_blackbox.render_video.subprocess.run") as mock_run:
            manifest = await build_proof_demo(
                job_id=job_id,
                vm_name=vm_name,
                evidence_root=evidence_root,
                jobs_root=video_chain_evidence["jobs_root"],
                video_enabled=False,
            )

        # Assert video output path is None
        assert manifest.video_output_path is None

        # Assert render_staging_path is None when video disabled
        assert manifest.render_staging_path is None

        # Assert subprocess.run was NOT called
        mock_run.assert_not_called()

    async def test_manifest_evidence_attribution(self, video_chain_evidence: VideoChainEvidence) -> None:
        """Manifest correctly attributes job_id and source evidence.

        This test:
        1. Calls build_proof_demo(video_enabled=True) with the mock
        2. Asserts the manifest's job field matches the job_id
        3. Asserts source_evidence is non-empty
        """
        job_id = video_chain_evidence["job_id"]
        vm_name = video_chain_evidence["vm_name"]
        evidence_root = video_chain_evidence["evidence_root"]
        render_staging_dir = video_chain_evidence["render_staging_dir"]

        expected_mp4 = render_staging_dir / "proof_video.mp4"

        mock_result = Mock()
        mock_result.returncode = 0
        mock_result.stderr = ""

        # Patch subprocess.run in render_video module
        with patch("mcp_vm_blackbox.render_video.subprocess.run") as mock_run:

            def side_effect_create_video(*args, **kwargs):
                expected_mp4.write_bytes(b"fake mp4 video content")
                return mock_result

            mock_run.side_effect = side_effect_create_video

            manifest = await build_proof_demo(
                job_id=job_id,
                vm_name=vm_name,
                evidence_root=evidence_root,
                jobs_root=video_chain_evidence["jobs_root"],
                video_enabled=True,
            )

        # Assert manifest primary_job_id matches input job_id
        assert manifest.primary_job_id == job_id
        assert manifest.primary_vm_name == vm_name

        # Assert source_evidence is non-empty
        assert manifest.source_evidence_count > 0

        # Verify evidence items contain the job record and screenshots
        evidence_kinds = [item.source_kind for item in manifest.evidence]
        assert "job_record" in evidence_kinds
        assert "screenshot" in evidence_kinds

    async def test_render_failure_raises_value_error_with_job_context(
        self, video_chain_evidence: VideoChainEvidence
    ) -> None:
        """Render failure wraps RuntimeError in ValueError with job_id context.

        This verifies the observability requirement:
        - ValueError messages with job_id and renderer detail on gate failure
        """
        job_id = video_chain_evidence["job_id"]
        vm_name = video_chain_evidence["vm_name"]
        evidence_root = video_chain_evidence["evidence_root"]

        mock_result = Mock()
        mock_result.returncode = 1
        mock_result.stderr = "FFmpeg error: invalid codec"

        # Patch subprocess.run in render_video module
        with patch("mcp_vm_blackbox.render_video.subprocess.run") as mock_run:
            mock_run.return_value = mock_result

            with pytest.raises(ValueError) as exc_info:
                await build_proof_demo(
                    job_id=job_id,
                    vm_name=vm_name,
                    evidence_root=evidence_root,
                    jobs_root=video_chain_evidence["jobs_root"],
                    video_enabled=True,
                )

        # Verify error message contains job_id for debugging
        error_msg = str(exc_info.value)
        assert "Video rendering failed" in error_msg
        assert job_id in error_msg
        assert "exit 1" in error_msg
        assert "FFmpeg error" in error_msg


class TestRenderStagingValidation:
    """Tests for render_staging directory validation in the chain."""

    async def test_render_staging_missing_fails_validation(self, video_chain_evidence: VideoChainEvidence) -> None:
        """Missing render_staging dir when video_enabled=True fails validation.

        This is part of the chain: evidence validation includes render_staging
        check when video is enabled.
        """
        job_id = video_chain_evidence["job_id"]
        vm_name = video_chain_evidence["vm_name"]
        evidence_root = video_chain_evidence["evidence_root"]
        render_staging_dir = video_chain_evidence["render_staging_dir"]

        # Remove the render_staging directory
        import shutil

        shutil.rmtree(render_staging_dir)

        with pytest.raises(ValueError) as exc_info:
            await build_proof_demo(
                job_id=job_id,
                vm_name=vm_name,
                evidence_root=evidence_root,
                jobs_root=video_chain_evidence["jobs_root"],
                video_enabled=True,
            )

        assert "Invalid proof evidence" in str(exc_info.value)
        assert "render_staging" in str(exc_info.value)

    async def test_render_staging_empty_dir_still_validates(self, video_chain_evidence: VideoChainEvidence) -> None:
        """Empty render_staging dir passes validation (existence check only).

        The validation only checks directory exists, not contents.
        The render.ts CLI will handle empty directory case.
        """
        job_id = video_chain_evidence["job_id"]
        vm_name = video_chain_evidence["vm_name"]
        evidence_root = video_chain_evidence["evidence_root"]
        render_staging_dir = video_chain_evidence["render_staging_dir"]

        # Remove all contents from render_staging but keep the directory
        for item in render_staging_dir.iterdir():
            item.unlink()

        expected_mp4 = render_staging_dir / "proof_video.mp4"

        mock_result = Mock()
        mock_result.returncode = 0
        mock_result.stderr = ""

        # Patch subprocess.run in render_video module
        with patch("mcp_vm_blackbox.render_video.subprocess.run") as mock_run:

            def side_effect_create_video(*args, **kwargs):
                expected_mp4.write_bytes(b"fake mp4 video content")
                return mock_result

            mock_run.side_effect = side_effect_create_video

            manifest = await build_proof_demo(
                job_id=job_id,
                vm_name=vm_name,
                evidence_root=evidence_root,
                jobs_root=video_chain_evidence["jobs_root"],
                video_enabled=True,
            )

        # Should succeed - validation only checks existence
        assert manifest.video_output_path is not None


class TestVideoOutputPathPopulation:
    """Tests for video_output_path field population scenarios."""

    async def test_video_output_path_is_absolute(self, video_chain_evidence: VideoChainEvidence) -> None:
        """video_output_path should be an absolute path."""
        job_id = video_chain_evidence["job_id"]
        vm_name = video_chain_evidence["vm_name"]
        evidence_root = video_chain_evidence["evidence_root"]
        render_staging_dir = video_chain_evidence["render_staging_dir"]

        expected_mp4 = render_staging_dir / "proof_video.mp4"

        mock_result = Mock()
        mock_result.returncode = 0
        mock_result.stderr = ""

        # Patch subprocess.run in render_video module
        with patch("mcp_vm_blackbox.render_video.subprocess.run") as mock_run:

            def side_effect_create_video(*args, **kwargs):
                expected_mp4.write_bytes(b"fake mp4 video content")
                return mock_result

            mock_run.side_effect = side_effect_create_video

            manifest = await build_proof_demo(
                job_id=job_id,
                vm_name=vm_name,
                evidence_root=evidence_root,
                jobs_root=video_chain_evidence["jobs_root"],
                video_enabled=True,
            )

        # video_output_path should be an absolute path
        assert manifest.video_output_path is not None
        assert manifest.video_output_path.startswith("/")

    async def test_render_staging_path_is_relative(self, video_chain_evidence: VideoChainEvidence) -> None:
        """render_staging_path should be relative to evidence_root."""
        job_id = video_chain_evidence["job_id"]
        vm_name = video_chain_evidence["vm_name"]
        evidence_root = video_chain_evidence["evidence_root"]
        render_staging_dir = video_chain_evidence["render_staging_dir"]

        expected_mp4 = render_staging_dir / "proof_video.mp4"

        mock_result = Mock()
        mock_result.returncode = 0
        mock_result.stderr = ""

        # Patch subprocess.run in render_video module
        with patch("mcp_vm_blackbox.render_video.subprocess.run") as mock_run:

            def side_effect_create_video(*args, **kwargs):
                expected_mp4.write_bytes(b"fake mp4 video content")
                return mock_result

            mock_run.side_effect = side_effect_create_video

            manifest = await build_proof_demo(
                job_id=job_id,
                vm_name=vm_name,
                evidence_root=evidence_root,
                jobs_root=video_chain_evidence["jobs_root"],
                video_enabled=True,
            )

        # render_staging_path should be relative
        assert manifest.render_staging_path is not None
        assert not manifest.render_staging_path.startswith("/")

        # Should follow the expected format: {vm_name}/{job_id}/render_staging
        expected_rel_path = f"{vm_name}/{job_id}/render_staging"
        assert manifest.render_staging_path == expected_rel_path


class TestManifestOnlyRejection:
    """Negative tests proving manifest-only output cannot pass when video requested.

    These tests prove R027 and R034 (anti-feature): the system cannot regress
    into accepting a manifest without a playable video as "video delivery."
    """

    async def test_render_nonzero_exit_raises(self, video_chain_evidence: VideoChainEvidence) -> None:
        """Non-zero exit from renderer raises ValueError with job context.

        This proves the gate catches renderer crashes and surfaces them
        with actionable diagnostic info.
        """
        job_id = video_chain_evidence["job_id"]
        vm_name = video_chain_evidence["vm_name"]
        evidence_root = video_chain_evidence["evidence_root"]

        mock_result = Mock()
        mock_result.returncode = 1
        mock_result.stderr = "render crashed"

        with patch("mcp_vm_blackbox.render_video.subprocess.run") as mock_run:
            mock_run.return_value = mock_result

            with pytest.raises(ValueError) as exc_info:
                await build_proof_demo(
                    job_id=job_id,
                    vm_name=vm_name,
                    evidence_root=evidence_root,
                    jobs_root=video_chain_evidence["jobs_root"],
                    video_enabled=True,
                )

        error_msg = str(exc_info.value)
        assert "Video rendering failed" in error_msg
        assert job_id in error_msg

    async def test_render_empty_output_raises(self, video_chain_evidence: VideoChainEvidence) -> None:
        """Empty output file raises ValueError - not acceptable as video delivery.

        This proves the gate checks that output file has content, not just existence.
        """
        job_id = video_chain_evidence["job_id"]
        vm_name = video_chain_evidence["vm_name"]
        evidence_root = video_chain_evidence["evidence_root"]
        render_staging_dir = video_chain_evidence["render_staging_dir"]

        expected_mp4 = render_staging_dir / "proof_video.mp4"

        mock_result = Mock()
        mock_result.returncode = 0
        mock_result.stderr = ""

        with patch("mcp_vm_blackbox.render_video.subprocess.run") as mock_run:

            def side_effect_empty_video(*args, **kwargs):
                """Create an empty output file."""
                expected_mp4.write_bytes(b"")  # Empty file
                return mock_result

            mock_run.side_effect = side_effect_empty_video

            with pytest.raises(ValueError) as exc_info:
                await build_proof_demo(
                    job_id=job_id,
                    vm_name=vm_name,
                    evidence_root=evidence_root,
                    jobs_root=video_chain_evidence["jobs_root"],
                    video_enabled=True,
                )

        error_msg = str(exc_info.value)
        assert job_id in error_msg

    async def test_render_no_output_file_raises(self, video_chain_evidence: VideoChainEvidence) -> None:
        """Missing output file raises ValueError - render must produce output.

        This proves the gate catches the case where renderer exits successfully
        but fails to create the output file.
        """
        job_id = video_chain_evidence["job_id"]
        vm_name = video_chain_evidence["vm_name"]
        evidence_root = video_chain_evidence["evidence_root"]

        mock_result = Mock()
        mock_result.returncode = 0
        mock_result.stderr = ""

        with patch("mcp_vm_blackbox.render_video.subprocess.run") as mock_run:
            # Return success but DON'T create the output file
            mock_run.return_value = mock_result

            with pytest.raises(ValueError) as exc_info:
                await build_proof_demo(
                    job_id=job_id,
                    vm_name=vm_name,
                    evidence_root=evidence_root,
                    jobs_root=video_chain_evidence["jobs_root"],
                    video_enabled=True,
                )

        error_msg = str(exc_info.value)
        assert job_id in error_msg

    async def test_render_staging_missing_raises(self, video_chain_evidence: VideoChainEvidence) -> None:
        """Missing render_staging raises ValueError when video requested.

        The evidence validation gate catches this before render is invoked.
        """
        job_id = video_chain_evidence["job_id"]
        vm_name = video_chain_evidence["vm_name"]
        evidence_root = video_chain_evidence["evidence_root"]
        render_staging_dir = video_chain_evidence["render_staging_dir"]

        # Remove the render_staging directory
        import shutil

        shutil.rmtree(render_staging_dir)

        with pytest.raises(ValueError) as exc_info:
            await build_proof_demo(
                job_id=job_id,
                vm_name=vm_name,
                evidence_root=evidence_root,
                jobs_root=video_chain_evidence["jobs_root"],
                video_enabled=True,
            )

        error_msg = str(exc_info.value)
        assert "Invalid proof evidence" in error_msg
        assert "render_staging" in error_msg

    async def test_manifest_only_ok_when_video_not_requested(self, video_chain_evidence: VideoChainEvidence) -> None:
        """Manifest-only output is acceptable when video not requested.

        This proves the anti-regression: manifest-only is fine when video_enabled=False.
        The system does not require video when video was not requested.
        """
        job_id = video_chain_evidence["job_id"]
        vm_name = video_chain_evidence["vm_name"]
        evidence_root = video_chain_evidence["evidence_root"]
        render_staging_dir = video_chain_evidence["render_staging_dir"]

        # Remove render_staging - should be OK since video not requested
        import shutil

        shutil.rmtree(render_staging_dir)

        # No mock needed - render should not be called
        with patch("mcp_vm_blackbox.render_video.subprocess.run") as mock_run:
            manifest = await build_proof_demo(
                job_id=job_id,
                vm_name=vm_name,
                evidence_root=evidence_root,
                jobs_root=video_chain_evidence["jobs_root"],
                video_enabled=False,
            )

        # Success - manifest created without video
        assert manifest.video_output_path is None
        # render was never invoked
        mock_run.assert_not_called()
