"""Tests for the taskkill CLI module."""

from __future__ import annotations

import logging
import subprocess
from unittest.mock import MagicMock, call, patch

import pytest

from pytola_system.taskkill.cli import (
    DEFAULT_MATCH_THRESHOLD,
    FORCE_MATCH_THRESHOLD,
    ProcessInfo,
    TaskkillCommand,
    main,
    parse_args,
)


@pytest.fixture(scope="session")
def sample_processes() -> list[ProcessInfo]:
    """Fixture providing sample process data for testing.

    Returns
    -------
        list[ProcessInfo]: A list of ProcessInfo objects.
    """
    return [
        ProcessInfo("python.exe", "1234"),
        ProcessInfo("python3.exe", "5678"),
        ProcessInfo("notepad.exe", "9012"),
        ProcessInfo("chrome.exe", "1357"),
        ProcessInfo("test-app.exe", "2468"),
        ProcessInfo("test_app.exe", "3579"),
    ]


@pytest.fixture
def taskkill_command() -> TaskkillCommand:
    """Fixture providing a fresh TaskkillCommand instance for each test.

    Returns
    -------
        TaskkillCommand: A new TaskkillCommand instance.
    """
    return TaskkillCommand()


@pytest.fixture(scope="session")
def mock_processes() -> list[ProcessInfo]:
    """Fixture providing mock process data for kill tests.

    Returns
    -------
        list[ProcessInfo]: A list of ProcessInfo objects.
    """
    return [ProcessInfo("test.exe", "1234"), ProcessInfo("test.exe", "5678")]


class TestProcessInfo:
    """Test cases for the ProcessInfo dataclass."""

    def test_process_info_creation(self) -> None:
        """Test ProcessInfo object creation and attributes."""
        process = ProcessInfo(name="python.exe", pid="1234")
        assert process.name == "python.exe", f"Expected name 'python.exe', got '{process.name}'"
        assert process.pid == "1234", f"Expected pid '1234', got '{process.pid}'"

    def test_process_info_equality(self) -> None:
        """Test ProcessInfo equality comparison."""
        process1 = ProcessInfo(name="python.exe", pid="1234")
        process2 = ProcessInfo(name="python.exe", pid="1234")
        process3 = ProcessInfo(name="notepad.exe", pid="1234")

        assert process1 == process2, f"Expected process1 == process2, got {process1} != {process2}"
        assert process1 != process3, f"Expected process1 != process3, got {process1} == {process3}"
        assert hash(process1) == hash(process2), (
            f"Expected hash(process1) == hash(process2), got {hash(process1)} != {hash(process2)}"
        )

    def test_process_info_repr(self) -> None:
        """Test ProcessInfo string representation."""
        process = ProcessInfo(name="python.exe", pid="1234")
        repr_str = repr(process)
        assert "ProcessInfo" in repr_str, f"Expected 'ProcessInfo' in repr_str, got '{repr_str}'"
        assert "python.exe" in repr_str, f"Expected 'python.exe' in repr_str, got '{repr_str}'"
        assert "1234" in repr_str, f"Expected '1234' in repr_str, got '{repr_str}'"


class TestTaskkillCommandInitialization:
    """Test cases for TaskkillCommand initialization."""

    @patch("pytola_system.taskkill.cli.IS_WINDOWS", True)
    @patch("pytola_system.taskkill.cli.TaskkillCommand._get_process_list_windows")
    def test_initialization_windows(self, mock_get_processes: MagicMock) -> None:
        """Test TaskkillCommand initialization on Windows."""
        cmd = TaskkillCommand()
        assert isinstance(cmd, TaskkillCommand), f"Expected TaskkillCommand instance, got {type(cmd)}"
        assert cmd.processes == [], f"Expected empty processes list, got {cmd.processes}"
        assert cmd.force is False, f"Expected force to be False, got {cmd.force}"
        mock_get_processes.assert_called_once()

    @patch("pytola_system.taskkill.cli.IS_WINDOWS", False)
    @patch("pytola_system.taskkill.cli.TaskkillCommand._get_process_list_unix")
    def test_initialization_unix(self, mock_get_processes: MagicMock) -> None:
        """Test TaskkillCommand initialization on Unix."""
        cmd = TaskkillCommand()
        assert isinstance(cmd, TaskkillCommand), f"Expected TaskkillCommand instance, got {type(cmd)}"
        assert cmd.processes == [], f"Expected empty processes list, got {cmd.processes}"
        assert cmd.force is False, f"Expected force to be False, got {cmd.force}"
        mock_get_processes.assert_called_once()

    def test_initialization_with_force_flag(self) -> None:
        """Test TaskkillCommand initialization with force flag."""
        cmd = TaskkillCommand(force=True)
        assert cmd.force is True, f"Expected force to be True, got {cmd.force}"
        assert cmd.match_threshold == FORCE_MATCH_THRESHOLD, (
            f"Expected match_threshold {FORCE_MATCH_THRESHOLD}, got {cmd.match_threshold}"
        )

    def test_initialization_without_force_flag(self) -> None:
        """Test TaskkillCommand initialization without force flag."""
        cmd = TaskkillCommand()
        assert cmd.force is False, f"Expected force to be False, got {cmd.force}"
        assert cmd.match_threshold == DEFAULT_MATCH_THRESHOLD, (
            f"Expected match_threshold {DEFAULT_MATCH_THRESHOLD}, got {cmd.match_threshold}"
        )


