"""Tests for vm-blackbox-record backend dispatch and VBoxManage backend.

Tests: backends/__init__.py get_backend dispatch and VBoxManageBackend methods.
Strategy: All _vboxmanage_sync and _screenshot_via_vboxapi calls are mocked
via pytest-mock; no VirtualBox or SSH required. Cleanup tests use tmp_path.
"""

from __future__ import annotations

import sys
import time
from pathlib import Path
from typing import TYPE_CHECKING

import pytest

if TYPE_CHECKING:
    from pytest_mock import MockerFixture

# Ensure the scripts directory is importable before any backend import.
_SCRIPTS_DIR = Path(__file__).parent.parent.parent.parent / "skills" / "vm-blackbox-record" / "scripts"
if str(_SCRIPTS_DIR) not in sys.path:
    sys.path.insert(0, str(_SCRIPTS_DIR))


# ---------------------------------------------------------------------------
# get_backend dispatch
# ---------------------------------------------------------------------------


class TestGetBackend:
    """Tests for the get_backend() dispatch function in backends/__init__.py."""

    def test_get_backend_vboxmanage(self) -> None:
        """get_backend('vboxmanage') returns a VBoxManageBackend instance.

        Tests: backends.get_backend dispatch.
        How: Import get_backend and VBoxManageBackend; assert isinstance check.
        Why: Dispatch must route the default backend name to the correct class.
        """
        from backends import get_backend
        from backends.vboxmanage import VBoxManageBackend

        backend = get_backend("vboxmanage")

        assert isinstance(backend, VBoxManageBackend)

    def test_get_backend_unknown_raises_value_error(self) -> None:
        """get_backend raises ValueError for an unrecognised backend name.

        Tests: backends.get_backend error path.
        How: Call get_backend with a nonsense name; assert ValueError with the
             name in the message.
        Why: Callers need a clear error when they typo the backend flag.
        """
        from backends import get_backend

        with pytest.raises(ValueError, match="bad"):
            get_backend("bad")

    def test_get_backend_error_message_contains_choices(self) -> None:
        """ValueError message lists all valid backend names.

        Tests: backends.get_backend error message quality.
        How: Catch ValueError and inspect its string representation.
        Why: Users need to know valid options without reading source code.
        """
        from backends import get_backend

        with pytest.raises(ValueError, match="vboxmanage"):
            get_backend("unknown_backend")


# ---------------------------------------------------------------------------
# VBoxManageBackend.record_start
# ---------------------------------------------------------------------------


class TestVBoxManageRecordStart:
    """Tests for VBoxManageBackend.record_start."""

    def test_record_start_calls_vboxmanage_sync_5_times(self, mocker: MockerFixture, tmp_path: Path) -> None:
        """record_start issues exactly 5 _vboxmanage_sync calls.

        Tests: VBoxManageBackend.record_start call count.
        How: Patch _vboxmanage_sync; call record_start; assert call_count == 5.
        Why: The backend must configure filename, videofps, videorate, on, start.
        """
        from backends.vboxmanage import VBoxManageBackend

        mock_sync = mocker.patch("backends.vboxmanage._vboxmanage_sync")
        backend = VBoxManageBackend()
        output_path = tmp_path / "MyVM-20240101-120000"

        backend.record_start("MyVM", output_path)

        assert mock_sync.call_count == 5

    def test_record_start_passes_vm_name_in_args(self, mocker: MockerFixture, tmp_path: Path) -> None:
        """record_start passes the VM name in every _vboxmanage_sync call.

        Tests: VBoxManageBackend.record_start argument correctness.
        How: Capture all call_args; verify vm_name appears in each.
        Why: Wrong VM name would silently record the wrong guest.
        """
        from backends.vboxmanage import VBoxManageBackend

        mock_sync = mocker.patch("backends.vboxmanage._vboxmanage_sync")
        backend = VBoxManageBackend()
        output_path = tmp_path / "TestVM-ts"

        backend.record_start("TestVM", output_path)

        for call in mock_sync.call_args_list:
            args_list = call.args[0]
            assert "TestVM" in args_list

    def test_record_start_sets_webm_filename(self, mocker: MockerFixture, tmp_path: Path) -> None:
        """record_start passes the .webm output path in the filename subcommand.

        Tests: VBoxManageBackend.record_start filename argument.
        How: Inspect the first _vboxmanage_sync call for the '.webm' suffix.
        Why: Recordings must land at the expected path for downstream scripts.
        """
        from backends.vboxmanage import VBoxManageBackend

        mock_sync = mocker.patch("backends.vboxmanage._vboxmanage_sync")
        backend = VBoxManageBackend()
        output_path = tmp_path / "vm-output"

        backend.record_start("vm", output_path)

        first_call_args = mock_sync.call_args_list[0].args[0]
        assert any(".webm" in str(a) for a in first_call_args)


