"""Unit tests for taskkill module."""

from __future__ import annotations

import subprocess
from unittest.mock import MagicMock, patch

import pytest

from sfi.taskkill.taskkill import (
    ProcessInfo,
    _get_process_list_unix,
    _get_process_list_windows,
    _kill_process_by_pid,
    _kill_process_by_pid_unix,
    _kill_process_by_pid_windows,
    get_matched_process,
    get_process_list,
    kill_process,
)


@pytest.fixture
def sample_processes():
    """Sample process list for testing."""
    return [
        ProcessInfo(name="python.exe", pid="1234"),
        ProcessInfo(name="chrome.exe", pid="5678"),
        ProcessInfo(name="notepad.exe", pid="9012"),
        ProcessInfo(name="python3.exe", pid="3456"),
    ]


class TestProcessInfo:
    """Tests for ProcessInfo dataclass."""

    def test_process_info_creation(self):
        """Test ProcessInfo object creation."""
        process = ProcessInfo(name="test.exe", pid="1234")
        assert process.name == "test.exe"
        assert process.pid == "1234"

    def test_process_info_equality(self):
        """Test ProcessInfo equality comparison."""
        process1 = ProcessInfo(name="test.exe", pid="1234")
        process2 = ProcessInfo(name="test.exe", pid="1234")
        process3 = ProcessInfo(name="other.exe", pid="1234")

        assert process1 == process2
        assert process1 != process3


class TestGetMatchedProcess:
    """Tests for get_matched_process function."""

    def test_exact_match(self, sample_processes, monkeypatch):
        """Test exact process name matching."""
        monkeypatch.setattr(
            "sfi.taskkill.taskkill._cached_process_list", sample_processes
        )
        matches = get_matched_process("python.exe")
        assert len(matches) == 1
        assert matches[0].name == "python.exe"
        assert matches[0].pid == "1234"

    def test_partial_match(self, sample_processes, monkeypatch):
        """Test partial process name matching with wildcards."""
        monkeypatch.setattr(
            "sfi.taskkill.taskkill._cached_process_list", sample_processes
        )
        matches = get_matched_process("*python*")
        assert len(matches) == 2
        names = [p.name for p in matches]
        assert "python.exe" in names
        assert "python3.exe" in names

    def test_case_insensitive_match(self, sample_processes, monkeypatch):
        """Test case insensitive matching."""
        monkeypatch.setattr(
            "sfi.taskkill.taskkill._cached_process_list", sample_processes
        )
        matches = get_matched_process("PYTHON.EXE")
        assert len(matches) == 1
        assert matches[0].name == "python.exe"

    def test_no_matches(self, sample_processes, monkeypatch):
        """Test when no processes match."""
        monkeypatch.setattr(
            "sfi.taskkill.taskkill._cached_process_list", sample_processes
        )
        matches = get_matched_process("nonexistent.exe")
        assert len(matches) == 0


class TestGetProcessList:
    """Tests for get_process_list function."""

    def test_get_process_list_windows(self, monkeypatch):
        """Test process list retrieval on Windows."""
        mock_cached = []
        monkeypatch.setattr("sfi.taskkill.taskkill._cached_process_list", mock_cached)
        monkeypatch.setattr("sfi.taskkill.taskkill._processes_cached", False)
        monkeypatch.setattr("sys.platform", "win32")

        with patch("sfi.taskkill.taskkill._get_process_list_windows") as mock_func:
            get_process_list()
            mock_func.assert_called_once()

    def test_get_process_list_unix(self, monkeypatch):
        """Test process list retrieval on Unix."""
        mock_cached = []
        monkeypatch.setattr("sfi.taskkill.taskkill._cached_process_list", mock_cached)
        monkeypatch.setattr("sfi.taskkill.taskkill._processes_cached", False)
        monkeypatch.setattr("sys.platform", "linux")

        with patch("sfi.taskkill.taskkill._get_process_list_unix") as mock_func:
            get_process_list()
            mock_func.assert_called_once()

    def test_force_refresh(self, monkeypatch):
        """Test force refresh functionality."""
        mock_cached = []
        monkeypatch.setattr("sfi.taskkill.taskkill._cached_process_list", mock_cached)
        monkeypatch.setattr("sfi.taskkill.taskkill._processes_cached", True)

        with patch("sfi.taskkill.taskkill._get_process_list_windows") as mock_func:
            monkeypatch.setattr("sys.platform", "win32")
            get_process_list(force_refresh=True)
            mock_func.assert_called_once()


