import importlib
import importlib.util
import subprocess
import sys

import pytest

from portalocker import __main__


def test_combine_uses_sys_executable(tmp_path, monkeypatch):
    """B6: the combined-file smoke run must use ``sys.executable``.

    Hardcoding ``python3`` breaks on Windows venvs (no ``python3.exe``).
    """
    calls: list[list[str]] = []

    def spy(cmd, *args, **kwargs):
        calls.append(list(cmd))
        return subprocess.CompletedProcess(cmd, 0)

    monkeypatch.setattr(subprocess, 'run', spy)
    output_file = tmp_path / 'combined.py'
    __main__.main(['combine', '--output-file', str(output_file)])

    assert [sys.executable, str(output_file)] in calls


def test_combined(tmpdir):
    output_file = tmpdir.join('combined.py')
    __main__.main(['combine', '--output-file', output_file.strpath])

    print(output_file)  # noqa: T201
    print('#################')  # noqa: T201
    print(output_file.read())  # noqa: T201
    print('#################')  # noqa: T201

    sys.path.append(output_file.dirname)
    # The `combined` module is generated by the `combine` call above, so
    # static analysis can't resolve it; import it dynamically instead.
    combined = importlib.import_module('combined')

    assert hasattr(combined, 'lock')


def test_build_extra_creates_versioned_dist():
    """``build_extra.py`` must work with the path-based ``combine`` interface.

    The release script runs ``build_extra.py`` to emit the versioned single
    file. It used to hand ``combine()`` an open file object; after ``combine``
    switched to a ``pathlib.Path`` ``output_file`` (mkdir/open on it) that
    crashed with ``AttributeError``. Exercise the script exactly as the
    release flow does and assert the versioned dist file is produced.
    """
    from portalocker import __about__

    dist_file = __main__.dist_path / f'portalocker-{__about__.__version__}.py'
    pre_existing = dist_file.exists()
    try:
        result = subprocess.run(
            [sys.executable, 'build_extra.py'],
            cwd=__main__.base_path,
            capture_output=True,
            text=True,
        )
        assert result.returncode == 0, result.stderr
        assert dist_file.exists(), (
            f'{dist_file} was not created\n{result.stdout}\n{result.stderr}'
        )
    finally:
        if not pre_existing:
            dist_file.unlink(missing_ok=True)


@pytest.mark.skipif(
    importlib.util.find_spec('redis') is None,
    reason='requires the optional redis package (always in the tests extra)',
)
def test_combined_includes_redis(tmp_path):
    """B7: the vendored single file must inline redis and bind RedisLock.

    The indented ``from .redis import RedisLock`` in ``__init__.py`` used to
    survive verbatim into the combined file, where it always ImportErrored,
    leaving ``RedisLock`` permanently ``None`` even with redis installed.
    """
    output_file = tmp_path / 'combined_redis.py'
    __main__.main(['combine', '--output-file', str(output_file)])

    # Execute the vendored file in a subprocess exactly as a downstream user
    # importing the single file would (no package context).
    script = (
        'import importlib.util;'
        'spec = importlib.util.spec_from_file_location('
        f'"vendored", r"{output_file}");'
        'mod = importlib.util.module_from_spec(spec);'
        'spec.loader.exec_module(mod);'
        'assert mod.RedisLock is not None, "RedisLock is None";'
        'assert callable(mod.lock), "lock is not callable";'
        'print("OK")'
    )
    result = subprocess.run(
        [sys.executable, '-c', script],
        capture_output=True,
        text=True,
    )
    assert result.returncode == 0, result.stderr
    assert 'OK' in result.stdout


def test_combine_emits_type_checking_block_verbatim(tmp_path):
    """The bundler must emit ``if TYPE_CHECKING:`` blocks verbatim rather
    than inlining their relative imports (which would duplicate a module or
    break the runtime guard the else-branch relies on).

    Exercised directly through ``_read_file`` with a synthetic module so the
    behaviour is covered regardless of whether any shipped module currently
    uses a ``TYPE_CHECKING`` guard.
    """
    module = tmp_path / 'synthetic.py'
    module.write_text(
        'import typing\n'
        'if typing.TYPE_CHECKING:\n'
        '    from .redis import RedisLock\n'
        '\n'
        'value = 1\n',
        encoding='ascii',
    )

    output = ''.join(__main__._read_file(module, set()))

    # The guarded relative import is emitted as-is, not inlined.
    assert 'from .redis import RedisLock' in output
    # ... so redis.py's contents are not pulled in.
    assert 'class RedisLock' not in output
    # The dedented line after the block resumes normal handling.
    assert 'value = 1' in output
