"""Tests for vm-blackbox-record vm_capture.py pure functions and CLI commands.

Tests: _safe_name, _timestamp, _repo_root, _resolve_backend, record start/stop,
       and screenshot CLI subcommands.
Strategy: Pure-function tests exercise logic directly; CLI tests use
typer.testing.CliRunner with all GitPython and backend calls mocked via
pytest-mock so no VirtualBox or git repository is required.
"""

from __future__ import annotations

import re
import sys
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 vm_capture is loaded.
_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))


# ---------------------------------------------------------------------------
# _safe_name
# ---------------------------------------------------------------------------


class TestSafeName:
    """Tests for _safe_name() — character sanitisation for VM names."""

    def test_safe_name_replaces_spaces(self) -> None:
        """_safe_name replaces space characters with underscores.

        Tests: vm_capture._safe_name space handling.
        How: Pass 'My VM'; assert result is 'My_VM'.
        Why: Filenames with spaces break shell glob expansion in CI scripts.
        """
        import vm_capture

        result = vm_capture._safe_name("My VM")

        assert result == "My_VM"

    def test_safe_name_allows_alphanumeric_dash_underscore(self) -> None:
        """_safe_name leaves alphanumeric, dash, and underscore chars unchanged.

        Tests: vm_capture._safe_name passthrough for valid chars.
        How: Pass 'DSR-QA-Windows_01'; assert result is identical.
        Why: Standard VM names must survive sanitisation without mutation.
        """
        import vm_capture

        vm_name = "DSR-QA-Windows_01"
        result = vm_capture._safe_name(vm_name)

        assert result == vm_name

    def test_safe_name_replaces_special_characters(self) -> None:
        """_safe_name replaces dots, slashes, and colons with underscores.

        Tests: vm_capture._safe_name special-char replacement.
        How: Pass a name with '.', '/', ':'; assert all replaced with '_'.
        Why: These characters are illegal in filenames on most operating systems.
        """
        import vm_capture

        result = vm_capture._safe_name("vm.1/test:prod")

        assert result == "vm_1_test_prod"

    def test_safe_name_empty_string(self) -> None:
        """_safe_name returns empty string unchanged.

        Tests: vm_capture._safe_name edge case for empty input.
        How: Pass ''; assert result is ''.
        Why: Defensive test; callers may forward unvalidated user input.
        """
        import vm_capture

        result = vm_capture._safe_name("")

        assert result == ""


# ---------------------------------------------------------------------------
# _timestamp
# ---------------------------------------------------------------------------


class TestTimestamp:
    """Tests for _timestamp() — datetime formatting."""

    def test_timestamp_format(self) -> None:
        """_timestamp returns a string matching YYYYMMDD-HHMMSS.

        Tests: vm_capture._timestamp format compliance.
        How: Call _timestamp(); assert regex match on the result.
        Why: Downstream filename parsing depends on this exact format.
        """
        import vm_capture

        result = vm_capture._timestamp()

        assert re.match(r"\d{8}-\d{6}", result), f"Unexpected format: {result!r}"

    def test_timestamp_returns_string(self) -> None:
        """_timestamp return value is a plain str.

        Tests: vm_capture._timestamp return type.
        How: Call _timestamp(); assert isinstance(result, str).
        Why: Callers concatenate with Path strings; must be str, not datetime.
        """
        import vm_capture

        result = vm_capture._timestamp()

        assert isinstance(result, str)


# ---------------------------------------------------------------------------
# _repo_root
# ---------------------------------------------------------------------------


