from __future__ import annotations

import os
import pathlib
from typing import TYPE_CHECKING

import pytest

import trio
from trio._file_io import AsyncIOWrapper

if TYPE_CHECKING:
    from collections.abc import Awaitable, Callable


@pytest.fixture
def path(tmp_path: pathlib.Path) -> trio.Path:
    return trio.Path(tmp_path / "test")


def method_pair(
    path: str,
    method_name: str,
) -> tuple[Callable[[], object], Callable[[], Awaitable[object]]]:
    sync_path = pathlib.Path(path)
    async_path = trio.Path(path)
    return getattr(sync_path, method_name), getattr(async_path, method_name)


@pytest.mark.skipif(os.name == "nt", reason="OS is not posix")
def test_instantiate_posix() -> None:
    assert isinstance(trio.Path(), trio.PosixPath)


@pytest.mark.skipif(os.name != "nt", reason="OS is not Windows")
def test_instantiate_windows() -> None:
    assert isinstance(trio.Path(), trio.WindowsPath)


async def test_open_is_async_context_manager(path: trio.Path) -> None:
    async with await path.open("w") as f:
        assert isinstance(f, AsyncIOWrapper)

    assert f.closed


def test_magic() -> None:
    path = trio.Path("test")

    assert str(path) == "test"
    assert bytes(path) == b"test"


EitherPathType = type[trio.Path] | type[pathlib.Path]
PathOrStrType = EitherPathType | type[str]
cls_pairs: list[tuple[EitherPathType, EitherPathType]] = [
    (trio.Path, pathlib.Path),
    (pathlib.Path, trio.Path),
    (trio.Path, trio.Path),
]


@pytest.mark.parametrize(("cls_a", "cls_b"), cls_pairs)
def test_cmp_magic(cls_a: EitherPathType, cls_b: EitherPathType) -> None:
    a, b = cls_a(""), cls_b("")
    assert a == b
    assert not a != b  # noqa: SIM202  # negate-not-equal-op

    a, b = cls_a("a"), cls_b("b")
    assert a < b
    assert b > a

    # this is intentionally testing equivalence with none, due to the
    # other=sentinel logic in _forward_magic
    assert not a == None  # noqa
    assert not b == None  # noqa


# upstream python3.8 bug: we should also test (pathlib.Path, trio.Path), but
# __*div__ does not properly raise NotImplementedError like the other comparison
# magic, so trio.Path's implementation does not get dispatched
cls_pairs_str: list[tuple[PathOrStrType, PathOrStrType]] = [
    (trio.Path, pathlib.Path),
    (trio.Path, trio.Path),
    (trio.Path, str),
    (str, trio.Path),
]


@pytest.mark.parametrize(("cls_a", "cls_b"), cls_pairs_str)
def test_div_magic(cls_a: PathOrStrType, cls_b: PathOrStrType) -> None:
    a, b = cls_a("a"), cls_b("b")

    result = a / b  # type: ignore[operator]
    # Type checkers think str / str could happen. Check each combo manually in type_tests/.
    assert isinstance(result, trio.Path)
    assert str(result) == os.path.join("a", "b")


@pytest.mark.parametrize(
    ("cls_a", "cls_b"),
    [(trio.Path, pathlib.Path), (trio.Path, trio.Path)],
)
@pytest.mark.parametrize("path", ["foo", "foo/bar/baz", "./foo"])
def test_hash_magic(
    cls_a: EitherPathType,
    cls_b: EitherPathType,
    path: str,
) -> None:
    a, b = cls_a(path), cls_b(path)
    assert hash(a) == hash(b)


def test_forwarded_properties(path: trio.Path) -> None:
    # use `name` as a representative of forwarded properties

    assert "name" in dir(path)
    assert path.name == "test"


def test_async_method_signature(path: trio.Path) -> None:
    # use `resolve` as a representative of wrapped methods

    assert path.resolve.__name__ == "resolve"
    assert path.resolve.__qualname__ == "Path.resolve"

    assert path.resolve.__doc__ is not None
    assert path.resolve.__qualname__ in path.resolve.__doc__


@pytest.mark.parametrize("method_name", ["is_dir", "is_file"])
async def test_compare_async_stat_methods(method_name: str) -> None:
    method, async_method = method_pair(".", method_name)

    result = method()
    async_result = await async_method()

    assert result == async_result


