"""Test suite for pyloadergen module."""

from __future__ import annotations

import platform
import tempfile
from pathlib import Path
from unittest import mock

import pytest

from sfi.pyloadergen import pyloadergen
from sfi.pyloadergen.pyloadergen import CompilerConfig, ProjectInfo


class TestSelectTemplate:
    """Test template selection logic."""

    def test_windows_gui_template_non_debug(self):
        """Test Windows GUI template selection in non-debug mode."""
        with mock.patch("sfi.pyloadergen.pyloadergen.is_windows", True):  # noqa: SIM117
            with mock.patch("sfi.pyloadergen.pyloadergen.is_macos", False):
                result = pyloadergen.select_c_template("gui", False)
                assert result == pyloadergen._WINDOWS_GUI_TEMPLATE

    def test_windows_console_template_non_debug(self):
        """Test Windows console template selection in non-debug mode."""
        with mock.patch("sfi.pyloadergen.pyloadergen.is_windows", True):  # noqa: SIM117
            with mock.patch("sfi.pyloadergen.pyloadergen.is_macos", False):
                result = pyloadergen.select_c_template("console", False)
                assert result == pyloadergen._WINDOWS_CONSOLE_TEMPLATE

    def test_windows_gui_template_debug(self):
        """Test that debug mode forces console template on Windows."""
        with mock.patch("sfi.pyloadergen.pyloadergen.is_windows", True):  # noqa: SIM117
            with mock.patch("sfi.pyloadergen.pyloadergen.is_macos", False):
                result = pyloadergen.select_c_template("gui", True)
                assert result == pyloadergen._WINDOWS_CONSOLE_TEMPLATE

    def test_macos_gui_template_non_debug(self):
        """Test macOS GUI template selection in non-debug mode."""
        with mock.patch("sfi.pyloadergen.pyloadergen.is_windows", False):  # noqa: SIM117
            with mock.patch("sfi.pyloadergen.pyloadergen.is_macos", True):
                result = pyloadergen.select_c_template("gui", False)
                assert result == pyloadergen._MACOS_GUI_TEMPLATE

    def test_macos_console_template_debug(self):
        """Test that debug mode forces console template on macOS."""
        with mock.patch("sfi.pyloadergen.pyloadergen.is_windows", False):  # noqa: SIM117
            with mock.patch("sfi.pyloadergen.pyloadergen.is_macos", True):
                result = pyloadergen.select_c_template("gui", True)
                assert result == pyloadergen._MACOS_CONSOLE_TEMPLATE

    def test_unix_gui_template_non_debug(self):
        """Test Unix GUI template selection in non-debug mode."""
        with mock.patch("sfi.pyloadergen.pyloadergen.is_windows", False):  # noqa: SIM117
            with mock.patch("sfi.pyloadergen.pyloadergen.is_macos", False):
                result = pyloadergen.select_c_template("gui", False)
                assert result == pyloadergen._UNIX_GUI_TEMPLATE

    def test_unix_console_template_debug(self):
        """Test that debug mode forces console template on Unix."""
        with mock.patch("sfi.pyloadergen.pyloadergen.is_windows", False):  # noqa: SIM117
            with mock.patch("sfi.pyloadergen.pyloadergen.is_macos", False):
                result = pyloadergen.select_c_template("gui", True)
                assert result == pyloadergen._UNIX_CONSOLE_TEMPLATE


class TestGenerateCSource:
    """Test C source code generation."""

    def test_replace_entry_file_placeholder(self):
        """Test that ENTRY_FILE placeholder is replaced."""
        template = "script: ${ENTRY_FILE}"
        result = pyloadergen.prepare_c_source(template, "my_script.py", False)
        assert "my_script.py" in result
        assert "${ENTRY_FILE}" not in result

    def test_replace_debug_mode_placeholder_true(self):
        """Test that DEBUG_MODE placeholder is replaced with 1."""
        template = "debug: ${DEBUG_MODE}"
        result = pyloadergen.prepare_c_source(template, "test.py", True)
        assert "debug: 1" in result
        assert "${DEBUG_MODE}" not in result

    def test_replace_debug_mode_placeholder_false(self):
        """Test that DEBUG_MODE placeholder is replaced with 0."""
        template = "debug: ${DEBUG_MODE}"
        result = pyloadergen.prepare_c_source(template, "test.py", False)
        assert "debug: 0" in result
        assert "${DEBUG_MODE}" not in result

    def test_replace_both_placeholders(self):
        """Test that both placeholders are replaced correctly."""
        template = "file: ${ENTRY_FILE}, debug: ${DEBUG_MODE}"
        result = pyloadergen.prepare_c_source(template, "main.py", True)
        assert "file: main.py" in result
        assert "debug: 1" in result
        assert "${ENTRY_FILE}" not in result
        assert "${DEBUG_MODE}" not in result