class TestTaskkillCommandMatchThreshold:
    """Test cases for match threshold property."""

    def test_default_match_threshold(self, taskkill_command) -> None:
        """Test default match threshold value."""
        cmd = taskkill_command
        assert cmd.match_threshold == DEFAULT_MATCH_THRESHOLD, (
            f"Expected {DEFAULT_MATCH_THRESHOLD}, got {cmd.match_threshold}"
        )

    def test_force_match_threshold(self) -> None:
        """Test force match threshold value."""
        cmd = TaskkillCommand(force=True)
        assert cmd.match_threshold == FORCE_MATCH_THRESHOLD, (
            f"Expected {FORCE_MATCH_THRESHOLD}, got {cmd.match_threshold}"
        )

    def test_threshold_consistency(self, taskkill_command) -> None:
        """Test that threshold remains consistent."""
        cmd = taskkill_command
        # Cache the property access to avoid repeated computation
        threshold = cmd.match_threshold
        assert threshold == cmd.match_threshold, f"Expected threshold consistency: {threshold} != {cmd.match_threshold}"


class TestTaskkillCommandMatch:
    """Test cases for the match method."""

    def test_match_exact_name(self, sample_processes, taskkill_command) -> None:
        """Test exact name matching."""
        cmd = taskkill_command
        cmd.processes = sample_processes[:3]  # Use first 3 processes

        matches = cmd.match("python.exe")
        assert len(matches) == 1
        assert matches[0].name == "python.exe"
        assert matches[0].pid == "1234"

    def test_match_case_insensitive(self, sample_processes, taskkill_command) -> None:
        """Test case insensitive matching."""
        cmd = taskkill_command
        # Create a process with uppercase name for testing
        uppercase_process = ProcessInfo("Python.EXE", "1234")
        cmd.processes = [uppercase_process]

        matches = cmd.match("python.exe")
        assert len(matches) == 1
        assert matches[0].name == "Python.EXE"

    def test_match_fuzzy_pattern(self, sample_processes, taskkill_command) -> None:
        """Test fuzzy pattern matching (automatic wildcard)."""
        cmd = taskkill_command
        cmd.processes = sample_processes[:3]  # Use first 3 processes

        matches = cmd.match("python")
        assert len(matches) == 2
        names = [p.name for p in matches]
        assert "python.exe" in names
        assert "python3.exe" in names

    def test_match_explicit_wildcard(self, sample_processes, taskkill_command) -> None:
        """Test explicit wildcard pattern matching."""
        # Add ipython to test data
        extended_processes = [*sample_processes[:2], ProcessInfo("ipython.exe", "9012")]
        cmd = taskkill_command
        cmd.processes = extended_processes

        matches = cmd.match("python*")
        assert len(matches) == 2
        names = [p.name for p in matches]
        assert "python.exe" in names
        assert "python3.exe" in names
        assert "ipython.exe" not in names

    def test_match_no_matches(self, sample_processes, taskkill_command) -> None:
        """Test matching when no processes match."""
        cmd = taskkill_command
        cmd.processes = [sample_processes[0]]  # Use python.exe

        matches = cmd.match("nonexistent")
        assert len(matches) == 0

    def test_match_empty_process_list(self, taskkill_command) -> None:
        """Test matching with empty process list."""
        cmd = taskkill_command
        cmd.processes = []

        matches = cmd.match("anything")
        assert len(matches) == 0

    def test_match_special_characters(self, sample_processes, taskkill_command) -> None:
        """Test matching with special characters in process names."""
        cmd = taskkill_command
        cmd.processes = sample_processes[4:6]  # Use test-app.exe and test_app.exe

        matches = cmd.match("test-app")
        assert len(matches) == 1
        assert matches[0].name == "test-app.exe"


