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

from __future__ import annotations

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

import pytest
from hypothesis import given
from hypothesis import strategies as st

from ..components.loader import (
    _COMPILER_CONFIGS,
    _DEFAULT_BUILD_DIR,
    _DEFAULT_OUTPUT_DIR,
    _MACOS_CONSOLE_TEMPLATE,
    _MACOS_GUI_TEMPLATE,
    _UNIX_CONSOLE_TEMPLATE,
    _UNIX_GUI_TEMPLATE,
    _WINDOWS_CONSOLE_TEMPLATE,
    _WINDOWS_GUI_TEMPLATE,
    CompilerConfig,
    PyLoaderBuilder,
    PyLoaderGenerator,
    compile_c_source,
    ext,
    find_compiler,
    get_compiler_args,
    logger,
    prepare_c_source,
    prepare_entry_file,
    select_c_template,
)
from ..models.entryfile import EntryFile
from ..models.project import Project, detect_entry_files, is_entry_file
from ..tools.pyloadergen import main, parse_args


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

    @pytest.mark.parametrize(
        ("is_windows", "is_macos", "loader_type", "debug", "expected"),
        [
            # Windows cases
            (True, False, "gui", False, _WINDOWS_GUI_TEMPLATE),
            (True, False, "console", False, _WINDOWS_CONSOLE_TEMPLATE),
            (True, False, "gui", True, _WINDOWS_CONSOLE_TEMPLATE),
            # macOS cases
            (False, True, "gui", False, _MACOS_GUI_TEMPLATE),
            (False, True, "console", False, _MACOS_CONSOLE_TEMPLATE),
            (False, True, "gui", True, _MACOS_CONSOLE_TEMPLATE),
            # Unix cases
            (False, False, "gui", False, _UNIX_GUI_TEMPLATE),
            (False, False, "console", False, _UNIX_CONSOLE_TEMPLATE),
            (False, False, "gui", True, _UNIX_CONSOLE_TEMPLATE),
        ],
    )
    def test_template_selection(self, is_windows, is_macos, loader_type, debug, expected):
        """Test template selection for all platform/type/debug combinations."""
        with mock.patch("pytola.dev.pypack.components.loader.is_windows", is_windows):
            with mock.patch("pytola.dev.pypack.components.loader.is_macos", is_macos):
                result = select_c_template(loader_type, debug)
                assert result == expected


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

    @pytest.mark.parametrize(
        ("template", "entry_file", "is_debug", "expected_parts", "not_expected"),
        [
            (
                "script: ${ENTRY_FILE}",
                "my_script.py",
                False,
                ["my_script.py"],
                ["${ENTRY_FILE}"],
            ),
            ("debug: ${DEBUG_MODE}", "test.py", True, ["debug: 1"], ["${DEBUG_MODE}"]),
            ("debug: ${DEBUG_MODE}", "test.py", False, ["debug: 2"], ["${DEBUG_MODE}"]),
            (
                "file: ${ENTRY_FILE}, debug: ${DEBUG_MODE}",
                "main.py",
                True,
                ["file: main.py", "debug: 1"],
                ["${ENTRY_FILE}", "${DEBUG_MODE}"],
            ),
        ],
    )
    def test_prepare_c_source(self, template, entry_file, is_debug, expected_parts, not_expected):
        """Test C source generation with various template/parameter combinations."""
        result = prepare_c_source(template, entry_file, is_debug)
        for expected in expected_parts:
            assert expected in result
        for unexpected in not_expected:
            assert unexpected not in result


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

    @pytest.mark.parametrize(
        ("compiler", "expected_args"),
        [
            ("gcc", ["-std=c99", "-Wall", "-O2"]),
            ("clang", ["-std=c99", "-Wall", "-O2"]),
            ("cl", ["/std:c99", "/O2"]),
            ("unknown_compiler", []),
        ],
    )
    def test_get_compiler_args(self, compiler, expected_args):
        """Test compiler argument retrieval for various compilers."""
        args = get_compiler_args(compiler)
        if expected_args:
            for arg in expected_args:
                assert arg in args
        else:
            assert args == []

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


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

    @pytest.mark.parametrize(
        ("which_results", "expected"),
        [
            ({"gcc": "/usr/bin/gcc", "clang": None, "cl": None}, "gcc"),
            ({"gcc": None, "clang": "/usr/bin/clang", "cl": None}, "clang"),
            ({"gcc": None, "clang": None, "cl": "C:\\vc\\cl.exe"}, "cl"),
            ({"gcc": None, "clang": None, "cl": None}, None),
        ],
    )
    def test_find_compiler(self, which_results, expected):
        """Test compiler detection with various available compilers."""
        with mock.patch("shutil.which") as mock_which:
            mock_which.side_effect = which_results.get
            result = find_compiler()
            assert result == expected


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

    @pytest.mark.parametrize(
        ("compiler_name", "expected_arg"),
        [
            ("gcc", "-std=c99"),
            ("clang", "-std=c99"),
            ("cl", "/std:c99"),
        ],
    )
    def test_compiler_configs(self, compiler_name, expected_arg):
        """Test that all compiler configs are properly defined."""
        configs = [c for c in _COMPILER_CONFIGS if c.name == compiler_name]
        assert len(configs) == 1
        assert expected_arg in configs[0].args


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

    @pytest.mark.parametrize(
        ("template", "expected_entry_point"),
        [
            (_WINDOWS_GUI_TEMPLATE, "int APIENTRY WinMain"),
            (_WINDOWS_CONSOLE_TEMPLATE, "int main("),
            (_UNIX_GUI_TEMPLATE, "int main("),
            (_UNIX_CONSOLE_TEMPLATE, "int main("),
            (_MACOS_GUI_TEMPLATE, "int main("),
            (_MACOS_CONSOLE_TEMPLATE, "int main("),
        ],
    )
    def test_template_has_entry_point(self, template, expected_entry_point):
        """Test all templates have correct entry points."""
        assert expected_entry_point in template

    @pytest.mark.parametrize(
        "template",
        [
            _WINDOWS_GUI_TEMPLATE,
            _WINDOWS_CONSOLE_TEMPLATE,
            _UNIX_GUI_TEMPLATE,
            _UNIX_CONSOLE_TEMPLATE,
            _MACOS_GUI_TEMPLATE,
            _MACOS_CONSOLE_TEMPLATE,
        ],
    )
    def test_template_has_placeholders(self, template):
        """Test all templates contain required placeholders."""
        assert "${ENTRY_FILE}" in 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 = 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 = 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 = 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(
                    "pytola.dev.pypack.components.loader.find_compiler",
                    return_value="gcc",
                ):
                    result = compile_c_source(str(c_source), output, None)
                    assert result is True

    def test_compile_with_cl_compiler_gui(self):
        """Test compilation with MSVC cl compiler for GUI application."""
        with tempfile.TemporaryDirectory() as temp_dir:
            c_source = Path(temp_dir) / "test_gui.c"
            c_source.write_text("int APIENTRY WinMain() { return 0; }")
            output = Path(temp_dir) / "test"

            with mock.patch("subprocess.run") as mock_run:
                mock_run.return_value = mock.Mock(returncode=0, stdout="", stderr="")
                result = compile_c_source(str(c_source), output, "cl")
                assert result is True
                # 验证命令行包含 SUBSYSTEM:WINDOWS
                call_args = mock_run.call_args[0][0]
                assert "/SUBSYSTEM:WINDOWS" in call_args

    def test_compile_with_cl_compiler_console(self):
        """Test compilation with MSVC cl compiler for console application."""
        with tempfile.TemporaryDirectory() as temp_dir:
            c_source = Path(temp_dir) / "test_console.c"
            c_source.write_text("int main() { return 0; }")
            output = Path(temp_dir) / "test"

            with mock.patch("subprocess.run") as mock_run:
                mock_run.return_value = mock.Mock(returncode=0, stdout="", stderr="")
                result = compile_c_source(str(c_source), output, "cl")
                assert result is True
                # 验证命令行包含 SUBSYSTEM:CONSOLE
                call_args = mock_run.call_args[0][0]
                assert "/SUBSYSTEM:CONSOLE" in call_args

    def test_compile_with_gcc_windows_gui(self):
        """Test compilation with GCC on Windows for GUI application."""
        with tempfile.TemporaryDirectory() as temp_dir:
            c_source = Path(temp_dir) / "test_gui.c"
            c_source.write_text("int APIENTRY WinMain() { return 0; }")
            output = Path(temp_dir) / "test"

            with mock.patch("subprocess.run") as mock_run:
                mock_run.return_value = mock.Mock(returncode=0, stdout="", stderr="")
                with mock.patch("pytola.dev.pypack.components.loader.is_windows", True):
                    result = compile_c_source(str(c_source), output, "gcc")
                    assert result is True
                    # 验证命令行包含 -mwindows
                    call_args = mock_run.call_args[0][0]
                    assert "-mwindows" in call_args

    def test_compile_exception_handling(self):
        """Test compilation handles unexpected exceptions."""
        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=RuntimeError("Unexpected error")):
                result = compile_c_source(str(c_source), output, "gcc")
                assert result is False


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 TestEntryFile:
    """Test EntryFile dataclass."""

    @pytest.mark.parametrize(
        ("project_name", "source_file", "expected_entry_name", "expected_module_name"),
        [
            ("myapp", Path("myapp.py"), "myapp", "myapp"),
            ("my-app", Path("my-app.py"), "my-app", "my_app"),
            ("test_tool", Path("test_tool.py"), "test_tool", "test_tool"),
            ("app", Path("app_gui.py"), "app_gui", "app_gui"),
        ],
    )
    def test_entry_file_properties(self, project_name, source_file, expected_entry_name, expected_module_name):
        """Test EntryFile dataclass properties."""
        entry_file = EntryFile(project_name=project_name, source_file=source_file)
        assert entry_file.entry_name == expected_entry_name
        assert entry_file.module_name == expected_module_name
        assert entry_file.project_name == project_name
        assert entry_file.source_file == source_file

    def test_entry_file_repr(self):
        """Test EntryFile string representation."""
        entry_file = EntryFile(project_name="test", source_file=Path("test.py"))
        repr_str = repr(entry_file)
        assert "EntryFile" in repr_str
        assert "project_name=test" in repr_str
        assert "entry_name=test" in repr_str


