import os.path
import sys
import unittest.mock
from pathlib import Path
from textwrap import dedent

import py

import pytest
from _pytest.pathlib import bestrelpath
from _pytest.pathlib import commonpath
from _pytest.pathlib import ensure_deletable
from _pytest.pathlib import fnmatch_ex
from _pytest.pathlib import get_extended_length_path_str
from _pytest.pathlib import get_lock_path
from _pytest.pathlib import import_path
from _pytest.pathlib import ImportPathMismatchError
from _pytest.pathlib import maybe_delete_a_numbered_dir
from _pytest.pathlib import resolve_package_path


class TestFNMatcherPort:
    """Test that our port of py.common.FNMatcher (fnmatch_ex) produces the
    same results as the original py.path.local.fnmatch method."""

    @pytest.fixture(params=["pathlib", "py.path"])
    def match(self, request):
        if request.param == "py.path":

            def match_(pattern, path):
                return py.path.local(path).fnmatch(pattern)

        else:
            assert request.param == "pathlib"

            def match_(pattern, path):
                return fnmatch_ex(pattern, path)

        return match_

    if sys.platform == "win32":
        drv1 = "c:"
        drv2 = "d:"
    else:
        drv1 = "/c"
        drv2 = "/d"

    @pytest.mark.parametrize(
        "pattern, path",
        [
            ("*.py", "foo.py"),
            ("*.py", "bar/foo.py"),
            ("test_*.py", "foo/test_foo.py"),
            ("tests/*.py", "tests/foo.py"),
            (drv1 + "/*.py", drv1 + "/foo.py"),
            (drv1 + "/foo/*.py", drv1 + "/foo/foo.py"),
            ("tests/**/test*.py", "tests/foo/test_foo.py"),
            ("tests/**/doc/test*.py", "tests/foo/bar/doc/test_foo.py"),
            ("tests/**/doc/**/test*.py", "tests/foo/doc/bar/test_foo.py"),
        ],
    )
    def test_matching(self, match, pattern, path):
        assert match(pattern, path)

    def test_matching_abspath(self, match):
        abspath = os.path.abspath(os.path.join("tests/foo.py"))
        assert match("tests/foo.py", abspath)

    @pytest.mark.parametrize(
        "pattern, path",
        [
            ("*.py", "foo.pyc"),
            ("*.py", "foo/foo.pyc"),
            ("tests/*.py", "foo/foo.py"),
            (drv1 + "/*.py", drv2 + "/foo.py"),
            (drv1 + "/foo/*.py", drv2 + "/foo/foo.py"),
            ("tests/**/test*.py", "tests/foo.py"),
            ("tests/**/test*.py", "foo/test_foo.py"),
            ("tests/**/doc/test*.py", "tests/foo/bar/doc/foo.py"),
            ("tests/**/doc/test*.py", "tests/foo/bar/test_foo.py"),
        ],
    )
    def test_not_matching(self, match, pattern, path):
        assert not match(pattern, path)