class TestGetCompilerArgs:
    """Test compiler argument retrieval."""

    def test_gcc_compiler_args(self):
        """Test GCC compiler arguments."""
        args = pyloadergen.get_compiler_args("gcc")
        assert "-std=c99" in args
        assert "-Wall" in args
        assert "-O2" in args

    def test_clang_compiler_args(self):
        """Test Clang compiler arguments."""
        args = pyloadergen.get_compiler_args("clang")
        assert "-std=c99" in args
        assert "-O2" in args

    def test_cl_compiler_args(self):
        """Test MSVC (cl) compiler arguments."""
        args = pyloadergen.get_compiler_args("cl")
        assert "/std:c99" in args
        assert "/O2" in args

    @pytest.mark.skipif(platform.system() != "Windows", reason="Only supported on Windows")
    def test_cl_exe_compiler_args(self):
        """Test cl.exe compiler arguments (full path)."""
        args = pyloadergen.get_compiler_args("C:\\vc\\bin\\cl.exe")
        assert "/std:c99" in args
        assert "/O2" in args

    def test_unknown_compiler_args(self):
        """Test unknown compiler returns empty list."""
        args = pyloadergen.get_compiler_args("unknown_compiler")
        assert args == []


class TestFindCompiler:
    """Test compiler detection."""

    def test_find_compiler_gcc(self):
        """Test finding gcc compiler."""
        with mock.patch("shutil.which") as mock_which:
            mock_which.return_value = "/usr/bin/gcc"
            with mock.patch("sfi.pyloadergen.pyloadergen.is_windows", False):
                result = pyloadergen.find_compiler()
                assert result == "gcc"

    def test_find_compiler_clang(self):
        """Test finding clang compiler."""
        # Mock shutil.which to return clang for clang but not for gcc
        with mock.patch("shutil.which") as mock_which:

            def which_side_effect(cmd):
                # Return gcc None, clang first
                if cmd == "gcc":
                    return None
                elif cmd == "clang":
                    return "/usr/bin/clang"
                return None

            mock_which.side_effect = which_side_effect

            with mock.patch("sfi.pyloadergen.pyloadergen.is_windows", False):
                result = pyloadergen.find_compiler()
                assert result == "clang"

    def test_no_compiler_found(self):
        """Test when no compiler is found."""
        with mock.patch("shutil.which") as mock_which:
            mock_which.return_value = None
            result = pyloadergen.find_compiler()
            assert result is None


class TestCompilerConfigs:
    """Test compiler configurations."""

    def test_compiler_configs_has_gcc(self):
        """Test GCC config is defined."""
        gcc_configs = [c for c in pyloadergen._COMPILER_CONFIGS if c.name == "gcc"]
        assert len(gcc_configs) == 1
        assert "-std=c99" in gcc_configs[0].args

    def test_compiler_configs_has_clang(self):
        """Test Clang config is defined."""
        clang_configs = [c for c in pyloadergen._COMPILER_CONFIGS if c.name == "clang"]
        assert len(clang_configs) == 1
        assert "-std=c99" in clang_configs[0].args

    def test_compiler_configs_has_cl(self):
        """Test MSVC config is defined."""
        cl_configs = [c for c in pyloadergen._COMPILER_CONFIGS if c.name == "cl"]
        assert len(cl_configs) == 1
        assert "/std:c99" in cl_configs[0].args