class TestPlatformSpecificFunctions:
    """Tests for platform-specific functions."""

    @patch("subprocess.run")
    def test_get_process_list_windows_success(self, mock_run):
        """Test Windows process list retrieval success."""
        # Mock successful subprocess result
        mock_result = MagicMock()
        mock_result.stdout = '"python.exe","1234"\n"chrome.exe","5678"\n'
        mock_result.stderr = ""
        mock_run.return_value = mock_result

        with patch("sys.platform", "win32"):
            processes = []
            with patch("sfi.taskkill.taskkill._cached_process_list", processes):
                _get_process_list_windows()
                assert len(processes) == 2
                assert processes[0].name == "python.exe"
                assert processes[0].pid == "1234"

    @patch("subprocess.run")
    def test_get_process_list_windows_decode_error(self, mock_run):
        """Test Windows process list handling decode errors."""
        # Mock decode error that falls back to utf8
        mock_result_gbk = MagicMock()
        mock_result_gbk.side_effect = UnicodeDecodeError(
            "gbk", b"", 0, 1, "decode error"
        )

        mock_result_utf8 = MagicMock()
        mock_result_utf8.stdout = '"test.exe","1234"\n'
        mock_result_utf8.stderr = ""

        mock_run.side_effect = [
            UnicodeDecodeError("gbk", b"", 0, 1, "decode error"),
            mock_result_utf8,
        ]

        with patch("sys.platform", "win32"):
            processes = []
            with patch("sfi.taskkill.taskkill._cached_process_list", processes):
                _get_process_list_windows()
                assert len(processes) == 1

    @patch("subprocess.run")
    def test_get_process_list_unix_success(self, mock_run):
        """Test Unix process list retrieval success."""
        mock_result = MagicMock()
        mock_result.stdout = "1234 python\n5678 chrome\n"
        mock_result.stderr = ""
        mock_run.return_value = mock_result

        with patch("sys.platform", "linux"):
            processes = []
            with patch("sfi.taskkill.taskkill._cached_process_list", processes):
                _get_process_list_unix()
                assert len(processes) == 2
                assert processes[0].name == "python"
                assert processes[0].pid == "1234"

    @patch("subprocess.run")
    def test_kill_process_by_pid_windows_success(self, mock_run):
        """Test successful Windows process termination."""
        mock_result = MagicMock()
        mock_result.returncode = 0
        mock_run.return_value = mock_result

        result = _kill_process_by_pid_windows("1234")
        assert result is True
        mock_run.assert_called_once_with(
            ["taskkill", "/F", "/PID", "1234"],
            capture_output=True,
            text=True,
            check=True,
            timeout=10,
        )

    @patch("subprocess.run")
    def test_kill_process_by_pid_windows_failure(self, mock_run):
        """Test failed Windows process termination."""
        mock_run.side_effect = subprocess.CalledProcessError(1, "taskkill")

        result = _kill_process_by_pid_windows("1234")
        assert result is False

    @patch("subprocess.run")
    def test_kill_process_by_pid_unix_success(self, mock_run):
        """Test successful Unix process termination."""
        mock_result = MagicMock()
        mock_result.returncode = 0
        mock_run.return_value = mock_result

        result = _kill_process_by_pid_unix("1234")
        assert result is True
        mock_run.assert_called_once_with(
            ["kill", "-9", "1234"],
            capture_output=True,
            text=True,
            check=True,
            timeout=10,
        )

    @patch("subprocess.run")
    def test_kill_process_by_pid_unix_failure(self, mock_run):
        """Test failed Unix process termination."""
        mock_run.side_effect = subprocess.CalledProcessError(1, "kill")

        result = _kill_process_by_pid_unix("1234")
        assert result is False