class TestRepoRoot:
    """Tests for _repo_root() — git repository detection."""

    def test_repo_root_raises_exit_when_not_in_git_repo(self, mocker: MockerFixture) -> None:
        """_repo_root raises typer.Exit(1) when not in a git repository.

        Tests: vm_capture._repo_root InvalidGitRepositoryError handling.
        How: Patch git.Repo to raise InvalidGitRepositoryError;
             assert typer.Exit is raised.
        Why: Running outside a git repo must fail fast with a clear error.
        """
        import typer
        from git import InvalidGitRepositoryError

        import vm_capture

        mocker.patch("vm_capture.Repo", side_effect=InvalidGitRepositoryError("not a git repository"))

        with pytest.raises(typer.Exit):
            vm_capture._repo_root()

    def test_repo_root_returns_path_when_git_succeeds(self, mocker: MockerFixture, tmp_path: Path) -> None:
        """_repo_root returns a Path object when Repo discovers a repo.

        Tests: vm_capture._repo_root happy path.
        How: Patch git.Repo to return a mock with working_tree_dir;
             assert the return value is the Path.
        Why: Subsequent scratch-dir construction depends on the returned Path.
        """
        import vm_capture

        mock_repo = mocker.MagicMock()
        mock_repo.working_tree_dir = str(tmp_path)
        mock_repo.common_dir = str(tmp_path / ".git")
        mocker.patch("vm_capture.Repo", return_value=mock_repo)

        result = vm_capture._repo_root()

        assert result == tmp_path


# ---------------------------------------------------------------------------
# _resolve_backend
# ---------------------------------------------------------------------------


class TestResolveBackend:
    """Tests for _resolve_backend() — backend lookup with error handling."""

    def test_resolve_backend_raises_exit_on_unknown(self, mocker: MockerFixture) -> None:
        """_resolve_backend raises typer.Exit(1) for an unknown backend name.

        Tests: vm_capture._resolve_backend error path.
        How: Set vm_capture._active_backend to 'bad'; call _resolve_backend();
             assert typer.Exit is raised.
        Why: Unknown --backend values must abort immediately with a message.
        """
        import typer

        import vm_capture

        original = vm_capture._state["backend"]
        try:
            vm_capture._state["backend"] = "bad"

            with pytest.raises(typer.Exit):
                vm_capture._resolve_backend()
        finally:
            vm_capture._state["backend"] = original

    def test_resolve_backend_returns_backend_for_vboxmanage(self, mocker: MockerFixture) -> None:
        """_resolve_backend returns a VBoxManageBackend for the default backend.

        Tests: vm_capture._resolve_backend happy path.
        How: Ensure _active_backend is 'vboxmanage'; call _resolve_backend();
             assert the returned object is a VBoxManageBackend instance.
        Why: The default backend must resolve without error on a clean import.
        """
        import vm_capture
        from backends.vboxmanage import VBoxManageBackend

        original = vm_capture._state["backend"]
        try:
            vm_capture._state["backend"] = "vboxmanage"

            result = vm_capture._resolve_backend()
        finally:
            vm_capture._state["backend"] = original

        assert isinstance(result, VBoxManageBackend)


# ---------------------------------------------------------------------------
# CLI — record start
# ---------------------------------------------------------------------------


class TestRecordStartCli:
    """Tests for the 'record start <vm>' CLI subcommand via CliRunner."""

    def test_record_start_calls_backend(self, mocker: MockerFixture, tmp_path: Path) -> None:
        """'record start' invokes backend.record_start once.

        Tests: vm_capture record start CLI command end-to-end.
        How: Mock _repo_root to return tmp_path; mock get_backend to return a
             mock backend; run 'record start MyVM' via CliRunner; assert
             backend.record_start was called once.
        Why: The CLI must wire repo root, safe name, timestamp, and backend together.
        """
        from typer.testing import CliRunner

        import vm_capture

        mocker.patch("vm_capture._repo_root", return_value=tmp_path)
        mock_backend = mocker.Mock()
        mocker.patch("vm_capture.get_backend", return_value=mock_backend)

        runner = CliRunner()
        result = runner.invoke(vm_capture.app, ["record", "start", "MyVM"])

        assert result.exit_code == 0, result.output
        mock_backend.record_start.assert_called_once()

    def test_record_start_uses_safe_name_in_output_path(self, mocker: MockerFixture, tmp_path: Path) -> None:
        """'record start' sanitises the VM name in the output path.

        Tests: vm_capture record start path construction.
        How: Use a VM name containing spaces; assert the recorded output path
             contains underscores, not spaces.
        Why: File paths with spaces break downstream shell invocations.
        """
        from typer.testing import CliRunner

        import vm_capture

        mocker.patch("vm_capture._repo_root", return_value=tmp_path)
        mock_backend = mocker.Mock()
        mocker.patch("vm_capture.get_backend", return_value=mock_backend)

        runner = CliRunner()
        runner.invoke(vm_capture.app, ["record", "start", "My VM"])

        call_args = mock_backend.record_start.call_args
        output_path: Path = call_args[0][1]
        assert " " not in str(output_path)