class TestTaskkillCommandKill:
    """Test cases for the kill method."""

    @patch("pytola_system.taskkill.cli.TaskkillCommand.kill_by_pid")
    def test_kill_successful_termination(self, mock_kill_by_pid: MagicMock, mock_processes, taskkill_command) -> None:
        """Test successful process termination."""
        mock_kill_by_pid.return_value = True
        cmd = taskkill_command
        cmd.processes = mock_processes

        with patch.object(cmd, "match", return_value=mock_processes):
            result = cmd.kill("test.exe")

        assert result is True
        assert mock_kill_by_pid.call_count == 2
        mock_kill_by_pid.assert_has_calls([call("1234"), call("5678")])

    @patch("pytola_system.taskkill.cli.TaskkillCommand.kill_by_pid")
    def test_kill_partial_failure(self, mock_kill_by_pid: MagicMock, mock_processes, taskkill_command) -> None:
        """Test partial failure in process termination."""
        mock_kill_by_pid.side_effect = [True, False]  # First succeeds, second fails
        cmd = taskkill_command
        cmd.processes = mock_processes

        with patch.object(cmd, "match", return_value=mock_processes):
            result = cmd.kill("test.exe")

        assert result is False
        assert mock_kill_by_pid.call_count == 2

    @patch("pytola_system.taskkill.cli.TaskkillCommand.kill_by_pid")
    def test_kill_no_matching_processes(self, mock_kill_by_pid: MagicMock, taskkill_command) -> None:
        """Test killing when no processes match."""
        cmd = taskkill_command

        with patch.object(cmd, "match", return_value=[]):
            result = cmd.kill("nonexistent.exe")

        assert result is False
        mock_kill_by_pid.assert_not_called()

    @pytest.fixture(scope="class")
    def many_processes(self) -> list[ProcessInfo]:
        """Fixture providing many processes to test threshold behavior.

        Returns
        -------
            list[ProcessInfo]: A list of ProcessInfo objects.
        """
        return [ProcessInfo(f"test{i}.exe", str(1000 + i)) for i in range(15)]

    @patch("pytola_system.taskkill.cli.TaskkillCommand.kill_by_pid")
    def test_kill_exceeds_threshold(self, mock_kill_by_pid: MagicMock, many_processes) -> None:
        """Test killing when match count exceeds threshold."""
        cmd = TaskkillCommand()
        cmd.processes = many_processes

        with patch.object(cmd, "match", return_value=many_processes):
            result = cmd.kill("test*")

        assert result is False
        mock_kill_by_pid.assert_not_called()

    @patch("pytola_system.taskkill.cli.TaskkillCommand.kill_by_pid")
    def test_kill_force_mode_exceeds_threshold(self, mock_kill_by_pid: MagicMock, many_processes) -> None:
        """Test killing with force mode when exceeding threshold."""
        mock_kill_by_pid.return_value = True
        cmd = TaskkillCommand(force=True)
        cmd.processes = many_processes

        with patch.object(cmd, "match", return_value=many_processes):
            result = cmd.kill("test*")

        assert result is True
        assert mock_kill_by_pid.call_count == 15