class TestTemplateContent:
    """Test template content validation."""

    def test_windows_gui_template_has_winmain(self):
        """Test Windows GUI template has WinMain entry point."""
        assert "int APIENTRY WinMain" in pyloadergen._WINDOWS_GUI_TEMPLATE

    def test_windows_console_template_has_main(self):
        """Test Windows console template has main entry point."""
        assert "int main(" in pyloadergen._WINDOWS_CONSOLE_TEMPLATE

    def test_unix_gui_template_has_main(self):
        """Test Unix GUI template has main entry point."""
        assert "int main(" in pyloadergen._UNIX_GUI_TEMPLATE

    def test_unix_console_template_has_main(self):
        """Test Unix console template has main entry point."""
        assert "int main(" in pyloadergen._UNIX_CONSOLE_TEMPLATE

    def test_macos_gui_template_has_main(self):
        """Test macOS GUI template has main entry point."""
        assert "int main(" in pyloadergen._MACOS_GUI_TEMPLATE

    def test_macos_console_template_has_main(self):
        """Test macOS console template has main entry point."""
        assert "int main(" in pyloadergen._MACOS_CONSOLE_TEMPLATE

    def test_windows_templates_have_placeholder(self):
        """Test Windows templates contain placeholders."""
        assert "${ENTRY_FILE}" in pyloadergen._WINDOWS_GUI_TEMPLATE
        assert "${DEBUG_MODE}" in pyloadergen._WINDOWS_GUI_TEMPLATE
        assert "${ENTRY_FILE}" in pyloadergen._WINDOWS_CONSOLE_TEMPLATE

    def test_unix_templates_have_placeholder(self):
        """Test Unix templates contain placeholder."""
        assert "${ENTRY_FILE}" in pyloadergen._UNIX_GUI_TEMPLATE
        assert "${ENTRY_FILE}" in pyloadergen._UNIX_CONSOLE_TEMPLATE

    def test_macos_templates_have_placeholder(self):
        """Test macOS templates contain placeholder."""
        assert "${ENTRY_FILE}" in pyloadergen._MACOS_GUI_TEMPLATE
        assert "${ENTRY_FILE}" in pyloadergen._MACOS_CONSOLE_TEMPLATE


class TestCompileCSource:
    """Test C source compilation."""

    def test_compile_creates_executable(self):
        """Test compilation creates executable file."""
        with tempfile.TemporaryDirectory() as temp_dir:
            c_source = Path(temp_dir) / "test.c"
            c_source.write_text("#include <stdio.h>\nint main() { return 0; }")
            output = Path(temp_dir) / "test.exe"

            with mock.patch("subprocess.run") as mock_run:
                mock_run.return_value = mock.Mock(returncode=0, stdout="", stderr="")
                result = pyloadergen.compile_c_source(str(c_source), output, "gcc")
                assert result is True

    def test_compile_failure(self):
        """Test compilation failure."""
        with tempfile.TemporaryDirectory() as temp_dir:
            c_source = Path(temp_dir) / "test.c"
            c_source.write_text("invalid C code")
            output = Path(temp_dir) / "test.exe"

            with mock.patch("subprocess.run") as mock_run:
                mock_run.return_value = mock.Mock(returncode=1, stdout="", stderr="error")
                result = pyloadergen.compile_c_source(str(c_source), output, "gcc")
                assert result is False

    def test_compiler_not_found(self):
        """Test when compiler is not found."""
        with tempfile.TemporaryDirectory() as temp_dir:
            c_source = Path(temp_dir) / "test.c"
            c_source.write_text("int main() { return 0; }")
            output = Path(temp_dir) / "test.exe"

            with mock.patch("subprocess.run", side_effect=FileNotFoundError):
                result = pyloadergen.compile_c_source(str(c_source), output, "gcc")
                assert result is False

    def test_no_compiler_specified_finds_one(self):
        """Test auto-detection of compiler when none specified."""
        with tempfile.TemporaryDirectory() as temp_dir:
            c_source = Path(temp_dir) / "test.c"
            c_source.write_text("int main() { return 0; }")
            output = Path(temp_dir) / "test.exe"

            with mock.patch("subprocess.run") as mock_run:
                mock_run.return_value = mock.Mock(returncode=0, stdout="", stderr="")
                with mock.patch("sfi.pyloadergen.pyloadergen.find_compiler", return_value="gcc"):
                    result = pyloadergen.compile_c_source(str(c_source), output, None)
                    assert result is True