class TestPrepareEntryFile:
    """Test entry file preparation."""

    def test_prepare_entry_file_basic(self):
        """Test entry file generation without Qt."""
        project = Project._from_dict(
            {
                "name": "test-project",
                "dependencies": [],
                "keywords": [],
                "toml_path": Path("pyproject.toml"),
            }
        )
        result = prepare_entry_file(project, "test_project")

        assert "from src.test_project.test_project import main" in result
        assert "main()" in result
        assert "sys.path.append" in result
        # Qt配置不应该存在
        assert "QT_QPA_PLATFORM_PLUGIN_PATH" not in result

    def test_prepare_entry_file_with_qt(self):
        """Test entry file generation with Qt."""
        project = Project._from_dict(
            {
                "name": "qt-app",
                "dependencies": ["PySide2"],
                "keywords": ["gui"],
                "toml_path": Path("pyproject.toml"),
            }
        )
        result = prepare_entry_file(project, "qt_app")

        assert "from src.qt_app.qt_app import main" in result
        assert "QT_QPA_PLATFORM_PLUGIN_PATH" in result
        assert "PySide2" in result
        assert "qt_dir" in result

    def test_prepare_entry_file_qt_only_dependencies(self):
        project = Project._from_dict(
            {
                "name": "qt-deps-only",
                "dependencies": ["PySide2>=5.15.0"],
                "keywords": [],  # 故意不包含GUI关键词
                "toml_path": Path("pyproject.toml"),
            }
        )
        result = prepare_entry_file(project, "qt_deps_only")

        # 即使没有GUI关键词, 只要有Qt依赖也应该生成Qt配置
        assert "from src.qt_deps_only.qt_deps_only import main" in result
        assert "QT_QPA_PLATFORM_PLUGIN_PATH" in result
        assert "PySide2" in result
        assert "qt_dir" in result

    def test_prepare_entry_file_qt_name_mapping(self):
        # 测试小写的pyside2依赖应该映射到PySide2目录
        project1 = Project._from_dict(
            {
                "name": "test-pyside2",
                "dependencies": ["pyside2>=5.15.0"],  # 小写依赖名
                "keywords": ["gui"],
                "toml_path": Path("pyproject.toml"),
            }
        )
        result1 = prepare_entry_file(project1, "test_pyside2")
        assert 'site-packages" / "PySide2"' in result1  # 应该是PySide2而不是pyside2

        # 测试小写的pyqt5依赖应该映射到PyQt5目录
        project2 = Project._from_dict(
            {
                "name": "test-pyqt5",
                "dependencies": ["pyqt5>=5.15.0"],  # 小写依赖名
                "keywords": ["gui"],
                "toml_path": Path("pyproject.toml"),
            }
        )
        result2 = prepare_entry_file(project2, "test_pyqt5")
        assert 'site-packages" / "PyQt5"' in result2  # 应该是PyQt5而不是pyqt5

    @given(
        project_name=st.text(
            alphabet=st.characters(whitelist_categories=("Ll", "Lu", "Nd")),
            min_size=1,
            max_size=20,
        ).filter(lambda x: x and x[0].isalpha())
    )
    def test_prepare_entry_file_various_names(self, project_name):
        """Test entry file generation with various project names using hypothesis."""
        project = Project._from_dict(
            {
                "name": project_name,
                "dependencies": [],
                "keywords": [],
                "toml_path": Path("pyproject.toml"),
            }
        )
        result = prepare_entry_file(project, project_name.replace("-", "_"))
        assert "import main" in result
        assert "main()" in result