# ---------------------------------------------------------------------------
# CLI — screenshot
# ---------------------------------------------------------------------------


class TestScreenshotCli:
    """Tests for the 'screenshot <vm>' CLI subcommand via CliRunner."""

    def test_screenshot_calls_backend(self, mocker: MockerFixture, tmp_path: Path) -> None:
        """'screenshot' invokes backend.screenshot once.

        Tests: vm_capture screenshot CLI command end-to-end.
        How: Mock _repo_root and get_backend; run 'screenshot MyVM'; assert
             backend.screenshot was called once.
        Why: The CLI must delegate the actual capture to the backend without
             performing any VirtualBox calls itself.
        """
        from typer.testing import CliRunner

        import vm_capture

        mocker.patch("vm_capture._repo_root", return_value=tmp_path)
        mock_backend = mocker.Mock()
        mocker.patch("vm_capture.get_backend", return_value=mock_backend)

        runner = CliRunner()
        result = runner.invoke(vm_capture.app, ["screenshot", "MyVM"])

        assert result.exit_code == 0, result.output
        mock_backend.screenshot.assert_called_once()

    def test_screenshot_output_contains_path(self, mocker: MockerFixture, tmp_path: Path) -> None:
        """'screenshot' prints the output path to stdout.

        Tests: vm_capture screenshot CLI output.
        How: Mock _repo_root and get_backend; run screenshot; assert 'Screenshot:'
             appears in the output.
        Why: Users pipe this output to scripts that parse the screenshot path.
        """
        from typer.testing import CliRunner

        import vm_capture

        mocker.patch("vm_capture._repo_root", return_value=tmp_path)
        mock_backend = mocker.Mock()
        mocker.patch("vm_capture.get_backend", return_value=mock_backend)

        runner = CliRunner()
        result = runner.invoke(vm_capture.app, ["screenshot", "MyVM"])

        assert "Screenshot:" in result.output


# ---------------------------------------------------------------------------
# CLI — record stop
# ---------------------------------------------------------------------------


class TestRecordStopCli:
    """Tests for the 'record stop <vm>' CLI subcommand via CliRunner."""

    def test_record_stop_calls_backend(self, mocker: MockerFixture, tmp_path: Path) -> None:
        """'record stop' invokes backend.record_stop once.

        Tests: vm_capture record stop CLI command.
        How: Mock _repo_root and get_backend; run 'record stop MyVM'; assert
             backend.record_stop called once with the VM name.
        Why: Stop must delegate to the backend — not call VBoxManage directly.
        """
        from typer.testing import CliRunner

        import vm_capture

        mocker.patch("vm_capture._repo_root", return_value=tmp_path)
        mock_backend = mocker.Mock()
        mocker.patch("vm_capture.get_backend", return_value=mock_backend)

        runner = CliRunner()
        result = runner.invoke(vm_capture.app, ["record", "stop", "MyVM"])

        assert result.exit_code == 0, result.output
        mock_backend.record_stop.assert_called_once_with("MyVM")
