"""Tests for the which CLI module."""

import os
import subprocess
from unittest.mock import MagicMock, patch

from pytola_system.which.cli import IS_WINDOWS, FindResult, find_executable, is_builtin, main


class TestIsBuiltin:
    """Test cases for the is_builtin function."""

    def test_windows_builtin_commands(self) -> None:
        """Test detection of Windows built-in commands."""
        if IS_WINDOWS:
            assert is_builtin("dir") is True, f"Expected is_builtin('dir') to be True, got {is_builtin('dir')}"
            assert is_builtin("copy") is True, f"Expected is_builtin('copy') to be True, got {is_builtin('copy')}"
            assert is_builtin("del") is True, f"Expected is_builtin('del') to be True, got {is_builtin('del')}"
            assert is_builtin("echo") is True, f"Expected is_builtin('echo') to be True, got {is_builtin('echo')}"
            assert is_builtin("cd") is True, f"Expected is_builtin('cd') to be True, got {is_builtin('cd')}"

    def test_non_builtin_commands(self) -> None:
        """Test non built-in commands."""
        assert is_builtin("python") is False, f"Expected is_builtin('python') to be False, got {is_builtin('python')}"
        assert is_builtin("notepad") is False, (
            f"Expected is_builtin('notepad') to be False, got {is_builtin('notepad')}"
        )
        assert is_builtin("calculator") is False, (
            f"Expected is_builtin('calculator') to be False, got {is_builtin('calculator')}"
        )

    def test_case_insensitive(self) -> None:
        """Test that command checking is case insensitive."""
        if IS_WINDOWS:
            assert is_builtin("DIR") is True, f"Expected is_builtin('DIR') to be True, got {is_builtin('DIR')}"
            assert is_builtin("Dir") is True, f"Expected is_builtin('Dir') to be True, got {is_builtin('Dir')}"
            assert is_builtin("dir") is True, f"Expected is_builtin('dir') to be True, got {is_builtin('dir')}"

    def test_empty_command(self) -> None:
        """Test empty command string."""
        assert is_builtin("") is False, f"Expected is_builtin('') to be False, got {is_builtin('')}"

    def test_special_characters(self) -> None:
        """Test commands with special characters."""
        assert is_builtin("@invalid") is False, (
            f"Expected is_builtin('@invalid') to be False, got {is_builtin('@invalid')}"
        )
        assert is_builtin("#test") is False, f"Expected is_builtin('#test') to be False, got {is_builtin('#test')}"

    def test_all_windows_builtins(self) -> None:
        """Test all Windows built-in commands are recognized."""
        if IS_WINDOWS:
            # Test a sample of built-in commands
            builtin_samples = ["assoc", "attrib", "break", "bcdedit", "cacls"]
            for cmd in builtin_samples:
                assert is_builtin(cmd) is True, f"{cmd} should be recognized as builtin"


class TestFindResult:
    """Test cases for the FindResult dataclass."""

    def test_find_result_with_path(self) -> None:
        """Test FindResult when path is found."""
        result = FindResult(name="python", path="C:\\Python\\python.exe")
        assert result.name == "python", f"Expected name 'python', got '{result.name}'"
        assert result.path == "C:\\Python\\python.exe", f"Expected path 'C:\\Python\\python.exe', got '{result.path}'"
        assert "[OK]" in str(result), f"Expected '[OK]' in str(result), got '{result!s}'"
        assert "python" in str(result), f"Expected 'python' in str(result), got '{result!s}'"

    def test_find_result_without_path(self) -> None:
        """Test FindResult when path is not found."""
        result = FindResult(name="nonexistent", path=None)
        assert result.name == "nonexistent", f"Expected name 'nonexistent', got '{result.name}'"
        assert result.path is None, f"Expected path to be None, got {result.path}"
        assert "[NOT FOUND]" in str(result), f"Expected '[NOT FOUND]' in str(result), got '{result!s}'"
        assert "nonexistent" in str(result), f"Expected 'nonexistent' in str(result), got '{result!s}'"

    def test_find_result_with_multiple_paths(self) -> None:
        """Test FindResult with multiple paths (Windows case)."""
        paths = ["C:\\Python\\python.exe", "D:\\Tools\\python.exe"]
        result = FindResult(name="python", path=paths)
        assert result.name == "python", f"Expected name 'python', got '{result.name}'"
        assert result.path == paths, f"Expected path {paths}, got {result.path}"
        assert "[OK]" in str(result), f"Expected '[OK]' in str(result), got '{result!s}'"

    def test_find_result_empty_path_list(self) -> None:
        """Test FindResult with empty path list."""
        result = FindResult(name="test", path=[])
        # Empty list is falsy, so should show NOT FOUND
        assert not result.path, f"Expected path to be falsy (empty list), got {result.path}"
        assert "[NOT FOUND]" in str(result), f"Expected '[NOT FOUND]' in str(result), got '{result!s}'"

    def test_find_result_special_paths(self) -> None:
        """Test FindResult with special path values."""
        # Test with path containing spaces
        result = FindResult(name="program", path="C:\\Program Files\\app.exe")
        assert "Program Files" in result.path, f"Expected 'Program Files' in result.path, got '{result.path}'"

        # Test with Unix path
        result = FindResult(name="binary", path="/usr/local/bin/binary")
        assert "/usr/local/bin/binary" in result.path, (
            f"Expected '/usr/local/bin/binary' in result.path, got '{result.path}'"
        )


