"""
Tests for pathlib.types._ReadablePath
"""

import collections.abc
import io
import unittest

from pathlib import Path
from pathlib.types import PathInfo, _ReadablePath
from pathlib._os import magic_open

from test.test_pathlib.support.local_path import ReadableLocalPath, LocalPathGround
from test.test_pathlib.support.zip_path import ReadableZipPath, ZipPathGround


class ReadTestBase:
    def setUp(self):
        self.root = self.ground.setup()
        self.ground.create_hierarchy(self.root)

    def tearDown(self):
        self.ground.teardown(self.root)

    def test_is_readable(self):
        self.assertIsInstance(self.root, _ReadablePath)

    def test_open_r(self):
        p = self.root / 'fileA'
        with magic_open(p, 'r') as f:
            self.assertIsInstance(f, io.TextIOBase)
            self.assertEqual(f.read(), 'this is file A\n')

    def test_open_rb(self):
        p = self.root / 'fileA'
        with magic_open(p, 'rb') as f:
            self.assertEqual(f.read(), b'this is file A\n')

    def test_read_bytes(self):
        p = self.root / 'fileA'
        self.assertEqual(p.read_bytes(), b'this is file A\n')

    def test_read_text(self):
        p = self.root / 'fileA'
        self.assertEqual(p.read_text(), 'this is file A\n')
        q = self.root / 'abc'
        self.ground.create_file(q, b'\xe4bcdefg')
        self.assertEqual(q.read_text(encoding='latin-1'), 'äbcdefg')
        self.assertEqual(q.read_text(encoding='utf-8', errors='ignore'), 'bcdefg')

    def test_read_text_with_newlines(self):
        p = self.root / 'abc'
        self.ground.create_file(p, b'abcde\r\nfghlk\n\rmnopq')
        # Check that `\n` character change nothing
        self.assertEqual(p.read_text(newline='\n'), 'abcde\r\nfghlk\n\rmnopq')
        # Check that `\r` character replaces `\n`
        self.assertEqual(p.read_text(newline='\r'), 'abcde\r\nfghlk\n\rmnopq')
        # Check that `\r\n` character replaces `\n`
        self.assertEqual(p.read_text(newline='\r\n'), 'abcde\r\nfghlk\n\rmnopq')

    def test_iterdir(self):
        expected = ['dirA', 'dirB', 'dirC', 'fileA']
        if self.ground.can_symlink:
            expected += ['linkA', 'linkB', 'brokenLink', 'brokenLinkLoop']
        expected = {self.root.joinpath(name) for name in expected}
        actual = set(self.root.iterdir())
        self.assertEqual(actual, expected)

    def test_iterdir_nodir(self):
        p = self.root / 'fileA'
        self.assertRaises(OSError, p.iterdir)

    def test_iterdir_info(self):
        for child in self.root.iterdir():
            self.assertIsInstance(child.info, PathInfo)
            self.assertTrue(child.info.exists(follow_symlinks=False))

    def test_glob(self):
        if not self.ground.can_symlink:
            self.skipTest("requires symlinks")

        p = self.root
        sep = self.root.parser.sep
        altsep = self.root.parser.altsep
        def check(pattern, expected):
            if altsep:
                expected = {name.replace(altsep, sep) for name in expected}
            expected = {p.joinpath(name) for name in expected}
            actual = set(p.glob(pattern, recurse_symlinks=True))
            self.assertEqual(actual, expected)

        it = p.glob("fileA")
        self.assertIsInstance(it, collections.abc.Iterator)
        self.assertEqual(list(it), [p.joinpath("fileA")])
        check("*A", ["dirA", "fileA", "linkA"])
        check("*A", ['dirA', 'fileA', 'linkA'])
        check("*B/*", ["dirB/fileB", "linkB/fileB"])
        check("*B/*", ['dirB/fileB', 'linkB/fileB'])
        check("brokenLink", ['brokenLink'])
        check("brokenLinkLoop", ['brokenLinkLoop'])
        check("**/", ["", "dirA/", "dirA/linkC/", "dirB/", "dirC/", "dirC/dirD/", "linkB/"])
        check("**/*/", ["dirA/", "dirA/linkC/", "dirB/", "dirC/", "dirC/dirD/", "linkB/"])
        check("*/", ["dirA/", "dirB/", "dirC/", "linkB/"])
        check("*/dirD/**/", ["dirC/dirD/"])
        check("*/dirD/**", ["dirC/dirD/", "dirC/dirD/fileD"])
        check("dir*/**", ["dirA/", "dirA/linkC", "dirA/linkC/fileB", "dirB/", "dirB/fileB", "dirC/",
                          "dirC/fileC", "dirC/dirD", "dirC/dirD/fileD", "dirC/novel.txt"])
        check("dir*/**/", ["dirA/", "dirA/linkC/", "dirB/", "dirC/", "dirC/dirD/"])
        check("dir*/**/..", ["dirA/..", "dirA/linkC/..", "dirB/..", "dirC/..", "dirC/dirD/.."])
        check("dir*/*/**", ["dirA/linkC/", "dirA/linkC/fileB", "dirC/dirD/", "dirC/dirD/fileD"])
        check("dir*/*/**/", ["dirA/linkC/", "dirC/dirD/"])
        check("dir*/*/**/..", ["dirA/linkC/..", "dirC/dirD/.."])
        check("dir*/*/..", ["dirC/dirD/..", "dirA/linkC/.."])
        check("dir*/*/../dirD/**/", ["dirC/dirD/../dirD/"])
        check("dir*/**/fileC", ["dirC/fileC"])
        check("dir*/file*", ["dirB/fileB", "dirC/fileC"])
        check("**/*/fileA", [])
        check("fileB", [])
        check("**/*/fileB", ["dirB/fileB", "dirA/linkC/fileB", "linkB/fileB"])
        check("**/fileB", ["dirB/fileB", "dirA/linkC/fileB", "linkB/fileB"])
        check("*/fileB", ["dirB/fileB", "linkB/fileB"])
        check("*/fileB", ['dirB/fileB', 'linkB/fileB'])
        check("**/file*",
              ["fileA", "dirA/linkC/fileB", "dirB/fileB", "dirC/fileC", "dirC/dirD/fileD",
               "linkB/fileB"])

    def test_walk_top_down(self):
        it = self.root.walk()

        path, dirnames, filenames = next(it)
        dirnames.sort()
        filenames.sort()
        self.assertEqual(path, self.root)
        self.assertEqual(dirnames, ['dirA', 'dirB', 'dirC'])
        self.assertEqual(filenames, ['brokenLink', 'brokenLinkLoop', 'fileA', 'linkA', 'linkB']
                                    if self.ground.can_symlink else ['fileA'])

        path, dirnames, filenames = next(it)
        self.assertEqual(path, self.root / 'dirA')
        self.assertEqual(dirnames, [])
        self.assertEqual(filenames, ['linkC'] if self.ground.can_symlink else [])

        path, dirnames, filenames = next(it)
        self.assertEqual(path, self.root / 'dirB')
        self.assertEqual(dirnames, [])
        self.assertEqual(filenames, ['fileB'])

        path, dirnames, filenames = next(it)
        filenames.sort()
        self.assertEqual(path, self.root / 'dirC')
        self.assertEqual(dirnames, ['dirD'])
        self.assertEqual(filenames, ['fileC', 'novel.txt'])

        path, dirnames, filenames = next(it)
        self.assertEqual(path, self.root / 'dirC' / 'dirD')
        self.assertEqual(dirnames, [])
        self.assertEqual(filenames, ['fileD'])

        self.assertRaises(StopIteration, next, it)

    def test_walk_prune(self):
        expected = {self.root, self.root / 'dirA', self.root / 'dirC', self.root / 'dirC' / 'dirD'}
        actual = set()
        for path, dirnames, filenames in self.root.walk():
            actual.add(path)
            if path == self.root:
                dirnames.remove('dirB')
        self.assertEqual(actual, expected)

    def test_walk_bottom_up(self):
        seen_root = seen_dira = seen_dirb = seen_dirc = seen_dird = False
        for path, dirnames, filenames in self.root.walk(top_down=False):
            if path == self.root:
                self.assertFalse(seen_root)
                self.assertTrue(seen_dira)
                self.assertTrue(seen_dirb)
                self.assertTrue(seen_dirc)
                self.assertEqual(sorted(dirnames), ['dirA', 'dirB', 'dirC'])
                self.assertEqual(sorted(filenames),
                                 ['brokenLink', 'brokenLinkLoop', 'fileA', 'linkA', 'linkB']
                                 if self.ground.can_symlink else ['fileA'])
                seen_root = True
            elif path == self.root / 'dirA':
                self.assertFalse(seen_root)
                self.assertFalse(seen_dira)
                self.assertEqual(dirnames, [])
                self.assertEqual(filenames, ['linkC'] if self.ground.can_symlink else [])
                seen_dira = True
            elif path == self.root / 'dirB':
                self.assertFalse(seen_root)
                self.assertFalse(seen_dirb)
                self.assertEqual(dirnames, [])
                self.assertEqual(filenames, ['fileB'])
                seen_dirb = True
            elif path == self.root / 'dirC':
                self.assertFalse(seen_root)
                self.assertFalse(seen_dirc)
                self.assertTrue(seen_dird)
                self.assertEqual(dirnames, ['dirD'])
                self.assertEqual(sorted(filenames), ['fileC', 'novel.txt'])
                seen_dirc = True
            elif path == self.root / 'dirC' / 'dirD':
                self.assertFalse(seen_root)
                self.assertFalse(seen_dirc)
                self.assertFalse(seen_dird)
                self.assertEqual(dirnames, [])
                self.assertEqual(filenames, ['fileD'])
                seen_dird = True
            else:
                raise AssertionError(f"Unexpected path: {path}")
        self.assertTrue(seen_root)

    def test_info_exists(self):
        p = self.root
        self.assertTrue(p.info.exists())
        self.assertTrue((p / 'dirA').info.exists())
        self.assertTrue((p / 'dirA').info.exists(follow_symlinks=False))
        self.assertTrue((p / 'fileA').info.exists())
        self.assertTrue((p / 'fileA').info.exists(follow_symlinks=False))
        self.assertFalse((p / 'non-existing').info.exists())
        self.assertFalse((p / 'non-existing').info.exists(follow_symlinks=False))
        if self.ground.can_symlink:
            self.assertTrue((p / 'linkA').info.exists())
            self.assertTrue((p / 'linkA').info.exists(follow_symlinks=False))
            self.assertTrue((p / 'linkB').info.exists())
            self.assertTrue((p / 'linkB').info.exists(follow_symlinks=True))
            self.assertFalse((p / 'brokenLink').info.exists())
            self.assertTrue((p / 'brokenLink').info.exists(follow_symlinks=False))
            self.assertFalse((p / 'brokenLinkLoop').info.exists())
            self.assertTrue((p / 'brokenLinkLoop').info.exists(follow_symlinks=False))
        self.assertFalse((p / 'fileA\udfff').info.exists())
        self.assertFalse((p / 'fileA\udfff').info.exists(follow_symlinks=False))
        self.assertFalse((p / 'fileA\x00').info.exists())
        self.assertFalse((p / 'fileA\x00').info.exists(follow_symlinks=False))

    def test_info_is_dir(self):
        p = self.root
        self.assertTrue((p / 'dirA').info.is_dir())
        self.assertTrue((p / 'dirA').info.is_dir(follow_symlinks=False))
        self.assertFalse((p / 'fileA').info.is_dir())
        self.assertFalse((p / 'fileA').info.is_dir(follow_symlinks=False))
        self.assertFalse((p / 'non-existing').info.is_dir())
        self.assertFalse((p / 'non-existing').info.is_dir(follow_symlinks=False))
        if self.ground.can_symlink:
            self.assertFalse((p / 'linkA').info.is_dir())
            self.assertFalse((p / 'linkA').info.is_dir(follow_symlinks=False))
            self.assertTrue((p / 'linkB').info.is_dir())
            self.assertFalse((p / 'linkB').info.is_dir(follow_symlinks=False))
            self.assertFalse((p / 'brokenLink').info.is_dir())
            self.assertFalse((p / 'brokenLink').info.is_dir(follow_symlinks=False))
            self.assertFalse((p / 'brokenLinkLoop').info.is_dir())
            self.assertFalse((p / 'brokenLinkLoop').info.is_dir(follow_symlinks=False))
        self.assertFalse((p / 'dirA\udfff').info.is_dir())
        self.assertFalse((p / 'dirA\udfff').info.is_dir(follow_symlinks=False))
        self.assertFalse((p / 'dirA\x00').info.is_dir())
        self.assertFalse((p / 'dirA\x00').info.is_dir(follow_symlinks=False))

    def test_info_is_file(self):
        p = self.root
        self.assertTrue((p / 'fileA').info.is_file())
        self.assertTrue((p / 'fileA').info.is_file(follow_symlinks=False))
        self.assertFalse((p / 'dirA').info.is_file())
        self.assertFalse((p / 'dirA').info.is_file(follow_symlinks=False))
        self.assertFalse((p / 'non-existing').info.is_file())
        self.assertFalse((p / 'non-existing').info.is_file(follow_symlinks=False))
        if self.ground.can_symlink:
            self.assertTrue((p / 'linkA').info.is_file())
            self.assertFalse((p / 'linkA').info.is_file(follow_symlinks=False))
            self.assertFalse((p / 'linkB').info.is_file())
            self.assertFalse((p / 'linkB').info.is_file(follow_symlinks=False))
            self.assertFalse((p / 'brokenLink').info.is_file())
            self.assertFalse((p / 'brokenLink').info.is_file(follow_symlinks=False))
            self.assertFalse((p / 'brokenLinkLoop').info.is_file())
            self.assertFalse((p / 'brokenLinkLoop').info.is_file(follow_symlinks=False))
        self.assertFalse((p / 'fileA\udfff').info.is_file())
        self.assertFalse((p / 'fileA\udfff').info.is_file(follow_symlinks=False))
        self.assertFalse((p / 'fileA\x00').info.is_file())
        self.assertFalse((p / 'fileA\x00').info.is_file(follow_symlinks=False))

    def test_info_is_symlink(self):
        p = self.root
        self.assertFalse((p / 'fileA').info.is_symlink())
        self.assertFalse((p / 'dirA').info.is_symlink())
        self.assertFalse((p / 'non-existing').info.is_symlink())
        if self.ground.can_symlink:
            self.assertTrue((p / 'linkA').info.is_symlink())
            self.assertTrue((p / 'linkB').info.is_symlink())
            self.assertTrue((p / 'brokenLink').info.is_symlink())
            self.assertFalse((p / 'linkA\udfff').info.is_symlink())
            self.assertFalse((p / 'linkA\x00').info.is_symlink())
            self.assertTrue((p / 'brokenLinkLoop').info.is_symlink())
        self.assertFalse((p / 'fileA\udfff').info.is_symlink())
        self.assertFalse((p / 'fileA\x00').info.is_symlink())


class ZipPathReadTest(ReadTestBase, unittest.TestCase):
    ground = ZipPathGround(ReadableZipPath)


class LocalPathReadTest(ReadTestBase, unittest.TestCase):
    ground = LocalPathGround(ReadableLocalPath)


class PathReadTest(ReadTestBase, unittest.TestCase):
    ground = LocalPathGround(Path)


if __name__ == "__main__":
    unittest.main()
