from __future__ import annotations

import pytest
from testing.helpers import contains_exe, contains_ref, has_src, is_exe
from testing.path import join as path

from virtualenv.create.via_global_ref.builtin.cpython.cpython3 import CPython3Windows

CPYTHON3_PATH = (
    "virtualenv.create.via_global_ref.builtin.cpython.common.Path",
    "virtualenv.create.via_global_ref.builtin.cpython.cpython3.Path",
)


@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"])
def test_2_exe_on_default_py_host(py_info, mock_files) -> None:
    mock_files(CPYTHON3_PATH, [py_info.system_executable])
    sources = tuple(CPython3Windows.sources(interpreter=py_info))
    # Default Python exe.
    assert contains_exe(sources, py_info.system_executable)
    # Should always exist.
    assert contains_exe(sources, path(py_info.prefix, "pythonw.exe"))


@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"])
def test_3_exe_on_not_default_py_host(py_info, mock_files) -> None:
    # Not default python host.
    py_info.system_executable = path(py_info.prefix, "python666.exe")
    mock_files(CPYTHON3_PATH, [py_info.system_executable])
    sources = tuple(CPython3Windows.sources(interpreter=py_info))
    # Not default Python exe linked to both the default name and origin.
    assert contains_exe(sources, py_info.system_executable, "python.exe")
    assert contains_exe(sources, py_info.system_executable, "python666.exe")
    # Should always exist.
    assert contains_exe(sources, path(py_info.prefix, "pythonw.exe"))


@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"])
def test_only_shim(py_info, mock_files) -> None:
    shim = path(py_info.system_stdlib, "venv\\scripts\\nt\\python.exe")
    py_files = (
        path(py_info.prefix, "libcrypto-1_1.dll"),
        path(py_info.prefix, "libffi-7.dll"),
        path(py_info.prefix, "_asyncio.pyd"),
        path(py_info.prefix, "_bz2.pyd"),
    )
    mock_files(CPYTHON3_PATH, [shim, *py_files])
    sources = tuple(CPython3Windows.sources(interpreter=py_info))
    assert CPython3Windows.has_shim(interpreter=py_info)
    assert contains_exe(sources, shim)
    assert not contains_exe(sources, py_info.system_executable)
    for file in py_files:
        assert not contains_ref(sources, file)


@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"])
def test_exe_dll_pyd_without_shim(py_info, mock_files) -> None:
    py_files = (
        path(py_info.prefix, "libcrypto-1_1.dll"),
        path(py_info.prefix, "libffi-7.dll"),
        path(py_info.prefix, "_asyncio.pyd"),
        path(py_info.prefix, "_bz2.pyd"),
    )
    mock_files(CPYTHON3_PATH, py_files)
    sources = tuple(CPython3Windows.sources(interpreter=py_info))
    assert not CPython3Windows.has_shim(interpreter=py_info)
    assert contains_exe(sources, py_info.system_executable)
    for file in py_files:
        assert contains_ref(sources, file)


@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"])
def test_python_zip_if_exists_and_set_in_path(py_info, mock_files) -> None:
    python_zip_name = f"python{py_info.version_nodot}.zip"
    python_zip = path(py_info.prefix, python_zip_name)
    mock_files(CPYTHON3_PATH, [python_zip])
    sources = tuple(CPython3Windows.sources(interpreter=py_info))
    assert python_zip in py_info.path
    assert contains_ref(sources, python_zip)


@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"])
def test_no_python_zip_if_exists_and_not_set_in_path(py_info, mock_files) -> None:
    python_zip_name = f"python{py_info.version_nodot}.zip"
    python_zip = path(py_info.prefix, python_zip_name)
    py_info.path.remove(python_zip)
    mock_files(CPYTHON3_PATH, [python_zip])
    sources = tuple(CPython3Windows.sources(interpreter=py_info))
    assert python_zip not in py_info.path
    assert not contains_ref(sources, python_zip)


@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"])
def test_no_python_zip_if_not_exists(py_info, mock_files) -> None:
    python_zip_name = f"python{py_info.version_nodot}.zip"
    python_zip = path(py_info.prefix, python_zip_name)
    # No `python_zip`, just python.exe file.
    mock_files(CPYTHON3_PATH, [py_info.system_executable])
    sources = tuple(CPython3Windows.sources(interpreter=py_info))
    assert python_zip in py_info.path
    assert not contains_ref(sources, python_zip)