class TestFindExecutable:
    """Test cases for the find_executable function."""

    @patch("subprocess.run")
    def test_find_executable_success(self, mock_run) -> None:
        """Test finding executable that exists."""
        # Mock successful subprocess call
        mock_run.return_value = subprocess.CompletedProcess(
            args=["which", "python"],
            returncode=0,
            stdout="/usr/bin/python",
            stderr="",
        )

        result = find_executable("python", fuzzy_mode=False)
        assert result.name == "python", f"Expected name 'python', got '{result.name}'"
        # On Windows, paths are returned as lists, on Unix as strings
        if IS_WINDOWS:
            assert "/usr/bin/python" in result.path, f"Expected '/usr/bin/python' in result.path, got {result.path}"
        else:
            assert result.path == "/usr/bin/python", f"Expected path '/usr/bin/python', got {result.path}"

    @patch("subprocess.run")
    def test_find_executable_not_found(self, mock_run) -> None:
        """Test finding executable that doesn't exist."""
        # Mock failed subprocess call
        error = subprocess.CalledProcessError(returncode=1, cmd=["which", "nonexistent"])
        error.stdout = ""
        error.stderr = ""
        mock_run.side_effect = error

        with patch("os.access", return_value=False):
            result = find_executable("nonexistent", fuzzy_mode=False)
            assert result.name == "nonexistent", f"Expected name 'nonexistent', got '{result.name}'"
            assert result.path is None, f"Expected path to be None, got {result.path}"

    @patch("subprocess.run")
    def test_find_executable_fuzzy_mode(self, mock_run) -> None:
        """Test finding executable with fuzzy matching."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=["which", "*python*"],
            returncode=0,
            stdout="/usr/bin/python3\n/usr/bin/python3.9",
            stderr="",
        )

        result = find_executable("python", fuzzy_mode=True)
        assert result.name == "python", f"Expected name 'python', got '{result.name}'"
        # Verify fuzzy matching pattern was used
        mock_run.assert_called_once()
        called_args = mock_run.call_args[0][0]
        assert "*python*" in called_args[1], f"Expected '*python*' in called_args[1], got {called_args[1]}"

    @patch("subprocess.run")
    @patch("platform.system")
    def test_find_executable_windows_builtin(self, mock_system, mock_run) -> None:
        """Test finding Windows built-in command."""
        mock_system.return_value = "Windows"
        error = subprocess.CalledProcessError(returncode=1, cmd=["where", "dir"])
        error.stdout = ""
        error.stderr = ""
        mock_run.side_effect = error

        # Patch IS_WINDOWS constant (not a function, so use direct assignment)
        with patch("pytola_system.which.cli.IS_WINDOWS", True):
            result = find_executable("dir", fuzzy_mode=False)
            assert result.name == "dir", f"Expected name 'dir', got '{result.name}'"
            assert result.path == "C:\\Windows\\System32\\cmd.exe", (
                f"Expected path 'C:\\Windows\\System32\\cmd.exe', got {result.path}"
            )

    @patch("subprocess.run")
    @patch("os.access")
    @patch("platform.system")
    def test_find_executable_unix_fallback(self, mock_system, mock_access, mock_run) -> None:
        """Test Unix fallback when which command fails."""
        mock_system.return_value = "Linux"
        error = subprocess.CalledProcessError(returncode=1, cmd=["which", "custom_cmd"])
        error.stdout = ""
        error.stderr = ""
        mock_run.side_effect = error
        # Mock os.access to return True for the specific path check
        mock_access.return_value = True

        # Patch IS_WINDOWS constant (not a function, so use direct assignment)
        with patch("pytola_system.which.cli.IS_WINDOWS", False):
            result = find_executable("custom_cmd", fuzzy_mode=False)
            assert result.name == "custom_cmd", f"Expected name 'custom_cmd', got '{result.name}'"
            assert result.path == "/usr/bin/custom_cmd", f"Expected path '/usr/bin/custom_cmd', got {result.path}"
            # Verify os.access was called with the correct path and mode
            mock_access.assert_called_once_with("/usr/bin/custom_cmd", os.X_OK)

    @patch("subprocess.run")
    @patch("platform.system")
    def test_find_executable_unix_multiple_paths(self, mock_system, mock_run) -> None:
        """Test Unix multiple path results with empty lines."""
        mock_system.return_value = "Linux"
        mock_run.return_value = subprocess.CompletedProcess(
            args=["which", "python"],
            returncode=0,
            stdout="/usr/bin/python3\n\n/usr/bin/python3.9\n  \n",
            stderr="",
        )

        result = find_executable("python", fuzzy_mode=False)
        assert result.name == "python", f"Expected name 'python', got '{result.name}'"
        assert isinstance(result.path, list), f"Expected result.path to be list, got {type(result.path)}"
        # Should filter out empty lines and whitespace-only lines
        assert len(result.path) == 2, f"Expected 2 paths, got {len(result.path)}"
        assert "/usr/bin/python3" in result.path, f"Expected '/usr/bin/python3' in result.path, got {result.path}"
        assert "/usr/bin/python3.9" in result.path, f"Expected '/usr/bin/python3.9' in result.path, got {result.path}"


class TestIntegration:
    """Integration tests for the which module."""

    def test_find_common_executables(self) -> None:
        """Test finding common executables that should exist."""
        # Test with common system executables
        common_cmds = ["python", "pip"] if not IS_WINDOWS else ["cmd", "dir"]

        for cmd in common_cmds:
            result = find_executable(cmd, fuzzy_mode=False)
            # We don't assert success as the executable might not be in PATH
            # but we verify the function returns a valid FindResult
            assert isinstance(result, FindResult), f"Expected FindResult instance, got {type(result)}"
            assert result.name == cmd, f"Expected name '{cmd}', got '{result.name}'"

    def test_result_string_representation(self) -> None:
        """Test that result string representation is informative."""
        # Test found case
        found_result = FindResult("test", "/path/to/test")
        found_str = str(found_result)
        assert "[OK]" in found_str, f"Expected '[OK]' in found_str, got '{found_str}'"
        assert "test" in found_str, f"Expected 'test' in found_str, got '{found_str}'"
        assert "/path/to/test" in found_str, f"Expected '/path/to/test' in found_str, got '{found_str}'"

        # Test not found case
        not_found_result = FindResult("missing", None)
        not_found_str = str(not_found_result)
        assert "[NOT FOUND]" in not_found_str, f"Expected '[NOT FOUND]' in not_found_str, got '{not_found_str}'"
        assert "missing" in not_found_str, f"Expected 'missing' in not_found_str, got '{not_found_str}'"


class TestMain:
    """Test cases for the main function."""

    @patch("pytola_system.which.cli.find_executable")
    @patch("pytola_system.which.cli.logger")
    def test_main_single_command(self, mock_logger: MagicMock, mock_find: MagicMock) -> None:
        """Test main function with single command."""
        # Setup mock return value
        mock_find.return_value = FindResult(name="python", path="/usr/bin/python")

        with patch("sys.argv", ["which", "python"]):
            main()

        # Verify find_executable was called correctly
        mock_find.assert_called_once_with("python", fuzzy_mode=False)

        # Verify logger was called
        assert mock_logger.info.call_count >= 1
        mock_logger.info.assert_any_call("Searching for executable paths...")

    @patch("pytola_system.which.cli.find_executable")
    def test_main_multiple_commands(self, mock_find: MagicMock) -> None:
        """Test main function with multiple commands."""
        # Setup mock to return different results
        mock_find.side_effect = [
            FindResult(name="python", path="/usr/bin/python"),
            FindResult(name="pip", path="/usr/bin/pip"),
        ]

        with patch("sys.argv", ["which", "python", "pip"]):
            main()

        # Verify find_executable was called twice
        assert mock_find.call_count == 2
        mock_find.assert_any_call("python", fuzzy_mode=False)
        mock_find.assert_any_call("pip", fuzzy_mode=False)

    @patch("pytola_system.which.cli.find_executable")
    def test_main_with_fuzzy_mode(self, mock_find: MagicMock) -> None:
        """Test main function with fuzzy matching enabled."""
        mock_find.return_value = FindResult(name="python", path="/usr/bin/python3")

        with patch("sys.argv", ["which", "python", "-f"]):
            main()

        # Verify fuzzy mode was enabled
        mock_find.assert_called_once_with("python", fuzzy_mode=True)

    @patch("pytola_system.which.cli.find_executable")
    @patch("pytola_system.which.cli.logger")
    def test_main_command_not_found(self, mock_logger: MagicMock, mock_find: MagicMock) -> None:
        """Test main function when command is not found."""
        mock_find.return_value = FindResult(name="missing", path=None)

        with patch("sys.argv", ["which", "missing"]):
            main()

        # Verify the call was made even for missing command
        mock_find.assert_called_once_with("missing", fuzzy_mode=False)
        mock_logger.info.assert_any_call("Searching for executable paths...")