class TestIsEntryFile:
    """Test entry file detection."""

    def test_is_entry_file_with_main_function(self, tmp_path):
        """Test detection of file with main() function."""
        entry_file = tmp_path / "app.py"
        entry_file.write_text("def main():\n    pass\n")
        assert is_entry_file(entry_file) is True

    def test_is_entry_file_with_main_block_single_quotes(self, tmp_path):
        """Test detection of file with if __name__ == '__main__' block."""
        entry_file = tmp_path / "script.py"
        entry_file.write_text("if __name__ == '__main__':\n    print('test')\n")
        assert is_entry_file(entry_file) is True

    def test_is_entry_file_with_main_block_double_quotes(self, tmp_path):
        r"""Test detection of file with if __name__ == \"__main__\" block."""
        entry_file = tmp_path / "script.py"
        entry_file.write_text('if __name__ == "__main__":\n    print("test")\n')
        assert is_entry_file(entry_file) is True

    def test_is_entry_file_without_entry_point(self, tmp_path):
        """Test detection fails for file without entry point."""
        non_entry_file = tmp_path / "utils.py"
        non_entry_file.write_text("def helper():\n    pass\n")
        assert is_entry_file(non_entry_file) is False

    def test_is_entry_file_nonexistent(self, tmp_path):
        """Test detection fails for nonexistent file."""
        nonexistent = tmp_path / "nonexistent.py"
        assert is_entry_file(nonexistent) is False

    def test_is_entry_file_read_error(self, tmp_path):
        """Test detection handles read errors gracefully."""
        with mock.patch("pathlib.Path.read_text", side_effect=PermissionError):
            entry_file = tmp_path / "app.py"
            entry_file.write_text("def main(): pass")
            assert is_entry_file(entry_file) is False