@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"])
def test_python3_exe_present(py_info, mock_files) -> None:
    mock_files(CPYTHON3_PATH, [py_info.system_executable])
    sources = tuple(CPython3Windows.sources(interpreter=py_info))
    assert contains_exe(sources, py_info.system_executable, "python3.exe")
    assert contains_exe(sources, py_info.system_executable, "python3")


@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"])
def test_pythonw3_exe_present(py_info, mock_files) -> None:
    mock_files(CPYTHON3_PATH, [py_info.system_executable])
    sources = tuple(CPython3Windows.sources(interpreter=py_info))
    pythonw_refs = [s for s in sources if is_exe(s) and has_src(path(py_info.prefix, "pythonw.exe"))(s)]
    assert len(pythonw_refs) == 1
    assert "pythonw3.exe" in pythonw_refs[0].aliases


@pytest.mark.parametrize("py_info_name", ["cpython3_win_free_threaded"])
def test_free_threaded_exe_naming(py_info, mock_files) -> None:
    mock_files(CPYTHON3_PATH, [py_info.system_executable])
    sources = tuple(CPython3Windows.sources(interpreter=py_info))
    assert contains_exe(sources, py_info.system_executable, "python3.13t.exe")
    pythonw_refs = [s for s in sources if is_exe(s) and has_src(path(py_info.prefix, "pythonw.exe"))(s)]
    assert len(pythonw_refs) == 1
    assert "pythonw3.exe" in pythonw_refs[0].aliases
    assert "pythonw3.13t.exe" in pythonw_refs[0].aliases


@pytest.mark.parametrize("py_info_name", ["cpython3_win_debug"])
def test_debug_build_shim(py_info, mock_files) -> None:
    shim = path(py_info.system_stdlib, "venv\\scripts\\nt\\venvlauncher_d.exe")
    mock_files(CPYTHON3_PATH, [shim])
    assert CPython3Windows.has_shim(interpreter=py_info)
    sources = tuple(CPython3Windows.sources(interpreter=py_info))
    assert contains_exe(sources, shim)


@pytest.mark.parametrize("py_info_name", ["cpython3_win_debug"])
def test_debug_build_uses_venvlauncher(py_info, mock_files) -> None:
    launcher = path(py_info.prefix, "venvlauncher_d.exe")
    w_launcher = path(py_info.prefix, "venvwlauncher_d.exe")
    mock_files(CPYTHON3_PATH, [py_info.system_executable, launcher, w_launcher])
    sources = tuple(CPython3Windows.sources(interpreter=py_info))
    assert contains_exe(sources, launcher, "python.exe")
    assert contains_exe(sources, w_launcher, "pythonw.exe")


@pytest.mark.parametrize("py_info_name", ["cpython3_win_embed"])
def test_pywin32_dll_exclusion(py_info, mock_files) -> None:
    """Test that pywin32 DLLs are excluded from virtualenv creation."""
    # Mock pywin32 DLLs that should be excluded
    pywin32_dlls = (
        path(py_info.prefix, "pywintypes39.dll"),
        path(py_info.prefix, "pywintypes310.dll"),
        path(py_info.prefix, "pythoncom39.dll"),
        path(py_info.prefix, "pythoncom310.dll"),
    )
    # Mock regular DLLs that should be included
    regular_dlls = (
        path(py_info.prefix, "libcrypto-1_1.dll"),
        path(py_info.prefix, "libffi-7.dll"),
    )
    # Only mock the DLL files (no shim) so dll_and_pyd() method will be called
    all_files = [*pywin32_dlls, *regular_dlls]
    mock_files(CPYTHON3_PATH, all_files)
    sources = tuple(CPython3Windows.sources(interpreter=py_info))

    # Make sure we're in the no-shim code path
    assert not CPython3Windows.has_shim(interpreter=py_info)

    # Verify pywin32 DLLs are excluded
    for dll in pywin32_dlls:
        assert not contains_ref(sources, dll), f"pywin32 DLL {dll} should be excluded"

    # Verify regular DLLs are included
    for dll in regular_dlls:
        assert contains_ref(sources, dll), f"Regular DLL {dll} should be included"