class TestImportPath:
    """

    Most of the tests here were copied from py lib's tests for "py.local.path.pyimport".

    Having our own pyimport-like function is inline with removing py.path dependency in the future.
    """

    @pytest.fixture(scope="session")
    def path1(self, tmpdir_factory):
        path = tmpdir_factory.mktemp("path")
        self.setuptestfs(path)
        yield path
        assert path.join("samplefile").check()

    def setuptestfs(self, path):
        # print "setting up test fs for", repr(path)
        samplefile = path.ensure("samplefile")
        samplefile.write("samplefile\n")

        execfile = path.ensure("execfile")
        execfile.write("x=42")

        execfilepy = path.ensure("execfile.py")
        execfilepy.write("x=42")

        d = {1: 2, "hello": "world", "answer": 42}
        path.ensure("samplepickle").dump(d)

        sampledir = path.ensure("sampledir", dir=1)
        sampledir.ensure("otherfile")

        otherdir = path.ensure("otherdir", dir=1)
        otherdir.ensure("__init__.py")

        module_a = otherdir.ensure("a.py")
        module_a.write("from .b import stuff as result\n")
        module_b = otherdir.ensure("b.py")
        module_b.write('stuff="got it"\n')
        module_c = otherdir.ensure("c.py")
        module_c.write(
            dedent(
                """
            import py;
            import otherdir.a
            value = otherdir.a.result
        """
            )
        )
        module_d = otherdir.ensure("d.py")
        module_d.write(
            dedent(
                """
            import py;
            from otherdir import a
            value2 = a.result
        """
            )
        )

    def test_smoke_test(self, path1):
        obj = import_path(path1.join("execfile.py"))
        assert obj.x == 42  # type: ignore[attr-defined]
        assert obj.__name__ == "execfile"

    def test_renamed_dir_creates_mismatch(self, tmpdir, monkeypatch):
        p = tmpdir.ensure("a", "test_x123.py")
        import_path(p)
        tmpdir.join("a").move(tmpdir.join("b"))
        with pytest.raises(ImportPathMismatchError):
            import_path(tmpdir.join("b", "test_x123.py"))

        # Errors can be ignored.
        monkeypatch.setenv("PY_IGNORE_IMPORTMISMATCH", "1")
        import_path(tmpdir.join("b", "test_x123.py"))

        # PY_IGNORE_IMPORTMISMATCH=0 does not ignore error.
        monkeypatch.setenv("PY_IGNORE_IMPORTMISMATCH", "0")
        with pytest.raises(ImportPathMismatchError):
            import_path(tmpdir.join("b", "test_x123.py"))

    def test_messy_name(self, tmpdir):
        # http://bitbucket.org/hpk42/py-trunk/issue/129
        path = tmpdir.ensure("foo__init__.py")
        module = import_path(path)
        assert module.__name__ == "foo__init__"

    def test_dir(self, tmpdir):
        p = tmpdir.join("hello_123")
        p_init = p.ensure("__init__.py")
        m = import_path(p)
        assert m.__name__ == "hello_123"
        m = import_path(p_init)
        assert m.__name__ == "hello_123"

    def test_a(self, path1):
        otherdir = path1.join("otherdir")
        mod = import_path(otherdir.join("a.py"))
        assert mod.result == "got it"  # type: ignore[attr-defined]
        assert mod.__name__ == "otherdir.a"

    def test_b(self, path1):
        otherdir = path1.join("otherdir")
        mod = import_path(otherdir.join("b.py"))
        assert mod.stuff == "got it"  # type: ignore[attr-defined]
        assert mod.__name__ == "otherdir.b"

    def test_c(self, path1):
        otherdir = path1.join("otherdir")
        mod = import_path(otherdir.join("c.py"))
        assert mod.value == "got it"  # type: ignore[attr-defined]

    def test_d(self, path1):
        otherdir = path1.join("otherdir")
        mod = import_path(otherdir.join("d.py"))
        assert mod.value2 == "got it"  # type: ignore[attr-defined]

    def test_import_after(self, tmpdir):
        tmpdir.ensure("xxxpackage", "__init__.py")
        mod1path = tmpdir.ensure("xxxpackage", "module1.py")
        mod1 = import_path(mod1path)
        assert mod1.__name__ == "xxxpackage.module1"
        from xxxpackage import module1

        assert module1 is mod1

    def test_check_filepath_consistency(self, monkeypatch, tmpdir):
        name = "pointsback123"
        ModuleType = type(os)
        p = tmpdir.ensure(name + ".py")
        for ending in (".pyc", ".pyo"):
            mod = ModuleType(name)
            pseudopath = tmpdir.ensure(name + ending)
            mod.__file__ = str(pseudopath)
            monkeypatch.setitem(sys.modules, name, mod)
            newmod = import_path(p)
            assert mod == newmod
        monkeypatch.undo()
        mod = ModuleType(name)
        pseudopath = tmpdir.ensure(name + "123.py")
        mod.__file__ = str(pseudopath)
        monkeypatch.setitem(sys.modules, name, mod)
        with pytest.raises(ImportPathMismatchError) as excinfo:
            import_path(p)
        modname, modfile, orig = excinfo.value.args
        assert modname == name
        assert modfile == pseudopath
        assert orig == p
        assert issubclass(ImportPathMismatchError, ImportError)

    def test_issue131_on__init__(self, tmpdir):
        # __init__.py files may be namespace packages, and thus the
        # __file__ of an imported module may not be ourselves
        # see issue
        p1 = tmpdir.ensure("proja", "__init__.py")
        p2 = tmpdir.ensure("sub", "proja", "__init__.py")
        m1 = import_path(p1)
        m2 = import_path(p2)
        assert m1 == m2

    def test_ensuresyspath_append(self, tmpdir):
        root1 = tmpdir.mkdir("root1")
        file1 = root1.ensure("x123.py")
        assert str(root1) not in sys.path
        import_path(file1, mode="append")
        assert str(root1) == sys.path[-1]
        assert str(root1) not in sys.path[:-1]

    def test_invalid_path(self, tmpdir):
        with pytest.raises(ImportError):
            import_path(tmpdir.join("invalid.py"))

    @pytest.fixture
    def simple_module(self, tmpdir):
        fn = tmpdir.join("mymod.py")
        fn.write(
            dedent(
                """
            def foo(x): return 40 + x
            """
            )
        )
        return fn

    def test_importmode_importlib(self, simple_module):
        """`importlib` mode does not change sys.path."""
        module = import_path(simple_module, mode="importlib")
        assert module.foo(2) == 42  # type: ignore[attr-defined]
        assert simple_module.dirname not in sys.path

    def test_importmode_twice_is_different_module(self, simple_module):
        """`importlib` mode always returns a new module."""
        module1 = import_path(simple_module, mode="importlib")
        module2 = import_path(simple_module, mode="importlib")
        assert module1 is not module2

    def test_no_meta_path_found(self, simple_module, monkeypatch):
        """Even without any meta_path should still import module."""
        monkeypatch.setattr(sys, "meta_path", [])
        module = import_path(simple_module, mode="importlib")
        assert module.foo(2) == 42  # type: ignore[attr-defined]

        # mode='importlib' fails if no spec is found to load the module
        import importlib.util

        monkeypatch.setattr(
            importlib.util, "spec_from_file_location", lambda *args: None
        )
        with pytest.raises(ImportError):
            import_path(simple_module, mode="importlib")