class TestDetectEntryFiles:
    """Test entry file detection in project directory."""

    def test_detect_single_entry_file(self, tmp_path):
        """Test detection of single entry file."""
        (tmp_path / "myapp.py").write_text("def main(): pass")
        (tmp_path / "utils.py").write_text("def helper(): pass")

        entry_files = detect_entry_files(tmp_path, "myapp")
        assert len(entry_files) == 1
        assert entry_files[0].entry_name == "myapp"

    def test_detect_multiple_entry_files(self, tmp_path):
        """Test detection of multiple entry files."""
        (tmp_path / "app.py").write_text("def main(): pass")
        (tmp_path / "app_gui.py").write_text("if __name__ == '__main__': pass")
        (tmp_path / "utils.py").write_text("def helper(): pass")

        entry_files = detect_entry_files(tmp_path, "app")
        assert len(entry_files) == 2
        entry_names = {ef.entry_name for ef in entry_files}
        assert "app" in entry_names
        assert "app_gui" in entry_names

    def test_detect_no_entry_files(self, tmp_path):
        """Test detection when no entry files exist."""
        (tmp_path / "utils.py").write_text("def helper(): pass")
        (tmp_path / "config.py").write_text("CONFIG = {}")

        entry_files = detect_entry_files(tmp_path, "myapp")
        assert len(entry_files) == 0

    def test_detect_entry_files_hyphenated_name(self, tmp_path):
        """Test detection normalizes hyphenated project names."""
        (tmp_path / "my-app.py").write_text("def main(): pass")

        entry_files = detect_entry_files(tmp_path, "my-app")
        assert len(entry_files) == 1
        assert entry_files[0].project_name == "my_app"