def test_invalid_name_not_wrapped(path: trio.Path) -> None:
    with pytest.raises(AttributeError):
        getattr(path, "invalid_fake_attr")  # noqa: B009  # "get-attr-with-constant"


@pytest.mark.parametrize("method_name", ["absolute", "resolve"])
async def test_async_methods_rewrap(method_name: str) -> None:
    method, async_method = method_pair(".", method_name)

    result = method()
    async_result = await async_method()

    assert isinstance(async_result, trio.Path)
    assert str(result) == str(async_result)


def test_forward_methods_rewrap(path: trio.Path, tmp_path: pathlib.Path) -> None:
    with_name = path.with_name("foo")
    with_suffix = path.with_suffix(".py")

    assert isinstance(with_name, trio.Path)
    assert with_name == tmp_path / "foo"
    assert isinstance(with_suffix, trio.Path)
    assert with_suffix == tmp_path / "test.py"


def test_forward_properties_rewrap(path: trio.Path) -> None:
    assert isinstance(path.parent, trio.Path)


def test_forward_methods_without_rewrap(path: trio.Path) -> None:
    assert "totally-unique-path" in str(path.joinpath("totally-unique-path"))


def test_repr() -> None:
    path = trio.Path(".")

    assert repr(path) == "trio.Path('.')"


@pytest.mark.parametrize("meth", [trio.Path.__init__, trio.Path.joinpath])
async def test_path_wraps_path(
    path: trio.Path,
    meth: Callable[[trio.Path, trio.Path], object],
) -> None:
    wrapped = await path.absolute()
    result = meth(path, wrapped)
    if result is None:
        result = path

    assert wrapped == result


def test_path_nonpath() -> None:
    with pytest.raises(TypeError):
        trio.Path(1)  # type: ignore


async def test_open_file_can_open_path(path: trio.Path) -> None:
    async with await trio.open_file(path, "w") as f:
        assert f.name == os.fspath(path)


async def test_globmethods(path: trio.Path) -> None:
    # Populate a directory tree
    await path.mkdir()
    await (path / "foo").mkdir()
    await (path / "foo" / "_bar.txt").write_bytes(b"")
    await (path / "bar.txt").write_bytes(b"")
    await (path / "bar.dat").write_bytes(b"")

    # Path.glob
    for pattern, results in {
        "*.txt": {"bar.txt"},
        "**/*.txt": {"_bar.txt", "bar.txt"},
    }.items():
        entries = set()
        for entry in await path.glob(pattern):
            assert isinstance(entry, trio.Path)
            entries.add(entry.name)

        assert entries == results

    # Path.rglob
    entries = set()
    for entry in await path.rglob("*.txt"):
        assert isinstance(entry, trio.Path)
        entries.add(entry.name)

    assert entries == {"_bar.txt", "bar.txt"}


async def test_as_uri(path: trio.Path) -> None:
    path = await path.parent.resolve()

    assert path.as_uri().startswith("file:///")


async def test_iterdir(path: trio.Path) -> None:
    # Populate a directory
    await path.mkdir()
    await (path / "foo").mkdir()
    await (path / "bar.txt").write_bytes(b"")

    entries = set()
    for entry in await path.iterdir():
        assert isinstance(entry, trio.Path)
        entries.add(entry.name)

    assert entries == {"bar.txt", "foo"}


async def test_classmethods() -> None:
    assert isinstance(await trio.Path.home(), trio.Path)

    # pathlib.Path has only two classmethods
    assert str(await trio.Path.home()) == os.path.expanduser("~")  # noqa: ASYNC240
    assert str(await trio.Path.cwd()) == os.getcwd()

    # Wrapped method has docstring
    assert trio.Path.home.__doc__


@pytest.mark.parametrize(
    "wrapper",
    [
        trio._path._wraps_async,
        trio._path._wrap_method,
        trio._path._wrap_method_path,
        trio._path._wrap_method_path_iterable,
    ],
)
def test_wrapping_without_docstrings(
    wrapper: Callable[[Callable[[], None]], Callable[[], None]],
) -> None:
    @wrapper
    def func_without_docstring() -> None: ...  # pragma: no cover

    assert func_without_docstring.__doc__ is None
