import os
import pathlib
import platform
import socket
import stat
from typing import Tuple

import pytest
from _pytest.tmpdir import TempPathFactory

from anyio import AsyncFile, Path, open_file, wrap_file

pytestmark = pytest.mark.anyio


class TestAsyncFile:
    @pytest.fixture(scope="class")
    def testdata(cls) -> bytes:
        return b"".join(bytes([i] * 1000) for i in range(10))

    @pytest.fixture
    def testdatafile(
        self, tmp_path_factory: TempPathFactory, testdata: bytes
    ) -> pathlib.Path:
        file = tmp_path_factory.mktemp("file").joinpath("testdata")
        file.write_bytes(testdata)
        return file

    async def test_open_close(self, testdatafile: pathlib.Path) -> None:
        f = await open_file(testdatafile)
        await f.aclose()

    async def test_read(self, testdatafile: pathlib.Path, testdata: bytes) -> None:
        async with await open_file(testdatafile, "rb") as f:
            data = await f.read()

        assert f.closed
        assert data == testdata

    async def test_write(self, testdatafile: pathlib.Path, testdata: bytes) -> None:
        async with await open_file(testdatafile, "ab") as f:
            await f.write(b"f" * 1000)

        assert testdatafile.stat().st_size == len(testdata) + 1000

    async def test_async_iteration(self, tmp_path: pathlib.Path) -> None:
        lines = ["blah blah\n", "foo foo\n", "bar bar"]
        testpath = tmp_path.joinpath("testfile")
        testpath.write_text("".join(lines), "ascii")
        async with await open_file(str(testpath)) as f:
            lines_i = iter(lines)
            async for line in f:
                assert line == next(lines_i)

    async def test_wrap_file(self, tmp_path: pathlib.Path) -> None:
        path = tmp_path / "testdata"
        with path.open("w") as fp:
            wrapped = wrap_file(fp)
            await wrapped.write("dummydata")

        assert path.read_text() == "dummydata"