class TestPyLoaderBuilder:
    """Test PyLoaderBuilder class."""

    def test_builder_properties(self, tmp_path):
        """Test PyLoaderBuilder property calculations."""
        project = Project._from_dict(
            {
                "name": "test-project",
                "dependencies": [],
                "keywords": [],
                "toml_path": Path("pyproject.toml"),
            }
        )
        entry_file = EntryFile(project_name="test_project", source_file=Path("main.py"))
        generator = PyLoaderGenerator(root_dir=tmp_path)
        builder = PyLoaderBuilder(parent=generator, project=project, entry_file=entry_file, is_debug=False)

        # 测试属性计算
        assert builder.build_dir == tmp_path / _DEFAULT_BUILD_DIR
        assert builder.output_dir == tmp_path / _DEFAULT_OUTPUT_DIR
        assert builder.output_exe.name == f"test_project{ext}"
        assert builder.entry_filepath.name == "test_project.ent"
        assert builder.c_source_path.name == "test_project_console.c"  # project.loader_type is console

    def test_builder_properties_debug(self, tmp_path):
        """Test PyLoaderBuilder property calculations in debug mode."""
        project = Project._from_dict(
            {
                "name": "test-project",
                "dependencies": [],
                "keywords": [],
                "toml_path": Path("pyproject.toml"),
            }
        )
        entry_file = EntryFile(project_name="test_project", source_file=Path("main.py"))
        generator = PyLoaderGenerator(root_dir=tmp_path)
        builder = PyLoaderBuilder(parent=generator, project=project, entry_file=entry_file, is_debug=True)

        # 在调试模式下, C源文件名应该不同
        assert builder.c_source_path.name == "test_project_debug_console.c"

    def test_builder_generate_success(self, tmp_path):
        """Test successful loader generation with PyLoaderBuilder."""
        project = Project._from_dict(
            {
                "name": "test-project",
                "dependencies": [],
                "keywords": [],
                "toml_path": Path("pyproject.toml"),
            }
        )
        # 创建一个入口文件
        (tmp_path / "main.py").write_text("def main(): pass")
        entry_file = EntryFile(project_name="test_project", source_file=tmp_path / "main.py")
        generator = PyLoaderGenerator(root_dir=tmp_path)
        builder = PyLoaderBuilder(parent=generator, project=project, entry_file=entry_file, is_debug=False)

        # Mock 编译过程
        with mock.patch("pytola.dev.pypack.components.loader.find_compiler", return_value="gcc"), mock.patch(
            "pytola.dev.pypack.components.loader.compile_c_source", return_value=True
        ):
            success = builder.generate()
            assert success is True
            # 验证入口文件被创建
            assert builder.entry_filepath.exists()
            # 验证C源文件被创建
            assert builder.c_source_path.exists()

    def test_builder_generate_compiler_not_found(self, tmp_path):
        """Test loader generation when compiler is not found."""
        project = Project._from_dict(
            {
                "name": "test-project",
                "dependencies": [],
                "keywords": [],
                "toml_path": Path("pyproject.toml"),
            }
        )
        (tmp_path / "main.py").write_text("def main(): pass")
        entry_file = EntryFile(project_name="test_project", source_file=tmp_path / "main.py")
        generator = PyLoaderGenerator(root_dir=tmp_path)
        builder = PyLoaderBuilder(parent=generator, project=project, entry_file=entry_file, is_debug=False)

        # Mock 编译器查找失败
        with mock.patch("pytola.dev.pypack.components.loader.find_compiler", return_value=None):
            success = builder.generate()
            assert success is False

    def test_builder_generate_compile_failed(self, tmp_path):
        """Test loader generation when compilation fails."""
        project = Project._from_dict(
            {
                "name": "test-project",
                "dependencies": [],
                "keywords": [],
                "toml_path": Path("pyproject.toml"),
            }
        )
        (tmp_path / "main.py").write_text("def main(): pass")
        entry_file = EntryFile(project_name="test_project", source_file=tmp_path / "main.py")
        generator = PyLoaderGenerator(root_dir=tmp_path)
        builder = PyLoaderBuilder(parent=generator, project=project, entry_file=entry_file, is_debug=False)

        # Mock 编译失败
        with mock.patch("pytola.dev.pypack.components.loader.find_compiler", return_value="gcc"), mock.patch(
            "pytola.dev.pypack.components.loader.compile_c_source", return_value=False
        ):
            success = builder.generate()
            assert success is False