class TestProjectInfo:
    """Test ProjectInfo dataclass."""

    def test_create_project_info_from_dict(self):
        """Test creating ProjectInfo from dictionary."""
        data = {
            "version": "1.0.0",
            "description": "Test project",
            "dependencies": ["requests", "pyside2"],
            "keywords": ["gui", "test"],
            "scripts": {"main": "src.test:main"},
        }
        project_info = ProjectInfo.from_dict("test_project", data)

        assert project_info.name == "test_project"
        assert project_info.version == "1.0.0"
        assert project_info.description == "Test project"
        assert "requests" in project_info.dependencies
        assert "pyside2" in project_info.dependencies
        assert "gui" in project_info.keywords
        assert "test" in project_info.keywords
        assert "main" in project_info.scripts

    def test_create_project_info_with_defaults(self):
        """Test creating ProjectInfo with default values."""
        data = {}
        project_info = ProjectInfo.from_dict("test_project", data)

        assert project_info.name == "test_project"
        assert project_info.version == "0.0.0"
        assert project_info.description == ""
        assert project_info.dependencies == []
        assert project_info.keywords == []
        assert project_info.scripts == {}

    def test_project_info_to_dict(self):
        """Test converting ProjectInfo to dictionary."""
        project_info = ProjectInfo(
            name="test_project",
            version="1.0.0",
            description="Test",
            dependencies=["requests"],
            keywords=["test"],
            scripts={"main": "src.main:main"},
        )

        result = project_info.to_dict()

        assert result["name"] == "test_project"
        assert result["version"] == "1.0.0"
        assert result["description"] == "Test"
        assert "requests" in result["dependencies"]
        assert "test" in result["keywords"]
        assert "main" in result["scripts"]

    def test_project_info_is_frozen(self):
        """Test that ProjectInfo dataclass is frozen."""
        project_info = ProjectInfo(name="test_project")

        with pytest.raises(AttributeError):
            project_info.name = "new_name"  # type: ignore[misc]


class TestCompilerConfig:
    """Test CompilerConfig dataclass."""

    def test_create_compiler_config(self):
        """Test creating CompilerConfig."""
        config = CompilerConfig(name="gcc", args=("-std=c99", "-Wall", "-Werror"))

        assert config.name == "gcc"
        assert "-std=c99" in config.args
        assert "-Wall" in config.args
        assert "-Werror" in config.args

    def test_compiler_config_to_list(self):
        """Test converting CompilerConfig args to list."""
        config = CompilerConfig(name="gcc", args=("-std=c99", "-Wall"))
        result = config.to_list()

        assert isinstance(result, list)
        assert result == ["-std=c99", "-Wall"]

    def test_compiler_config_is_frozen(self):
        """Test that CompilerConfig dataclass is frozen."""
        config = CompilerConfig(name="gcc", args=("-std=c99",))

        with pytest.raises(AttributeError):
            config.name = "clang"  # type: ignore[misc]


class TestDetectLoaderType:
    """Test loader type detection with ProjectInfo."""

    def test_detect_gui_loader_with_qt_dependency(self):
        """Test GUI loader detection with Qt dependency."""
        project_info = ProjectInfo(
            name="test_project",
            dependencies=["pyside2", "requests"],
            keywords=[],
        )

        loader_type, use_qt = pyloadergen._detect_loader_type(project_info)

        assert loader_type == "gui"
        assert use_qt is True

    def test_detect_console_loader_without_qt(self):
        """Test console loader detection without Qt."""
        project_info = ProjectInfo(
            name="test_project",
            dependencies=["requests", "flask"],
            keywords=[],
        )

        loader_type, use_qt = pyloadergen._detect_loader_type(project_info)

        assert loader_type == "console"
        assert use_qt is False

    def test_detect_gui_loader_with_gui_keyword(self):
        """Test GUI loader detection with GUI keyword."""
        project_info = ProjectInfo(
            name="test_project",
            dependencies=["requests"],
            keywords=["gui", "desktop"],
        )

        loader_type, use_qt = pyloadergen._detect_loader_type(project_info)

        assert loader_type == "gui"
        assert use_qt is False

    def test_detect_qt_case_insensitive(self):
        """Test Qt detection is case insensitive."""
        project_info = ProjectInfo(
            name="test_project",
            dependencies=["PySide2", "PyQt6"],
            keywords=[],
        )

        loader_type, use_qt = pyloadergen._detect_loader_type(project_info)

        assert loader_type == "gui"
        assert use_qt is True


class TestGetEntryModule:
    """Test entry module retrieval with ProjectInfo."""

    def test_get_entry_module_from_scripts(self):
        """Test getting entry module from scripts."""
        project_info = ProjectInfo(
            name="test-project",
            scripts={"main": "src.test_project.main:main"},
        )

        entry_module = pyloadergen._get_entry_module(project_info)

        assert entry_module == "src.test_project.main"

    def test_get_entry_module_default(self):
        """Test getting default entry module."""
        project_info = ProjectInfo(name="test-project", scripts={})

        entry_module = pyloadergen._get_entry_module(project_info)

        assert entry_module == "test_project"

    def test_get_entry_module_hyphen_to_underscore(self):
        """Test hyphen to underscore conversion."""
        project_info = ProjectInfo(name="my-app", scripts={})

        entry_module = pyloadergen._get_entry_module(project_info)

        assert entry_module == "my_app"