class TestTaskkillCommandPlatformSpecific:
    """Test cases for platform-specific process management."""

    @patch("pytola_system.taskkill.cli.IS_WINDOWS", True)
    @patch("pytola_system.taskkill.cli.subprocess.run")
    def test_get_process_list_windows_success(self, mock_run: MagicMock) -> None:
        """Test Windows process list retrieval success."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=["tasklist"],
            returncode=0,
            stdout='"python.exe","1234"\n"notepad.exe","5678"\n',
            stderr="",
        )

        cmd = TaskkillCommand()
        assert len(cmd.processes) == 2
        assert cmd.processes[0].name == "python.exe"
        assert cmd.processes[0].pid == "1234"
        assert cmd.processes[1].name == "notepad.exe"
        assert cmd.processes[1].pid == "5678"

    @patch("pytola_system.taskkill.cli.IS_WINDOWS", True)
    @patch("pytola_system.taskkill.cli.subprocess.run")
    def test_get_process_list_windows_encoding_fallback(self, mock_run: MagicMock) -> None:
        """Test Windows process list encoding fallback."""
        # First call fails with UnicodeDecodeError, second succeeds
        mock_run.side_effect = [
            UnicodeDecodeError("gbk", b"", 0, 1, "codec error"),
            subprocess.CompletedProcess(
                args=["tasklist"],
                returncode=0,
                stdout='"python.exe","1234"\n',
                stderr="",
            ),
        ]

        cmd = TaskkillCommand()
        assert len(cmd.processes) == 1
        assert cmd.processes[0].name == "python.exe"

    @patch("pytola_system.taskkill.cli.IS_WINDOWS", False)
    @patch("pytola_system.taskkill.cli.subprocess.run")
    def test_get_process_list_unix_success(self, mock_run: MagicMock) -> None:
        """Test Unix process list retrieval success."""
        mock_run.return_value = subprocess.CompletedProcess(
            args=["ps"],
            returncode=0,
            stdout="1234 python\n5678 notepad\n",
            stderr="",
        )

        cmd = TaskkillCommand()
        assert len(cmd.processes) == 2
        assert cmd.processes[0].name == "python"
        assert cmd.processes[0].pid == "1234"
        assert cmd.processes[1].name == "notepad"
        assert cmd.processes[1].pid == "5678"

    @patch("pytola_system.taskkill.cli.IS_WINDOWS", True)
    @patch("pytola_system.taskkill.cli.subprocess.run")
    @patch("pytola_system.taskkill.cli.TaskkillCommand._kill_by_pid_windows")
    def test_kill_by_pid_windows(self, mock_kill_windows: MagicMock, mock_run: MagicMock) -> None:
        """Test Windows process termination."""
        mock_kill_windows.return_value = True
        mock_run.return_value = subprocess.CompletedProcess(
            args=["tasklist"],
            returncode=0,
            stdout='"test.exe","1234"\n',
            stderr="",
        )

        cmd = TaskkillCommand()
        result = cmd.kill_by_pid("1234")

        mock_kill_windows.assert_called_once_with("1234")
        assert result is True

    @patch("pytola_system.taskkill.cli.IS_WINDOWS", False)
    @patch("pytola_system.taskkill.cli.subprocess.run")
    @patch("pytola_system.taskkill.cli.TaskkillCommand._kill_by_pid_unix")
    def test_kill_by_pid_unix(self, mock_kill_unix: MagicMock, mock_run: MagicMock) -> None:
        """Test Unix process termination."""
        mock_kill_unix.return_value = True
        mock_run.return_value = subprocess.CompletedProcess(
            args=["ps"],
            returncode=0,
            stdout="1234 test\n",
            stderr="",
        )

        cmd = TaskkillCommand()
        result = cmd.kill_by_pid("1234")

        mock_kill_unix.assert_called_once_with("1234")
        assert result is True


class TestParseArgs:
    """Test cases for argument parsing."""

    def test_parse_args_single_process(self) -> None:
        """Test parsing single process argument."""
        with patch("sys.argv", ["taskk", "python.exe"]):
            args = parse_args()
            assert args.processes == ["python.exe"]
            assert args.debug is False
            assert args.force is False

    def test_parse_args_multiple_processes(self) -> None:
        """Test parsing multiple process arguments."""
        with patch("sys.argv", ["taskk", "python.exe", "notepad.exe"]):
            args = parse_args()
            assert args.processes == ["python.exe", "notepad.exe"]
            assert args.debug is False
            assert args.force is False

    def test_parse_args_with_debug_flag(self) -> None:
        """Test parsing with debug flag."""
        with patch("sys.argv", ["taskk", "python.exe", "--debug"]):
            args = parse_args()
            assert args.processes == ["python.exe"]
            assert args.debug is True
            assert args.force is False

    def test_parse_args_with_force_flag(self) -> None:
        """Test parsing with force flag."""
        with patch("sys.argv", ["taskk", "python.exe", "--force"]):
            args = parse_args()
            assert args.processes == ["python.exe"]
            assert args.debug is False
            assert args.force is True

    def test_parse_args_with_short_flags(self) -> None:
        """Test parsing with short flag versions."""
        with patch("sys.argv", ["taskk", "python.exe", "-d", "-f"]):
            args = parse_args()
            assert args.processes == ["python.exe"]
            assert args.debug is True
            assert args.force is True


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

    @patch("pytola_system.taskkill.cli.parse_args")
    @patch("pytola_system.taskkill.cli.TaskkillCommand")
    @patch("pytola_system.taskkill.cli.logger")
    def test_main_basic_execution(
        self,
        mock_logger: MagicMock,
        mock_taskkill: MagicMock,
        mock_parse: MagicMock,
    ) -> None:
        """Test basic main function execution."""
        # Setup mocks
        mock_args = MagicMock()
        mock_args.processes = ["python.exe"]
        mock_args.debug = False
        mock_args.force = False
        mock_parse.return_value = mock_args

        mock_instance = MagicMock()
        mock_instance.kill.return_value = True
        mock_taskkill.return_value = mock_instance

        # Execute
        result = main()

        # Verify
        assert result == 0
        mock_parse.assert_called_once()
        mock_taskkill.assert_called_once_with()
        mock_instance.kill.assert_called_once_with("python.exe")
        mock_logger.setLevel.assert_not_called()

    @patch("pytola_system.taskkill.cli.parse_args")
    @patch("pytola_system.taskkill.cli.TaskkillCommand")
    @patch("pytola_system.taskkill.cli.logger")
    def test_main_debug_mode(
        self,
        mock_logger: MagicMock,
        mock_taskkill: MagicMock,
        mock_parse: MagicMock,
    ) -> None:
        """Test main function with debug mode enabled."""
        # Setup mocks
        mock_args = MagicMock()
        mock_args.processes = ["python.exe"]
        mock_args.debug = True
        mock_args.force = False
        mock_parse.return_value = mock_args

        mock_instance = MagicMock()
        mock_instance.kill.return_value = True
        mock_taskkill.return_value = mock_instance

        # Execute
        result = main()

        # Verify
        assert result == 0
        mock_logger.setLevel.assert_called_once_with(logging.DEBUG)

    @patch("pytola_system.taskkill.cli.parse_args")
    @patch("pytola_system.taskkill.cli.TaskkillCommand")
    def test_main_force_mode(self, mock_taskkill: MagicMock, mock_parse: MagicMock) -> None:
        """Test main function with force mode enabled."""
        # Setup mocks
        mock_args = MagicMock()
        mock_args.processes = ["temp*"]
        mock_args.debug = False
        mock_args.force = True
        mock_parse.return_value = mock_args

        mock_instance = MagicMock()
        mock_instance.kill.return_value = True
        mock_taskkill.return_value = mock_instance

        # Execute
        result = main()

        # Verify
        assert result == 0
        mock_taskkill.assert_called_once_with(force=True)

    @patch("pytola_system.taskkill.cli.parse_args")
    @patch("pytola_system.taskkill.cli.TaskkillCommand")
    def test_main_multiple_processes(
        self,
        mock_taskkill: MagicMock,
        mock_parse: MagicMock,
    ) -> None:
        """Test main function with multiple processes."""
        # Setup mocks
        mock_args = MagicMock()
        mock_args.processes = ["firefox", "chrome", "edge"]
        mock_args.debug = False
        mock_args.force = False
        mock_parse.return_value = mock_args

        mock_instance = MagicMock()
        mock_instance.kill.side_effect = [True, True, True]
        mock_taskkill.return_value = mock_instance

        # Execute
        result = main()

        # Verify
        assert result == 0
        assert mock_instance.kill.call_count == 3
        mock_instance.kill.assert_any_call("firefox")
        mock_instance.kill.assert_any_call("chrome")
        mock_instance.kill.assert_any_call("edge")

    @patch("pytola_system.taskkill.cli.parse_args")
    @patch("pytola_system.taskkill.cli.TaskkillCommand")
    def test_main_partial_failure(self, mock_taskkill: MagicMock, mock_parse: MagicMock) -> None:
        """Test main function with partial failure."""
        # Setup mocks
        mock_args = MagicMock()
        mock_args.processes = ["process1", "process2"]
        mock_args.debug = False
        mock_args.force = False
        mock_parse.return_value = mock_args

        mock_instance = MagicMock()
        mock_instance.kill.side_effect = [True, False]  # First succeeds, second fails
        mock_taskkill.return_value = mock_instance

        # Execute
        result = main()

        # Verify
        assert result == 1
        assert mock_instance.kill.call_count == 2


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

    def test_process_info_dataclass_integration(self) -> None:
        """Test ProcessInfo dataclass integration with TaskkillCommand."""
        process = ProcessInfo(name="integration_test.exe", pid="9999")
        cmd = TaskkillCommand()
        cmd.processes = [process]

        matches = cmd.match("integration_test")
        assert len(matches) == 1
        assert matches[0] == process

    def test_threshold_behavior_integration(self) -> None:
        """Test threshold behavior integration."""
        # Pre-compute values to avoid repeated property access
        cmd_normal = TaskkillCommand()
        cmd_force = TaskkillCommand(force=True)
        normal_threshold = cmd_normal.match_threshold
        force_threshold = cmd_force.match_threshold

        assert normal_threshold < force_threshold

    @patch("pytola_system.taskkill.cli.TaskkillCommand._get_process_list_windows")
    def test_platform_detection_integration(self, mock_get_processes: MagicMock) -> None:
        """Test platform detection integration."""
        cmd = TaskkillCommand()
        # Just verify the command initializes without error
        assert isinstance(cmd, TaskkillCommand)
        # Verify that process list was retrieved based on platform
        mock_get_processes.assert_called_once()