class TestPyLoaderGenerator:
    """Test PyLoaderGenerator class."""

    def test_generator_properties(self, tmp_path):
        """Test PyLoaderGenerator property calculations."""
        generator = PyLoaderGenerator(root_dir=tmp_path)
        assert generator.root_dir == tmp_path
        assert generator.build_dir == _DEFAULT_BUILD_DIR
        assert generator.output_dir == _DEFAULT_OUTPUT_DIR

    def test_generator_with_custom_directories(self, tmp_path):
        """Test PyLoaderGenerator with custom build/output directories."""
        build_dir = tmp_path / "custom_build"
        output_dir = tmp_path / "custom_dist"
        generator = PyLoaderGenerator(root_dir=tmp_path, build_dir=build_dir, output_dir=output_dir)
        assert generator.build_dir == build_dir
        assert generator.output_dir == output_dir

    def test_generate_for_project_success(self, tmp_path):
        """Test successful project generation with PyLoaderGenerator."""
        project = Project._from_dict(
            {
                "name": "test-project",
                "dependencies": [],
                "keywords": [],
                "toml_path": Path("pyproject.toml"),
            }
        )
        # 创建一个入口文件
        (tmp_path / "main.py").write_text("def main(): pass")

        generator = PyLoaderGenerator(root_dir=tmp_path)

        # Mock 编译过程
        with mock.patch("pytola.dev.pypack.components.loader.find_compiler", return_value="gcc"), mock.patch(
            "pytola.dev.pypack.components.loader.compile_c_source", return_value=True
        ):
            success = generator.generate_for_project(project, tmp_path, debug=False)
            assert success is True

    def test_generate_for_project_no_entry_files(self, tmp_path):
        """Test project generation when no entry files are found.

        For library projects (no entry files and not console apps),
        should return True to indicate skipping rather than failure.
        """
        project = Project._from_dict(
            {
                "name": "test-project",
                "dependencies": [],
                "keywords": [],
                "toml_path": Path("pyproject.toml"),
            }
        )
        # 不创建任何入口文件
        generator = PyLoaderGenerator(root_dir=tmp_path)

        success = generator.generate_for_project(project, tmp_path, debug=False)
        assert success is True  # Library project, should skip gracefully

    def test_generate_for_project_compiler_not_found(self, tmp_path):
        """Test project generation when compiler is not found."""
        project = Project._from_dict(
            {
                "name": "test-project",
                "dependencies": [],
                "keywords": [],
                "toml_path": Path("pyproject.toml"),
            }
        )
        # 创建一个入口文件
        (tmp_path / "main.py").write_text("def main(): pass")

        generator = PyLoaderGenerator(root_dir=tmp_path)

        # Mock 编译器查找失败
        with mock.patch("pytola.dev.pypack.components.loader.find_compiler", return_value=None):
            success = generator.generate_for_project(project, tmp_path, debug=False)
            assert success is False

    def test_generate_for_project_with_missing_directory(self, tmp_path):
        """Test generate_for_project with missing project directory.

        For library projects with missing directory, should return True
        to indicate skipping rather than failure.
        """
        project = Project._from_dict(
            {
                "name": "missing-project",
                "dependencies": [],
                "keywords": [],
                "toml_path": Path("pyproject.toml"),
            }
        )

        generator = PyLoaderGenerator(root_dir=tmp_path)

        # Try to generate for a project with missing directory
        project_dir = tmp_path / "missing_project"
        success = generator.generate_for_project(project, project_dir, debug=False)
        assert success is True  # Library project, should skip gracefully


class TestCommandLineInterface:
    """Test command line interface."""

    def test_parse_args_defaults(self):
        """Test argument parsing with defaults."""
        with mock.patch("sys.argv", ["pyloadergen"]):
            args = parse_args()
            assert args.directory == str(Path.cwd())
            assert args.debug is False
            assert args.compiler is None

    def test_parse_args_with_directory(self):
        """Test argument parsing with directory."""
        test_dir = "/test/dir"
        with mock.patch("sys.argv", ["pyloadergen", test_dir]):
            args = parse_args()
            assert args.directory == test_dir

    def test_parse_args_with_debug(self):
        """Test argument parsing with debug flag."""
        with mock.patch("sys.argv", ["pyloadergen", "--debug"]):
            args = parse_args()
            assert args.debug is True

    def test_parse_args_with_compiler(self):
        """Test argument parsing with compiler specification."""
        compiler = "gcc"
        with mock.patch("sys.argv", ["pyloadergen", "--compiler", compiler]):
            args = parse_args()
            assert args.compiler == compiler

    def test_main_function_with_debug(self, tmp_path):
        """Test main function with debug flag."""
        # 创建一个有效的 pyproject.toml 文件
        pyproject_file = tmp_path / "pyproject.toml"
        pyproject_file.write_text("""[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

[project]
name = "test-project"
version = "1.0.0"
description = "Test project"
""")

        # 创建一个入口文件
        (tmp_path / "main.py").write_text("def main(): pass")

        with mock.patch("sys.argv", ["pyloadergen", str(tmp_path), "--debug"]), mock.patch(
            "pytola.dev.pypack.components.loader.find_compiler", return_value="gcc"
        ), mock.patch("pytola.dev.pypack.components.loader.compile_c_source", return_value=True):
            # 调用主函数
            main()
            # 验证日志级别设置为 DEBUG
            assert logger.level <= 10  # DEBUG level

    def test_main_function_directory_not_exists(self, tmp_path):
        """Test main function with non-existent directory."""
        non_existent_dir = str(tmp_path / "nonexistent")

        with mock.patch("sys.argv", ["pyloadergen", non_existent_dir]), mock.patch("sys.stderr"):
            main()
            # 函数应该不会崩溃


