import importlib
import os
import subprocess
import sys
import types
from unittest import mock

import pytest

import lazy_loader as lazy


def test_lazy_import_basics():
    math = lazy.load("math")
    anything_not_real = lazy.load("anything_not_real")

    # Now test that accessing attributes does what it should
    assert math.sin(math.pi) == pytest.approx(0, 1e-6)
    # poor-mans pytest.raises for testing errors on attribute access
    try:
        anything_not_real.pi
        raise AssertionError()  # Should not get here
    except ModuleNotFoundError:
        pass
    assert isinstance(anything_not_real, lazy.DelayedImportErrorModule)
    # see if it changes for second access
    try:
        anything_not_real.pi
        raise AssertionError()  # Should not get here
    except ModuleNotFoundError:
        pass


def test_lazy_import_subpackages():
    with pytest.warns(RuntimeWarning):
        hp = lazy.load("html.parser")
    assert "html" in sys.modules
    assert type(sys.modules["html"]) == type(pytest)
    assert isinstance(hp, importlib.util._LazyModule)
    assert "html.parser" in sys.modules
    assert sys.modules["html.parser"] == hp


def test_lazy_import_impact_on_sys_modules():
    math = lazy.load("math")
    anything_not_real = lazy.load("anything_not_real")

    assert isinstance(math, types.ModuleType)
    assert "math" in sys.modules
    assert isinstance(anything_not_real, lazy.DelayedImportErrorModule)
    assert "anything_not_real" not in sys.modules

    # only do this if numpy is installed
    pytest.importorskip("numpy")
    np = lazy.load("numpy")
    assert isinstance(np, types.ModuleType)
    assert "numpy" in sys.modules

    np.pi  # trigger load of numpy

    assert isinstance(np, types.ModuleType)
    assert "numpy" in sys.modules


def test_lazy_import_nonbuiltins():
    np = lazy.load("numpy")
    sp = lazy.load("scipy")
    if not isinstance(np, lazy.DelayedImportErrorModule):
        assert np.sin(np.pi) == pytest.approx(0, 1e-6)
    if isinstance(sp, lazy.DelayedImportErrorModule):
        try:
            sp.pi
            raise AssertionError()
        except ModuleNotFoundError:
            pass


def test_lazy_attach():
    name = "mymod"
    submods = ["mysubmodule", "anothersubmodule"]
    myall = {"not_real_submod": ["some_var_or_func"]}

    locls = {
        "attach": lazy.attach,
        "name": name,
        "submods": submods,
        "myall": myall,
    }
    s = "__getattr__, __lazy_dir__, __all__ = attach(name, submods, myall)"

    exec(s, {}, locls)
    expected = {
        "attach": lazy.attach,
        "name": name,
        "submods": submods,
        "myall": myall,
        "__getattr__": None,
        "__lazy_dir__": None,
        "__all__": None,
    }
    assert locls.keys() == expected.keys()
    for k, v in expected.items():
        if v is not None:
            assert locls[k] == v


def test_attach_same_module_and_attr_name():
    from lazy_loader.tests import fake_pkg

    # Grab attribute twice, to ensure that importing it does not
    # override function by module
    assert isinstance(fake_pkg.some_func, types.FunctionType)
    assert isinstance(fake_pkg.some_func, types.FunctionType)

    # Ensure imports from submodule still work
    from lazy_loader.tests.fake_pkg.some_func import some_func

    assert isinstance(some_func, types.FunctionType)


FAKE_STUB = """
from . import rank
from ._gaussian import gaussian
from .edges import sobel, scharr, prewitt, roberts
"""


def test_stub_loading(tmp_path):
    stub = tmp_path / "stub.pyi"
    stub.write_text(FAKE_STUB)
    _get, _dir, _all = lazy.attach_stub("my_module", str(stub))
    expect = {"gaussian", "sobel", "scharr", "prewitt", "roberts", "rank"}
    assert set(_dir()) == set(_all) == expect


def test_stub_loading_parity():
    from lazy_loader.tests import fake_pkg

    from_stub = lazy.attach_stub(fake_pkg.__name__, fake_pkg.__file__)
    stub_getter, stub_dir, stub_all = from_stub
    assert stub_all == fake_pkg.__all__
    assert stub_dir() == fake_pkg.__lazy_dir__()
    assert stub_getter("some_func") == fake_pkg.some_func


def test_stub_loading_errors(tmp_path):
    stub = tmp_path / "stub.pyi"
    stub.write_text("from ..mod import func\n")

    with pytest.raises(ValueError, match="Only within-module imports are supported"):
        lazy.attach_stub("name", str(stub))

    with pytest.raises(ValueError, match="Cannot load imports from non-existent stub"):
        lazy.attach_stub("name", "not a file")

    stub2 = tmp_path / "stub2.pyi"
    stub2.write_text("from .mod import *\n")
    with pytest.raises(ValueError, match=".*does not support star import"):
        lazy.attach_stub("name", str(stub2))


def test_require_kwarg():
    have_importlib_metadata = importlib.util.find_spec("importlib.metadata") is not None
    dot = "." if have_importlib_metadata else "_"
    # Test with a module that definitely exists, behavior hinges on requirement
    with mock.patch(f"importlib{dot}metadata.version") as version:
        version.return_value = "1.0.0"
        math = lazy.load("math", require="somepkg >= 2.0")
        assert isinstance(math, lazy.DelayedImportErrorModule)

        math = lazy.load("math", require="somepkg >= 1.0")
        assert math.sin(math.pi) == pytest.approx(0, 1e-6)

        # We can fail even after a successful import
        math = lazy.load("math", require="somepkg >= 2.0")
        assert isinstance(math, lazy.DelayedImportErrorModule)

    # When a module can be loaded but the version can't be checked,
    # raise a ValueError
    with pytest.raises(ValueError):
        lazy.load("math", require="somepkg >= 1.0")


def test_parallel_load():
    pytest.importorskip("numpy")

    subprocess.run(
        [
            sys.executable,
            os.path.join(os.path.dirname(__file__), "import_np_parallel.py"),
        ]
    )
