import importlib.util
import os

import pytest

import portalocker
from portalocker import LockFlags
from portalocker_tests.conftest import LOCKERS

# Since portalocker 4.0.0 pywin32 is an optional extra. On Windows without it,
# `win32file` is unavailable and shared locks are unsupported by design
# (msvcrt has no true shared lock), so they raise ImportError pointing at the
# `portalocker[win32]` extra. Skip the shared-lock cases in that configuration.
_needs_win32_extra = pytest.mark.skipif(
    os.name == 'nt' and importlib.util.find_spec('win32file') is None,
    reason='shared locks on Windows require the pywin32 extra '
    '(portalocker[win32])',
)


def test_exclusive(tmpdir):
    """Test that exclusive lock prevents reading and writing by others."""
    tmpfile = tmpdir.join('test_exclusive.lock')
    text_0 = 'spam and eggs'
    with open(tmpfile, 'w') as fh:
        fh.write(text_0)

    with open(tmpfile) as fh:
        portalocker.lock(fh, portalocker.LOCK_EX | portalocker.LOCK_NB)

        # Make sure we can't read the locked file
        with (
            pytest.raises(portalocker.LockException),
            open(
                tmpfile,
                'r+',
            ) as fh2,
        ):
            portalocker.lock(fh2, portalocker.LOCK_EX | portalocker.LOCK_NB)
            assert fh2.read() == text_0

        # Make sure we can't write the locked file
        with (
            pytest.raises(portalocker.LockException),
            open(
                tmpfile,
                'w+',
            ) as fh2,
        ):
            portalocker.lock(fh2, portalocker.LOCK_EX | portalocker.LOCK_NB)
            fh2.write('surprise and fear')

        # Make sure we can explicitly unlock the file
        portalocker.unlock(fh)


@_needs_win32_extra
def test_shared(tmpdir):
    """Test that shared lock allows reading but not writing by others."""
    tmpfile = tmpdir.join('test_shared.lock')
    with open(tmpfile, 'w') as fh:
        fh.write('spam and eggs')

    with open(tmpfile) as f:
        portalocker.lock(f, portalocker.LOCK_SH | portalocker.LOCK_NB)

        # Make sure we can read the locked file
        with open(tmpfile) as fh2:
            portalocker.lock(fh2, portalocker.LOCK_SH | portalocker.LOCK_NB)
            assert fh2.read() == 'spam and eggs'

        # Make sure we can't write the locked file
        with (
            pytest.raises(portalocker.LockException),
            open(
                tmpfile,
                'w+',
            ) as fh2,
        ):
            portalocker.lock(fh2, portalocker.LOCK_EX | portalocker.LOCK_NB)
            fh2.write('surprise and fear')

        # Make sure we can explicitly unlock the file
        portalocker.unlock(f)


@pytest.mark.parametrize('locker', LOCKERS, indirect=True)
def test_blocking_timeout(tmpdir, locker):
    """Test that a warning is raised when using a blocking timeout."""
    tmpfile = tmpdir.join('test_blocking_timeout.lock')
    flags = LockFlags.SHARED

    with pytest.warns(UserWarning):  # noqa: SIM117
        with portalocker.Lock(tmpfile, 'a+', timeout=5, flags=flags):
            pass

    lock = portalocker.Lock(tmpfile, 'a+', flags=flags)
    with pytest.warns(UserWarning):
        lock.acquire(timeout=5)


@pytest.mark.skipif(
    os.name == 'nt',
    reason='Windows uses an entirely different lockmechanism, which does not '
    'support NON_BLOCKING flag within a single process.',
)
@pytest.mark.parametrize('locker', LOCKERS, indirect=True)
def test_nonblocking(tmpdir, locker):
    """Test that using NON_BLOCKING flag raises RuntimeError."""
    tmpfile = tmpdir.join('test_nonblocking.lock')
    with open(tmpfile, 'w') as fh, pytest.raises(RuntimeError):
        portalocker.lock(fh, LockFlags.NON_BLOCKING)