# ---------------------------------------------------------------------------
# VBoxManageBackend.record_stop
# ---------------------------------------------------------------------------


class TestVBoxManageRecordStop:
    """Tests for VBoxManageBackend.record_stop."""

    def test_record_stop_calls_vboxmanage_sync_2_times(self, mocker: MockerFixture) -> None:
        """record_stop issues exactly 2 _vboxmanage_sync calls.

        Tests: VBoxManageBackend.record_stop call count.
        How: Patch _vboxmanage_sync; call record_stop; assert call_count == 2.
        Why: Stop and off are the two required recording teardown subcommands.
        """
        from backends.vboxmanage import VBoxManageBackend

        mock_sync = mocker.patch("backends.vboxmanage._vboxmanage_sync")
        backend = VBoxManageBackend()

        backend.record_stop("MyVM")

        assert mock_sync.call_count == 2

    def test_record_stop_sends_stop_then_off(self, mocker: MockerFixture) -> None:
        """record_stop sends 'stop' before 'off' in its two calls.

        Tests: VBoxManageBackend.record_stop argument ordering.
        How: Inspect call_args_list for the 'stop' and 'off' subcommand tokens.
        Why: VirtualBox requires stop before off; wrong order leaves recording enabled.
        """
        from backends.vboxmanage import VBoxManageBackend

        mock_sync = mocker.patch("backends.vboxmanage._vboxmanage_sync")
        backend = VBoxManageBackend()

        backend.record_stop("MyVM")

        first_args = mock_sync.call_args_list[0].args[0]
        second_args = mock_sync.call_args_list[1].args[0]
        assert "stop" in first_args
        assert "off" in second_args


# ---------------------------------------------------------------------------
# VBoxManageBackend.screenshot
# ---------------------------------------------------------------------------


class TestVBoxManageScreenshot:
    """Tests for VBoxManageBackend.screenshot."""

    def test_screenshot_calls_screenshot_via_vboxapi_once(self, mocker: MockerFixture, tmp_path: Path) -> None:
        """screenshot issues exactly 1 _screenshot_via_vboxapi call.

        Tests: VBoxManageBackend.screenshot call count.
        How: Patch _screenshot_via_vboxapi; call screenshot; assert call_count == 1.
        Why: A single vboxapi screenshot call is the entire operation.
        """
        from backends.vboxmanage import VBoxManageBackend

        mock_shot = mocker.patch("backends.vboxmanage._screenshot_via_vboxapi")
        backend = VBoxManageBackend()
        output_path = tmp_path / "frame.png"

        backend.screenshot("MyVM", output_path)

        assert mock_shot.call_count == 1

    def test_screenshot_returns_output_path(self, mocker: MockerFixture, tmp_path: Path) -> None:
        """screenshot returns the output_path passed in.

        Tests: VBoxManageBackend.screenshot return value.
        How: Patch _screenshot_via_vboxapi; verify returned value is the same Path.
        Why: Callers chain the returned path for copy/display; must be correct.
        """
        from backends.vboxmanage import VBoxManageBackend

        mocker.patch("backends.vboxmanage._screenshot_via_vboxapi")
        backend = VBoxManageBackend()
        output_path = tmp_path / "screen.png"

        result = backend.screenshot("MyVM", output_path)

        assert result == output_path

    def test_screenshot_passes_vm_name_and_path(self, mocker: MockerFixture, tmp_path: Path) -> None:
        """screenshot passes vm_name and output_path to _screenshot_via_vboxapi.

        Tests: VBoxManageBackend.screenshot argument content.
        How: Inspect call args for vm_name and output_path.
        Why: Wrong args would capture the wrong VM or wrong file.
        """
        from backends.vboxmanage import VBoxManageBackend

        mock_shot = mocker.patch("backends.vboxmanage._screenshot_via_vboxapi")
        backend = VBoxManageBackend()
        output_path = tmp_path / "out.png"

        backend.screenshot("VM1", output_path)

        call_args = mock_shot.call_args[0]
        assert call_args[0] == "VM1"
        assert call_args[1] == output_path