class TestKillProcessFunction:
    """Tests for kill_process function."""

    @patch("sfi.taskkill.taskkill.get_process_list")
    @patch("sfi.taskkill.taskkill.get_matched_process")
    def test_kill_process_success(
        self, mock_get_matched, mock_get_process_list, sample_processes
    ):
        """Test successful process termination."""
        mock_get_matched.return_value = [sample_processes[0]]  # python.exe

        with patch("sfi.taskkill.taskkill._kill_process_by_pid") as mock_kill:
            mock_kill.return_value = True

            with patch("sfi.taskkill.taskkill.logger") as mock_logger:
                kill_process("python.exe")

                mock_get_process_list.assert_called_once_with(force_refresh=True)
                mock_get_matched.assert_called_once_with("python.exe")
                mock_kill.assert_called_once_with("1234")
                mock_logger.info.assert_any_call(
                    "Successfully terminated process python.exe (PID: 1234)"
                )

    @patch("sfi.taskkill.taskkill.get_process_list")
    @patch("sfi.taskkill.taskkill.get_matched_process")
    def test_kill_process_not_found(self, mock_get_matched, mock_get_process_list):
        """Test process not found scenario."""
        mock_get_matched.return_value = []

        with patch("sfi.taskkill.taskkill.logger") as mock_logger:
            kill_process("nonexistent.exe")

            mock_logger.warning.assert_called_once_with(
                "Process `nonexistent.exe` not found"
            )

    @patch("sfi.taskkill.taskkill.get_process_list")
    @patch("sfi.taskkill.taskkill.get_matched_process")
    def test_kill_process_exceeds_threshold(
        self, mock_get_matched, mock_get_process_list, sample_processes
    ):
        """Test process termination when exceeding match threshold."""
        # Create more processes than threshold
        many_processes = sample_processes * 3  # 12 processes
        mock_get_matched.return_value = many_processes

        with patch("sfi.taskkill.taskkill._max_match_threshold", 10):
            with patch("sfi.taskkill.taskkill.logger") as mock_logger:
                kill_process("python*")

                # Should warn about exceeding threshold
                mock_logger.warning.assert_any_call(
                    "Found 12 processes matching 'python*' (exceeds threshold of 10)"
                )

    @patch("sfi.taskkill.taskkill.get_process_list")
    @patch("sfi.taskkill.taskkill.get_matched_process")
    def test_kill_process_partial_failure(
        self, mock_get_matched, mock_get_process_list, sample_processes
    ):
        """Test partial process termination failure."""
        mock_get_matched.return_value = sample_processes[
            :2
        ]  # python.exe and chrome.exe

        with patch("sfi.taskkill.taskkill._kill_process_by_pid") as mock_kill:
            # First succeeds, second fails
            mock_kill.side_effect = [True, False]

            with patch("sfi.taskkill.taskkill.logger") as mock_logger:
                kill_process("test*")

                # Should log both success and failure
                mock_logger.info.assert_any_call(
                    "Successfully terminated process python.exe (PID: 1234)"
                )
                mock_logger.info.assert_any_call(
                    "Failed to terminate process chrome.exe (PID: 5678)"
                )


def test_kill_process_by_pid_platform_dispatch():
    """Test platform dispatch in _kill_process_by_pid."""
    with patch("sys.platform", "win32"):
        with patch("sfi.taskkill.taskkill._kill_process_by_pid_windows") as mock_win:
            mock_win.return_value = True
            result = _kill_process_by_pid("1234")
            assert result is True
            mock_win.assert_called_once_with("1234")

    with patch("sys.platform", "linux"):
        with patch("sfi.taskkill.taskkill._kill_process_by_pid_unix") as mock_unix:
            mock_unix.return_value = True
            result = _kill_process_by_pid("1234")
            assert result is True
            mock_unix.assert_called_once_with("1234")