class TestEdgeCases:
    """Test edge cases and error conditions."""

    def test_main_function_directory_exists_but_invalid(self, tmp_path):
        """Test main function with existing directory but no projects."""
        with mock.patch("sys.argv", ["pyloadergen", str(tmp_path)]), mock.patch(
            "pytola.dev.pypack.components.loader.find_compiler", return_value="gcc"
        ), mock.patch("pytola.dev.pypack.components.loader.compile_c_source", return_value=True):
            # 调用主函数 - 这应该不会崩溃
            main()

    def test_generator_solution_loading_failure(self, tmp_path):
        """Test PyLoaderGenerator when solution loading fails."""
        generator = PyLoaderGenerator(root_dir=tmp_path)

        # Mock the solution property to return an empty solution
        with mock.patch.object(generator, "solution", create=True) as mock_solution:
            mock_solution.projects = {}

            # This should handle the case when no projects are found
            generator.run(debug=False)

    def test_generate_for_project_with_missing_directory(self, tmp_path):
        """Test generate_for_project with missing project directory.

        For library projects with missing directory, should return True
        to indicate skipping rather than failure.
        """
        project = Project._from_dict(
            {
                "name": "missing-project",
                "dependencies": [],
                "keywords": [],
                "toml_path": Path("pyproject.toml"),
            }
        )

        generator = PyLoaderGenerator(root_dir=tmp_path)

        # Try to generate for a project with missing directory
        project_dir = tmp_path / "missing_project"
        success = generator.generate_for_project(project, project_dir, debug=False)
        assert success is True  # Library project, should skip gracefully


class TestAdditionalEdgeCases:
    """Additional edge cases to improve coverage."""

    def test_compile_c_source_no_compiler_found(self, tmp_path):
        """Test compile_c_source when no compiler is found."""
        c_source = tmp_path / "test.c"
        c_source.write_text("#include <stdio.h>\nint main() { return 0; }")
        output = tmp_path / "test.exe"

        # Mock find_compiler to return None
        with mock.patch("pytola.dev.pypack.components.loader.find_compiler", return_value=None):
            result = compile_c_source(str(c_source), output, None)
            assert result is False

    def test_pyloaderbuilder_no_compiler_found(self, tmp_path):
        """Test PyLoaderBuilder when compiler is not found."""
        project = Project._from_dict(
            {
                "name": "test-project",
                "dependencies": [],
                "keywords": [],
                "toml_path": Path("pyproject.toml"),
            }
        )
        (tmp_path / "main.py").write_text("def main(): pass")
        entry_file = EntryFile(project_name="test_project", source_file=tmp_path / "main.py")
        generator = PyLoaderGenerator(root_dir=tmp_path)
        builder = PyLoaderBuilder(parent=generator, project=project, entry_file=entry_file, is_debug=False)

        # Mock find_compiler to return None
        with mock.patch("pytola.dev.pypack.components.loader.find_compiler", return_value=None):
            success = builder.generate()
            assert success is False

    def test_generator_run_with_no_projects(self, tmp_path):
        """Test PyLoaderGenerator.run when no projects are found."""
        generator = PyLoaderGenerator(root_dir=tmp_path)

        # Mock the solution to return no projects
        with mock.patch.object(generator, "solution", create=True) as mock_solution:
            mock_solution.projects = {}

            # This should handle the case when no projects are found
            generator.run(debug=False)

    def test_generator_run_with_multiple_projects_missing_dirs(self, tmp_path):
        """Test PyLoaderGenerator.run with multiple projects but missing directories."""
        generator = PyLoaderGenerator(root_dir=tmp_path)

        # Mock the solution to return multiple projects
        mock_projects = {
            "project1": Project._from_dict(
                {
                    "name": "project1",
                    "dependencies": [],
                    "keywords": [],
                    "toml_path": Path("pyproject.toml"),
                }
            ),
            "project2": Project._from_dict(
                {
                    "name": "project2",
                    "dependencies": [],
                    "keywords": [],
                    "toml_path": Path("pyproject.toml"),
                }
            ),
        }

        with mock.patch.object(generator, "solution", create=True) as mock_solution:
            mock_solution.projects = mock_projects

            # Run the generator - this should handle missing project directories
            generator.run(debug=False)

    def test_generator_run_with_multiple_projects_and_exceptions(self, tmp_path):
        generator = PyLoaderGenerator(root_dir=tmp_path)

        # Mock the solution to return multiple projects
        mock_projects = {
            "project1": Project._from_dict(
                {
                    "name": "project1",
                    "dependencies": [],
                    "keywords": [],
                    "toml_path": Path("pyproject.toml"),
                }
            ),
            "project2": Project._from_dict(
                {
                    "name": "project2",
                    "dependencies": [],
                    "keywords": [],
                    "toml_path": Path("pyproject.toml"),
                }
            ),
        }

        # Create project directories
        (tmp_path / "project1").mkdir()
        (tmp_path / "project2").mkdir()

        with mock.patch.object(generator, "solution", create=True) as mock_solution:
            mock_solution.projects = mock_projects

            # Mock generate_for_project to raise an exception for one project
            original_method = generator.generate_for_project

            def mock_generate_for_project(project, project_dir, debug=False):
                if project.name == "project2":
                    raise RuntimeError("Test exception")
                return original_method(project, project_dir, debug)

            with mock.patch.object(generator, "generate_for_project", side_effect=mock_generate_for_project):
                generator.run(debug=False)  # Should handle the exception gracefully

    def test_main_function_with_invalid_directory(self, tmp_path):
        """Test main function with directory that doesn't exist."""
        non_existent_dir = str(tmp_path / "nonexistent")

        with mock.patch("sys.argv", ["pyloadergen", non_existent_dir]):
            # Capture stderr to avoid printing error message
            with mock.patch("sys.stderr"):
                main()  # Should handle gracefully

    def test_main_function_working_dir_not_exist(self):
        """Test main function when working directory doesn't exist."""
        fake_dir = "/fake/nonexistent/directory"
        with mock.patch("sys.argv", ["pyloadergen", fake_dir]), mock.patch("sys.stderr"):
            main()  # Should handle gracefully