def test_resolve_package_path(tmp_path):
    pkg = tmp_path / "pkg1"
    pkg.mkdir()
    (pkg / "__init__.py").touch()
    (pkg / "subdir").mkdir()
    (pkg / "subdir/__init__.py").touch()
    assert resolve_package_path(pkg) == pkg
    assert resolve_package_path(pkg.joinpath("subdir", "__init__.py")) == pkg


def test_package_unimportable(tmp_path):
    pkg = tmp_path / "pkg1-1"
    pkg.mkdir()
    pkg.joinpath("__init__.py").touch()
    subdir = pkg.joinpath("subdir")
    subdir.mkdir()
    pkg.joinpath("subdir/__init__.py").touch()
    assert resolve_package_path(subdir) == subdir
    xyz = subdir.joinpath("xyz.py")
    xyz.touch()
    assert resolve_package_path(xyz) == subdir
    assert not resolve_package_path(pkg)


def test_access_denied_during_cleanup(tmp_path, monkeypatch):
    """Ensure that deleting a numbered dir does not fail because of OSErrors (#4262)."""
    path = tmp_path / "temp-1"
    path.mkdir()

    def renamed_failed(*args):
        raise OSError("access denied")

    monkeypatch.setattr(Path, "rename", renamed_failed)

    lock_path = get_lock_path(path)
    maybe_delete_a_numbered_dir(path)
    assert not lock_path.is_file()


def test_long_path_during_cleanup(tmp_path):
    """Ensure that deleting long path works (particularly on Windows (#6775))."""
    path = (tmp_path / ("a" * 250)).resolve()
    if sys.platform == "win32":
        # make sure that the full path is > 260 characters without any
        # component being over 260 characters
        assert len(str(path)) > 260
        extended_path = "\\\\?\\" + str(path)
    else:
        extended_path = str(path)
    os.mkdir(extended_path)
    assert os.path.isdir(extended_path)
    maybe_delete_a_numbered_dir(path)
    assert not os.path.isdir(extended_path)


def test_get_extended_length_path_str():
    assert get_extended_length_path_str(r"c:\foo") == r"\\?\c:\foo"
    assert get_extended_length_path_str(r"\\share\foo") == r"\\?\UNC\share\foo"
    assert get_extended_length_path_str(r"\\?\UNC\share\foo") == r"\\?\UNC\share\foo"
    assert get_extended_length_path_str(r"\\?\c:\foo") == r"\\?\c:\foo"


def test_suppress_error_removing_lock(tmp_path):
    """ensure_deletable should be resilient if lock file cannot be removed (#5456, #7491)"""
    path = tmp_path / "dir"
    path.mkdir()
    lock = get_lock_path(path)
    lock.touch()
    mtime = lock.stat().st_mtime

    with unittest.mock.patch.object(Path, "unlink", side_effect=OSError) as m:
        assert not ensure_deletable(
            path, consider_lock_dead_if_created_before=mtime + 30
        )
        assert m.call_count == 1
    assert lock.is_file()

    with unittest.mock.patch.object(Path, "is_file", side_effect=OSError) as m:
        assert not ensure_deletable(
            path, consider_lock_dead_if_created_before=mtime + 30
        )
        assert m.call_count == 1
    assert lock.is_file()

    # check now that we can remove the lock file in normal circumstances
    assert ensure_deletable(path, consider_lock_dead_if_created_before=mtime + 30)
    assert not lock.is_file()


def test_bestrelpath() -> None:
    curdir = Path("/foo/bar/baz/path")
    assert bestrelpath(curdir, curdir) == "."
    assert bestrelpath(curdir, curdir / "hello" / "world") == "hello" + os.sep + "world"
    assert bestrelpath(curdir, curdir.parent / "sister") == ".." + os.sep + "sister"
    assert bestrelpath(curdir, curdir.parent) == ".."
    assert bestrelpath(curdir, Path("hello")) == "hello"


def test_commonpath() -> None:
    path = Path("/foo/bar/baz/path")
    subpath = path / "sampledir"
    assert commonpath(path, subpath) == path
    assert commonpath(subpath, path) == path
    assert commonpath(Path(str(path) + "suffix"), path) == path.parent
    assert commonpath(path, path.parent.parent) == path.parent.parent
