"""Tests for Windows-specific LLVM version detection."""

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

from clangir._clang._version import (
    _try_windows_program_files,
    _try_windows_registry,
)


class TestWindowsRegistry:
    """Tests for _try_windows_registry()."""

    def test_finds_version_from_registry(self):
        """Registry returns install dir, clang.exe returns version."""
        mock_winreg = MagicMock()
        mock_key = MagicMock()
        mock_winreg.OpenKey.return_value = mock_key
        mock_winreg.QueryValueEx.return_value = (r"C:\Program Files\LLVM", 1)
        mock_winreg.HKEY_LOCAL_MACHINE = 0x80000002
        mock_winreg.KEY_READ = 0x20019
        mock_winreg.KEY_WOW64_64KEY = 0x0100

        mock_result = MagicMock()
        mock_result.returncode = 0
        mock_result.stdout = "#define __clang_major__ 18\n#define __clang_minor__ 1\n"

        with (
            patch("clangir._clang._version.sys.platform", "win32"),
            patch.dict(sys.modules, {"winreg": mock_winreg}),
            patch("clangir._clang._version.os.path.isdir", return_value=True),
            patch("clangir._clang._version.os.path.isfile", return_value=True),
            patch("clangir._clang._version.subprocess.run", return_value=mock_result),
        ):
            result = _try_windows_registry()
            assert result == "18"

    def test_registry_key_not_found(self):
        """Registry key does not exist, returns None."""
        mock_winreg = MagicMock()
        mock_winreg.OpenKey.side_effect = OSError("Key not found")
        mock_winreg.HKEY_LOCAL_MACHINE = 0x80000002
        mock_winreg.KEY_READ = 0x20019
        mock_winreg.KEY_WOW64_64KEY = 0x0100

        with (
            patch("clangir._clang._version.sys.platform", "win32"),
            patch.dict(sys.modules, {"winreg": mock_winreg}),
        ):
            result = _try_windows_registry()
            assert result is None

    def test_install_dir_does_not_exist(self):
        """Registry returns dir that does not exist on disk."""
        mock_winreg = MagicMock()
        mock_key = MagicMock()
        mock_winreg.OpenKey.return_value = mock_key
        mock_winreg.QueryValueEx.return_value = (r"C:\Nonexistent\LLVM", 1)
        mock_winreg.HKEY_LOCAL_MACHINE = 0x80000002
        mock_winreg.KEY_READ = 0x20019
        mock_winreg.KEY_WOW64_64KEY = 0x0100

        with (
            patch("clangir._clang._version.sys.platform", "win32"),
            patch.dict(sys.modules, {"winreg": mock_winreg}),
            patch("clangir._clang._version.os.path.isdir", return_value=False),
        ):
            result = _try_windows_registry()
            assert result is None

    def test_clang_exe_not_found_in_install_dir(self):
        """Registry install dir exists but clang.exe is missing."""
        mock_winreg = MagicMock()
        mock_key = MagicMock()
        mock_winreg.OpenKey.return_value = mock_key
        mock_winreg.QueryValueEx.return_value = (r"C:\Program Files\LLVM", 1)
        mock_winreg.HKEY_LOCAL_MACHINE = 0x80000002
        mock_winreg.KEY_READ = 0x20019
        mock_winreg.KEY_WOW64_64KEY = 0x0100

        with (
            patch("clangir._clang._version.sys.platform", "win32"),
            patch.dict(sys.modules, {"winreg": mock_winreg}),
            patch("clangir._clang._version.os.path.isdir", return_value=True),
            patch("clangir._clang._version.os.path.isfile", return_value=False),
        ):
            result = _try_windows_registry()
            assert result is None

    def test_clang_exe_returns_no_version(self):
        """clang.exe runs but output lacks __clang_major__."""
        mock_winreg = MagicMock()
        mock_key = MagicMock()
        mock_winreg.OpenKey.return_value = mock_key
        mock_winreg.QueryValueEx.return_value = (r"C:\Program Files\LLVM", 1)
        mock_winreg.HKEY_LOCAL_MACHINE = 0x80000002
        mock_winreg.KEY_READ = 0x20019
        mock_winreg.KEY_WOW64_64KEY = 0x0100

        mock_result = MagicMock()
        mock_result.returncode = 0
        mock_result.stdout = "#define __STDC__ 1\n"

        with (
            patch("clangir._clang._version.sys.platform", "win32"),
            patch.dict(sys.modules, {"winreg": mock_winreg}),
            patch("clangir._clang._version.os.path.isdir", return_value=True),
            patch("clangir._clang._version.os.path.isfile", return_value=True),
            patch("clangir._clang._version.subprocess.run", return_value=mock_result),
        ):
            result = _try_windows_registry()
            assert result is None

    def test_skipped_on_non_windows(self):
        """Returns None immediately on non-Windows platforms."""
        with patch("clangir._clang._version.sys.platform", "linux"):
            result = _try_windows_registry()
            assert result is None

    def test_subprocess_timeout(self):
        """clang.exe subprocess times out, returns None."""
        mock_winreg = MagicMock()
        mock_key = MagicMock()
        mock_winreg.OpenKey.return_value = mock_key
        mock_winreg.QueryValueEx.return_value = (r"C:\Program Files\LLVM", 1)
        mock_winreg.HKEY_LOCAL_MACHINE = 0x80000002
        mock_winreg.KEY_READ = 0x20019
        mock_winreg.KEY_WOW64_64KEY = 0x0100

        with (
            patch("clangir._clang._version.sys.platform", "win32"),
            patch.dict(sys.modules, {"winreg": mock_winreg}),
            patch("clangir._clang._version.os.path.isdir", return_value=True),
            patch("clangir._clang._version.os.path.isfile", return_value=True),
            patch(
                "clangir._clang._version.subprocess.run",
                side_effect=subprocess.TimeoutExpired(cmd="clang.exe", timeout=5),
            ),
        ):
            result = _try_windows_registry()
            assert result is None

    def test_winreg_import_error(self):
        """When winreg module is not importable, returns None."""
        with (
            patch("clangir._clang._version.sys.platform", "win32"),
            patch.dict(sys.modules, {"winreg": None}),
        ):
            result = _try_windows_registry()
            assert result is None