class TestRemainingCoverage:
    """Tests for remaining uncovered lines."""

    def test_generator_run_single_project_failed(self, tmp_path):
        """Test PyLoaderGenerator.run when single project generation fails."""
        generator = PyLoaderGenerator(root_dir=tmp_path)

        # Mock the solution to return one project
        mock_projects = {
            "project1": Project._from_dict(
                {
                    "name": "project1",
                    "dependencies": [],
                    "keywords": [],
                    "toml_path": Path("pyproject.toml"),
                }
            )
        }

        with mock.patch.object(generator, "solution", create=True) as mock_solution:
            mock_solution.projects = mock_projects

            # Mock generate_for_project to return False (failure)
            with mock.patch.object(generator, "generate_for_project", return_value=False):
                generator.run(debug=False)  # This should trigger line 1250

    def test_generator_run_multiple_projects_success(self, tmp_path):
        """Test PyLoaderGenerator.run when multiple projects all succeed."""
        generator = PyLoaderGenerator(root_dir=tmp_path)

        # Mock the solution to return multiple projects
        mock_projects = {
            "project1": Project._from_dict(
                {
                    "name": "project1",
                    "dependencies": [],
                    "keywords": [],
                    "toml_path": Path("pyproject.toml"),
                }
            ),
            "project2": Project._from_dict(
                {
                    "name": "project2",
                    "dependencies": [],
                    "keywords": [],
                    "toml_path": Path("pyproject.toml"),
                }
            ),
        }

        # Create project directories
        (tmp_path / "project1").mkdir()
        (tmp_path / "project2").mkdir()

        with mock.patch.object(generator, "solution", create=True) as mock_solution:
            mock_solution.projects = mock_projects

            # Mock generate_for_project to return True (success)
            with mock.patch.object(generator, "generate_for_project", return_value=True):
                generator.run(debug=False)  # This should trigger line 1278

    def test_main_execution_directly(self, tmp_path):
        """Test the main function directly to cover __main__ condition."""
        # Create a valid pyproject.toml file
        pyproject_file = tmp_path / "pyproject.toml"
        pyproject_file.write_text("""[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

[project]
name = "test-project"
version = "1.0.0"
description = "Test project"
""")

        # Create a main.py file
        (tmp_path / "main.py").write_text("def main(): pass")

        # Mock all the necessary parts to avoid actual compilation
        with mock.patch("sys.argv", ["pyloadergen", str(tmp_path)]), mock.patch(
            "pytola.dev.pypack.components.loader.find_compiler", return_value="gcc"
        ), mock.patch("pytola.dev.pypack.components.loader.compile_c_source", return_value=True):
            main()


class TestMainExecution:
    """Test direct script execution."""

    def test_script_execution_as_main(self, tmp_path):
        """Test execution when script is run as main."""
        # We can't easily test the __name__ == "__main__" condition directly
        # since it depends on how the script is executed, but we've already
        # tested the main() function thoroughly.
        # The one line left (line 1328) is just the if __name__ == "__main__": condition
        # which can't be covered by unit tests in the normal way.
        pass