# ---------------------------------------------------------------------------
# VBoxManageBackend.cleanup
# ---------------------------------------------------------------------------


class TestVBoxManageCleanup:
    """Tests for VBoxManageBackend.cleanup — file age filtering and deletion."""

    def _create_old_file(self, directory: Path, name: str, age_days: int) -> Path:
        """Create a file and backdate its mtime to simulate age.

        Args:
            directory: Parent directory for the new file.
            name: Filename to create.
            age_days: How many days old the file should appear.

        Returns:
            Path to the created file.
        """
        f = directory / name
        f.write_text("dummy content")
        old_mtime = time.time() - (age_days * 86400)
        import os

        os.utime(f, (old_mtime, old_mtime))
        return f

    def test_cleanup_dry_run_returns_candidates_without_deleting(self, tmp_path: Path) -> None:
        """cleanup(dry_run=True) returns old files but does not delete them.

        Tests: VBoxManageBackend.cleanup dry_run mode.
        How: Create a file 10 days old; call cleanup with days=5, dry_run=True;
             verify file still exists and is in the returned list.
        Why: Dry-run must never modify the filesystem — users preview before committing.
        """
        from backends.vboxmanage import VBoxManageBackend

        backend = VBoxManageBackend()
        old_file = self._create_old_file(tmp_path, "old_recording.webm", age_days=10)

        candidates = backend.cleanup(tmp_path, older_than_days=5, dry_run=True)

        assert old_file in candidates
        assert old_file.exists()

    def test_cleanup_deletes_old_files_when_not_dry_run(self, tmp_path: Path) -> None:
        """cleanup(dry_run=False) deletes files older than older_than_days.

        Tests: VBoxManageBackend.cleanup deletion mode.
        How: Create a file 10 days old; call cleanup with days=5, dry_run=False;
             assert the file no longer exists.
        Why: The cleanup command must actually free disk space when confirmed.
        """
        from backends.vboxmanage import VBoxManageBackend

        backend = VBoxManageBackend()
        old_file = self._create_old_file(tmp_path, "stale.webm", age_days=10)

        candidates = backend.cleanup(tmp_path, older_than_days=5, dry_run=False)

        assert old_file in candidates
        assert not old_file.exists()

    def test_cleanup_does_not_delete_recent_files(self, tmp_path: Path) -> None:
        """cleanup leaves files newer than older_than_days untouched.

        Tests: VBoxManageBackend.cleanup age filter correctness.
        How: Create a file 1 day old; call cleanup with days=5, dry_run=False;
             assert file still exists and is not in candidates.
        Why: Recent recordings must not be accidentally deleted.
        """
        from backends.vboxmanage import VBoxManageBackend

        backend = VBoxManageBackend()
        recent_file = self._create_old_file(tmp_path, "recent.webm", age_days=1)

        candidates = backend.cleanup(tmp_path, older_than_days=5, dry_run=False)

        assert recent_file not in candidates
        assert recent_file.exists()

    def test_cleanup_returns_empty_list_for_nonexistent_scratch_dir(self, tmp_path: Path) -> None:
        """cleanup returns an empty list when scratch_dir does not exist.

        Tests: VBoxManageBackend.cleanup missing-directory edge case.
        How: Pass a path that was never created; assert empty return.
        Why: First-run environments have no scratch dir; must not crash.
        """
        from backends.vboxmanage import VBoxManageBackend

        backend = VBoxManageBackend()
        nonexistent = tmp_path / "no_such_dir"

        candidates = backend.cleanup(nonexistent, older_than_days=1, dry_run=True)

        assert candidates == []

    def test_vboxmanage_backend_constructs_without_vboxmanage_on_path(self) -> None:
        """VBoxManageBackend() constructs without requiring VBoxManage on PATH.

        Tests: Backend no longer uses subprocess/shutil.which for VBoxManage.
        How: Construct VBoxManageBackend; assert no exception.
        Why: Backend uses vboxapi and asyncssh; validation happens on first use.
        """
        from backends.vboxmanage import VBoxManageBackend

        backend = VBoxManageBackend()
        assert backend is not None