class TestPath:
    @pytest.fixture
    def populated_tmpdir(self, tmp_path: pathlib.Path) -> pathlib.Path:
        tmp_path.joinpath("testfile").touch()
        tmp_path.joinpath("testfile2").touch()
        subdir = tmp_path / "subdir"
        subdir.mkdir()
        subdir.joinpath("dummyfile1.txt").touch()
        subdir.joinpath("dummyfile2.txt").touch()
        return tmp_path

    async def test_properties(self) -> None:
        """Ensure that all public properties and methods are available on the async Path class."""
        path = pathlib.Path("/test/path/another/part")
        stdlib_properties = {
            p for p in dir(path) if p.startswith("__") or not p.startswith("_")
        }
        stdlib_properties.discard("link_to")
        stdlib_properties.discard("__class_getitem__")
        stdlib_properties.discard("__enter__")
        stdlib_properties.discard("__exit__")

        async_path = Path(path)
        anyio_properties = {
            p for p in dir(async_path) if p.startswith("__") or not p.startswith("_")
        }

        missing = stdlib_properties - anyio_properties
        assert not missing

    def test_repr(self) -> None:
        assert repr(Path("/foo")) == "Path('/foo')"

    def test_bytes(self) -> None:
        assert bytes(Path("/foo-åäö")) == os.fsencode(f"{os.path.sep}foo-åäö")

    def test_hash(self) -> None:
        assert hash(Path("/foo")) == hash(pathlib.Path("/foo"))

    def test_comparison(self) -> None:
        path1 = Path("/foo1")
        path2 = Path("/foo2")
        assert path1 < path2
        assert path1 <= path2
        assert path2 > path1
        assert path2 >= path1

    def test_truediv(self) -> None:
        result = Path("/foo") / "bar"
        assert isinstance(result, Path)
        assert result == pathlib.Path("/foo/bar")

    def test_rtruediv(self) -> None:
        result = "/foo" / Path("bar")
        assert isinstance(result, Path)
        assert result == pathlib.Path("/foo/bar")

    def test_parts_property(self) -> None:
        assert Path("/abc/xyz/foo.txt").parts == (os.path.sep, "abc", "xyz", "foo.txt")

    @pytest.mark.skipif(
        platform.system() != "Windows", reason="Drive only makes sense on Windows"
    )
    def test_drive_property(self) -> None:
        assert Path("c:\\abc\\xyz").drive == "c:"

    def test_root_property(self) -> None:
        assert Path("/abc/xyz/foo.txt").root == os.path.sep

    def test_anchor_property(self) -> None:
        assert Path("/abc/xyz/foo.txt.zip").anchor == os.path.sep

    def test_parents_property(self) -> None:
        parents = Path("/abc/xyz/foo.txt").parents
        assert len(parents) == 3
        assert all(isinstance(parent, Path) for parent in parents)
        assert str(parents[0]) == f"{os.path.sep}abc{os.path.sep}xyz"
        assert str(parents[1]) == f"{os.path.sep}abc"
        assert str(parents[2]) == os.path.sep

    def test_parent_property(self) -> None:
        parent = Path("/abc/xyz/foo.txt").parent
        assert isinstance(parent, Path)
        assert str(parent) == f"{os.path.sep}abc{os.path.sep}xyz"

    def test_name_property(self) -> None:
        assert Path("/abc/xyz/foo.txt.zip").name == "foo.txt.zip"

    def test_suffix_property(self) -> None:
        assert Path("/abc/xyz/foo.txt.zip").suffix == ".zip"

    def test_suffixes_property(self) -> None:
        assert Path("/abc/xyz/foo.tar.gz").suffixes == [".tar", ".gz"]

    def test_stem_property(self) -> None:
        assert Path("/abc/xyz/foo.txt.zip").stem == "foo.txt"

    async def test_absolute(self) -> None:
        result = await Path("../foo/bar").absolute()
        assert isinstance(result, Path)
        assert result == pathlib.Path.cwd() / "../foo/bar"

    @pytest.mark.skipif(
        platform.system() != "Windows", reason="Only makes sense on Windows"
    )
    def test_as_posix(self) -> None:
        assert Path("c:\\foo\\bar").as_posix() == "c:/foo/bar"

    def test_as_uri(self) -> None:
        if platform.system() == "Windows":
            assert Path("c:\\foo\\bar").as_uri() == "file:///c:/foo/bar"
        else:
            assert Path("/foo/bar").as_uri() == "file:///foo/bar"

    async def test_cwd(self) -> None:
        result = await Path.cwd()
        assert isinstance(result, Path)
        assert result == pathlib.Path.cwd()

    async def test_exists(self, tmp_path: pathlib.Path) -> None:
        assert not await Path("~/btelkbee").exists()
        assert await Path(tmp_path).exists()

    async def test_expanduser(self) -> None:
        result = await Path("~/btelkbee").expanduser()
        assert isinstance(result, Path)
        assert str(result) == os.path.expanduser(f"~{os.path.sep}btelkbee")

    async def test_home(self) -> None:
        result = await Path.home()
        assert isinstance(result, Path)
        assert result == pathlib.Path.home()

    @pytest.mark.parametrize(
        "arg, result",
        [
            ("c:/xyz" if platform.system() == "Windows" else "/xyz", True),
            ("../xyz", False),
        ],
    )
    def test_is_absolute(self, arg: str, result: bool) -> None:
        assert Path(arg).is_absolute() == result

    @pytest.mark.skipif(
        platform.system() == "Windows",
        reason="Block devices are not available on Windows",
    )
    async def test_is_block_device(self) -> None:
        assert not await Path("/btelkbee").is_block_device()
        with os.scandir("/dev") as iterator:
            for entry in iterator:
                if stat.S_ISBLK(entry.stat().st_mode):
                    assert await Path(entry.path).is_block_device()
                    break
            else:
                pytest.skip("Could not find a suitable block device")

    @pytest.mark.skipif(
        platform.system() == "Windows",
        reason="Character devices are not available on Windows",
    )
    async def test_is_char_device(self) -> None:
        assert not await Path("/btelkbee").is_char_device()
        assert await Path("/dev/random").is_char_device()

    async def test_is_dir(self, tmp_path: pathlib.Path) -> None:
        path = tmp_path / "somedir"
        assert not await Path(path).is_dir()
        path.mkdir()
        assert await Path(path).is_dir()

    @pytest.mark.skipif(
        platform.system() == "Windows", reason="mkfifo() is not available on Windows"
    )
    async def test_is_fifo(self, tmp_path: pathlib.Path) -> None:
        path = tmp_path / "somefifo"
        assert not await Path(path).is_fifo()
        os.mkfifo(path)
        assert await Path(path).is_fifo()

    async def test_is_file(self, tmp_path: pathlib.Path) -> None:
        path = tmp_path / "somefile"
        assert not await Path(path).is_file()
        path.touch()
        assert await Path(path).is_file()

    async def test_is_mount(self) -> None:
        assert not await Path("/gfobj4ewiotj").is_mount()
        assert await Path("/").is_mount()

    def test_is_reserved(self) -> None:
        expected_result = platform.system() == "Windows"
        assert Path("nul").is_reserved() == expected_result

    @pytest.mark.skipif(
        platform.system() == "Windows",
        reason="UNIX sockets are not available on Windows",
    )
    async def test_is_socket(self, tmp_path_factory: TempPathFactory) -> None:
        path = tmp_path_factory.mktemp("unix").joinpath("socket")
        assert not await Path(path).is_socket()
        with socket.socket(socket.AF_UNIX) as sock:
            sock.bind(str(path))
            assert await Path(path).is_socket()

    @pytest.mark.skipif(
        platform.system() == "Windows",
        reason="symbolic links are not supported on Windows",
    )
    async def test_is_symlink(self, tmp_path: pathlib.Path) -> None:
        path = tmp_path / "testfile"
        assert not await Path(path).is_symlink()
        path.symlink_to("/foo")
        assert await Path(path).is_symlink()

    @pytest.mark.parametrize(
        "args, result", [(("/xyz", "abc"), True), (("/xyz", "baz"), False)]
    )
    def test_is_relative_to(self, args: Tuple[str], result: bool) -> None:
        assert Path("/xyz/abc/foo").is_relative_to(*args) == result

    async def test_glob(self, populated_tmpdir: pathlib.Path) -> None:
        all_paths = []
        async for path in Path(populated_tmpdir).glob("**/*.txt"):
            assert isinstance(path, Path)
            all_paths.append(path.name)

        all_paths.sort()
        assert all_paths == ["dummyfile1.txt", "dummyfile2.txt"]

    async def test_rglob(self, populated_tmpdir: pathlib.Path) -> None:
        all_paths = []
        async for path in Path(populated_tmpdir).rglob("*.txt"):
            assert isinstance(path, Path)
            all_paths.append(path.name)

        all_paths.sort()
        assert all_paths == ["dummyfile1.txt", "dummyfile2.txt"]

    async def test_iterdir(self, populated_tmpdir: pathlib.Path) -> None:
        all_paths = []
        async for path in Path(populated_tmpdir).iterdir():
            assert isinstance(path, Path)
            all_paths.append(path.name)

        all_paths.sort()
        assert all_paths == ["subdir", "testfile", "testfile2"]

    def test_joinpath(self) -> None:
        path = Path("/foo").joinpath("bar")
        assert path == Path("/foo/bar")

    def test_match(self) -> None:
        assert Path("/foo/bar").match("/foo/*")
        assert not Path("/foo/bar").match("/baz/*")

    @pytest.mark.skipif(
        platform.system() == "Windows", reason="chmod() is not available on Windows"
    )
    async def test_chmod(self, tmp_path: pathlib.Path) -> None:
        path = tmp_path / "testfile"
        path.touch(0o666)
        await Path(path).chmod(0o444)
        assert path.stat().st_mode & 0o777 == 0o444

    @pytest.mark.skipif(
        platform.system() == "Windows", reason="hard links are not supported on Windows"
    )
    async def test_hardlink_to(self, tmp_path: pathlib.Path) -> None:
        path = tmp_path / "testfile"
        target = tmp_path / "link"
        target.touch()
        await Path(path).hardlink_to(Path(target))
        assert path.stat().st_nlink == 2
        assert target.stat().st_nlink == 2

    @pytest.mark.skipif(
        not hasattr(os, "lchmod"), reason="os.lchmod() is not available"
    )
    async def test_lchmod(self, tmp_path: pathlib.Path) -> None:
        path = tmp_path / "testfile"
        path.symlink_to("/foo/bar/baz")
        await Path(path).lchmod(0o600)
        assert path.lstat().st_mode & 0o777 == 0o600

    @pytest.mark.skipif(
        platform.system() == "Windows",
        reason="symbolic links are not supported on Windows",
    )
    async def test_lstat(self, tmp_path: pathlib.Path) -> None:
        path = tmp_path.joinpath("testfile")
        path.symlink_to("/foo/bar/baz")
        result = await Path(path).lstat()
        assert isinstance(result, os.stat_result)

    @pytest.mark.skipif(
        platform.system() == "Windows",
        reason="owner and group are not supported on Windows",
    )
    async def test_group(self, tmp_path: pathlib.Path) -> None:
        import grp

        group_name = grp.getgrgid(os.getegid()).gr_name
        assert await Path(tmp_path).group() == group_name

    async def test_mkdir(self, tmp_path: pathlib.Path) -> None:
        path = tmp_path / "testdir"
        await Path(path).mkdir()
        assert path.is_dir()

    async def test_open(self, tmp_path: pathlib.Path) -> None:
        path = tmp_path / "testfile"
        path.write_bytes(b"bibbitibobbitiboo")
        fp = await Path(path).open("rb")
        assert isinstance(fp, AsyncFile)
        assert fp.name == str(path)
        await fp.aclose()

    @pytest.mark.skipif(
        platform.system() == "Windows",
        reason="owner and group are not supported on Windows",
    )
    async def test_owner(self, tmp_path: pathlib.Path) -> None:
        import pwd

        user_name = pwd.getpwuid(os.geteuid()).pw_name
        assert await Path(tmp_path).owner() == user_name

    @pytest.mark.skipif(
        platform.system() == "Windows",
        reason="symbolic links are not supported on Windows",
    )
    async def test_readlink(self, tmp_path: pathlib.Path) -> None:
        path = tmp_path.joinpath("testfile")
        path.symlink_to("/foo/bar/baz")
        link_target = await Path(path).readlink()
        assert isinstance(link_target, Path)
        assert str(link_target) == "/foo/bar/baz"

    async def test_read_bytes(self, tmp_path: pathlib.Path) -> None:
        path = tmp_path / "testfile"
        path.write_bytes(b"bibbitibobbitiboo")
        assert await Path(path).read_bytes() == b"bibbitibobbitiboo"

    async def test_read_text(self, tmp_path: pathlib.Path) -> None:
        path = tmp_path / "testfile"
        path.write_text("some text åäö", encoding="utf-8")
        assert await Path(path).read_text(encoding="utf-8") == "some text åäö"

    async def test_rename(self, tmp_path: pathlib.Path) -> None:
        path = tmp_path / "somefile"
        path.touch()
        target = tmp_path / "anotherfile"
        result = await Path(path).rename(Path(target))
        assert isinstance(result, Path)
        assert result == target

    async def test_replace(self, tmp_path: pathlib.Path) -> None:
        path = tmp_path / "somefile"
        path.write_text("hello")
        target = tmp_path / "anotherfile"
        target.write_text("world")
        result = await Path(path).replace(Path(target))
        assert isinstance(result, Path)
        assert result == target
        assert target.read_text() == "hello"

    async def test_resolve(self, tmp_path: pathlib.Path) -> None:
        path = tmp_path / "somedir" / ".." / "somefile"
        result = await Path(path).resolve()
        assert result == tmp_path / "somefile"

    async def test_rmdir(self, tmp_path: pathlib.Path) -> None:
        path = tmp_path / "somedir"
        path.mkdir()
        await Path(path).rmdir()
        assert not path.exists()

    async def test_samefile(self, tmp_path: pathlib.Path) -> None:
        path = tmp_path / "somefile"
        path.touch()
        assert await Path(tmp_path / "somefile").samefile(Path(path))

    async def test_stat(self, tmp_path: pathlib.Path) -> None:
        result = await Path(tmp_path).stat()
        assert isinstance(result, os.stat_result)

    async def test_touch(self, tmp_path: pathlib.Path) -> None:
        path = tmp_path / "testfile"
        await Path(path).touch()
        assert path.is_file()

    @pytest.mark.skipif(
        platform.system() == "Windows",
        reason="symbolic links are not supported on Windows",
    )
    async def test_symlink_to(self, tmp_path: pathlib.Path) -> None:
        path = tmp_path / "testfile"
        target = tmp_path / "link"
        await Path(path).symlink_to(Path(target))
        assert path.is_symlink()

    async def test_unlink(self, tmp_path: pathlib.Path) -> None:
        path = tmp_path / "testfile"
        path.touch()
        await Path(path).unlink()
        assert not path.exists()

    async def test_unlink_missing_file(self, tmp_path: pathlib.Path) -> None:
        path = tmp_path / "testfile"
        await Path(path).unlink(missing_ok=True)
        with pytest.raises(FileNotFoundError):
            await Path(path).unlink(missing_ok=False)

    def test_with_name(self) -> None:
        assert Path("/xyz/foo.txt").with_name("bar").name == "bar"

    def test_with_stem(self) -> None:
        assert Path("/xyz/foo.txt").with_stem("bar").name == "bar.txt"

    def test_with_suffix(self) -> None:
        assert Path("/xyz/foo.txt.gz").with_suffix(".zip").name == "foo.txt.zip"

    async def test_write_bytes(self, tmp_path: pathlib.Path) -> None:
        path = tmp_path / "testfile"
        await Path(path).write_bytes(b"bibbitibobbitiboo")
        assert path.read_bytes() == b"bibbitibobbitiboo"

    async def test_write_text(self, tmp_path: pathlib.Path) -> None:
        path = tmp_path / "testfile"
        await Path(path).write_text("some text åäö", encoding="utf-8")
        assert path.read_text(encoding="utf-8") == "some text åäö"