class TestWindowsProgramFiles:
    """Tests for _try_windows_program_files()."""

    def test_finds_version_from_programfiles(self):
        """Finds LLVM in PROGRAMFILES directory."""
        mock_result = MagicMock()
        mock_result.returncode = 0
        mock_result.stdout = "#define __clang_major__ 20\n"

        with (
            patch("clangir._clang._version.sys.platform", "win32"),
            patch.dict(
                os.environ,
                {
                    "PROGRAMFILES": r"C:\Program Files",
                    "PROGRAMFILES(X86)": r"C:\Program Files (x86)",
                },
            ),
            patch("clangir._clang._version.os.path.isfile", return_value=True),
            patch("clangir._clang._version.subprocess.run", return_value=mock_result),
        ):
            result = _try_windows_program_files()
            assert result == "20"

    def test_finds_version_from_programfiles_x86(self):
        """Finds LLVM in PROGRAMFILES(X86) when PROGRAMFILES path has no clang."""
        mock_result = MagicMock()
        mock_result.returncode = 0
        mock_result.stdout = "#define __clang_major__ 19\n"

        def isfile_side_effect(path):
            # Only the x86 path has clang.exe
            return "x86" in path.lower()

        with (
            patch("clangir._clang._version.sys.platform", "win32"),
            patch.dict(
                os.environ,
                {
                    "PROGRAMFILES": r"C:\Program Files",
                    "PROGRAMFILES(X86)": r"C:\Program Files (x86)",
                },
            ),
            patch("clangir._clang._version.os.path.isfile", side_effect=isfile_side_effect),
            patch("clangir._clang._version.subprocess.run", return_value=mock_result),
        ):
            result = _try_windows_program_files()
            assert result == "19"

    def test_no_programfiles_env_var(self):
        """Both PROGRAMFILES env vars are absent."""
        env = os.environ.copy()
        env.pop("PROGRAMFILES", None)
        env.pop("PROGRAMFILES(X86)", None)

        with (
            patch("clangir._clang._version.sys.platform", "win32"),
            patch.dict(os.environ, env, clear=True),
        ):
            result = _try_windows_program_files()
            assert result is None

    def test_clang_exe_not_found(self):
        """PROGRAMFILES set but no clang.exe at the expected path."""
        with (
            patch("clangir._clang._version.sys.platform", "win32"),
            patch.dict(
                os.environ,
                {
                    "PROGRAMFILES": r"C:\Program Files",
                },
            ),
            patch("clangir._clang._version.os.path.isfile", return_value=False),
        ):
            result = _try_windows_program_files()
            assert result is None

    def test_skipped_on_non_windows(self):
        """Returns None on non-Windows platforms."""
        with patch("clangir._clang._version.sys.platform", "darwin"):
            result = _try_windows_program_files()
            assert result is None

    def test_subprocess_error_continues_to_next_candidate(self):
        """If clang.exe fails for PROGRAMFILES, tries PROGRAMFILES(X86)."""
        success_result = MagicMock()
        success_result.returncode = 0
        success_result.stdout = "#define __clang_major__ 21\n"

        call_count = 0

        def run_side_effect(cmd, **kwargs):  # noqa: ARG001
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                raise subprocess.SubprocessError("clang.exe crashed")
            return success_result

        with (
            patch("clangir._clang._version.sys.platform", "win32"),
            patch.dict(
                os.environ,
                {
                    "PROGRAMFILES": r"C:\Program Files",
                    "PROGRAMFILES(X86)": r"C:\Program Files (x86)",
                },
            ),
            patch("clangir._clang._version.os.path.isfile", return_value=True),
            patch("clangir._clang._version.subprocess.run", side_effect=run_side_effect),
        ):
            result = _try_windows_program_files()
            assert result == "21"
