import glob
import json
import os
import pickle
import subprocess
import sys
from collections import defaultdict
from pathlib import Path

import numpy as np
import pytest

import fsspec
from fsspec.implementations.ftp import FTPFileSystem
from fsspec.implementations.http import HTTPFileSystem
from fsspec.implementations.local import LocalFileSystem
from fsspec.spec import AbstractBufferedFile, AbstractFileSystem
from fsspec.tests.conftest import data

PATHS_FOR_GLOB_TESTS = (
    {"name": "test0.json", "type": "file", "size": 100},
    {"name": "test0.yaml", "type": "file", "size": 100},
    {"name": "test0", "type": "directory", "size": 0},
    {"name": "test0/test0.json", "type": "file", "size": 100},
    {"name": "test0/test0.yaml", "type": "file", "size": 100},
    {"name": "test0/test1", "type": "directory", "size": 0},
    {"name": "test0/test1/test0.json", "type": "file", "size": 100},
    {"name": "test0/test1/test0.yaml", "type": "file", "size": 100},
    {"name": "test0/test1/test2", "type": "directory", "size": 0},
    {"name": "test0/test1/test2/test0.json", "type": "file", "size": 100},
    {"name": "test0/test1/test2/test0.yaml", "type": "file", "size": 100},
    {"name": "test0/test2", "type": "directory", "size": 0},
    {"name": "test0/test2/test0.json", "type": "file", "size": 100},
    {"name": "test0/test2/test0.yaml", "type": "file", "size": 100},
    {"name": "test0/test2/test1", "type": "directory", "size": 0},
    {"name": "test0/test2/test1/test0.json", "type": "file", "size": 100},
    {"name": "test0/test2/test1/test0.yaml", "type": "file", "size": 100},
    {"name": "test0/test2/test1/test3", "type": "directory", "size": 0},
    {"name": "test0/test2/test1/test3/test0.json", "type": "file", "size": 100},
    {"name": "test0/test2/test1/test3/test0.yaml", "type": "file", "size": 100},
    {"name": "test1.json", "type": "file", "size": 100},
    {"name": "test1.yaml", "type": "file", "size": 100},
    {"name": "test1", "type": "directory", "size": 0},
    {"name": "test1/test0.json", "type": "file", "size": 100},
    {"name": "test1/test0.yaml", "type": "file", "size": 100},
    {"name": "test1/test0", "type": "directory", "size": 0},
    {"name": "test1/test0/test0.json", "type": "file", "size": 100},
    {"name": "test1/test0/test0.yaml", "type": "file", "size": 100},
    {"name": "special_chars", "type": "directory", "size": 0},
    {"name": "special_chars/f\\oo.txt", "type": "file", "size": 100},
    {"name": "special_chars/f.oo.txt", "type": "file", "size": 100},
    {"name": "special_chars/f+oo.txt", "type": "file", "size": 100},
    {"name": "special_chars/f(oo.txt", "type": "file", "size": 100},
    {"name": "special_chars/f)oo.txt", "type": "file", "size": 100},
    {"name": "special_chars/f|oo.txt", "type": "file", "size": 100},
    {"name": "special_chars/f^oo.txt", "type": "file", "size": 100},
    {"name": "special_chars/f$oo.txt", "type": "file", "size": 100},
    {"name": "special_chars/f{oo.txt", "type": "file", "size": 100},
    {"name": "special_chars/f}oo.txt", "type": "file", "size": 100},
)

GLOB_POSIX_TESTS = {
    "argnames": ("path", "expected"),
    "argvalues": [
        ("nonexistent", []),
        ("test0.json", ["test0.json"]),
        ("test0", ["test0"]),
        ("test0/", ["test0"]),
        ("test1/test0.yaml", ["test1/test0.yaml"]),
        ("test0/test[1-2]", ["test0/test1", "test0/test2"]),
        ("test0/test[1-2]/", ["test0/test1", "test0/test2"]),
        (
            "test0/test[1-2]/*",
            [
                "test0/test1/test0.json",
                "test0/test1/test0.yaml",
                "test0/test1/test2",
                "test0/test2/test0.json",
                "test0/test2/test0.yaml",
                "test0/test2/test1",
            ],
        ),
        (
            "test0/test[1-2]/*.[j]*",
            ["test0/test1/test0.json", "test0/test2/test0.json"],
        ),
        ("special_chars/f\\oo.*", ["special_chars/f\\oo.txt"]),
        ("special_chars/f.oo.*", ["special_chars/f.oo.txt"]),
        ("special_chars/f+oo.*", ["special_chars/f+oo.txt"]),
        ("special_chars/f(oo.*", ["special_chars/f(oo.txt"]),
        ("special_chars/f)oo.*", ["special_chars/f)oo.txt"]),
        ("special_chars/f|oo.*", ["special_chars/f|oo.txt"]),
        ("special_chars/f^oo.*", ["special_chars/f^oo.txt"]),
        ("special_chars/f$oo.*", ["special_chars/f$oo.txt"]),
        ("special_chars/f{oo.*", ["special_chars/f{oo.txt"]),
        ("special_chars/f}oo.*", ["special_chars/f}oo.txt"]),
        (
            "*",
            [
                "special_chars",
                "test0.json",
                "test0.yaml",
                "test0",
                "test1.json",
                "test1.yaml",
                "test1",
            ],
        ),
        ("*.yaml", ["test0.yaml", "test1.yaml"]),
        (
            "**",
            [
                "special_chars",
                "special_chars/f$oo.txt",
                "special_chars/f(oo.txt",
                "special_chars/f)oo.txt",
                "special_chars/f+oo.txt",
                "special_chars/f.oo.txt",
                "special_chars/f\\oo.txt",
                "special_chars/f^oo.txt",
                "special_chars/f{oo.txt",
                "special_chars/f|oo.txt",
                "special_chars/f}oo.txt",
                "test0.json",
                "test0.yaml",
                "test0",
                "test0/test0.json",
                "test0/test0.yaml",
                "test0/test1",
                "test0/test1/test0.json",
                "test0/test1/test0.yaml",
                "test0/test1/test2",
                "test0/test1/test2/test0.json",
                "test0/test1/test2/test0.yaml",
                "test0/test2",
                "test0/test2/test0.json",
                "test0/test2/test0.yaml",
                "test0/test2/test1",
                "test0/test2/test1/test0.json",
                "test0/test2/test1/test0.yaml",
                "test0/test2/test1/test3",
                "test0/test2/test1/test3/test0.json",
                "test0/test2/test1/test3/test0.yaml",
                "test1.json",
                "test1.yaml",
                "test1",
                "test1/test0.json",
                "test1/test0.yaml",
                "test1/test0",
                "test1/test0/test0.json",
                "test1/test0/test0.yaml",
            ],
        ),
        ("*/", ["special_chars", "test0", "test1"]),
        (
            "**/",
            [
                "special_chars",
                "test0",
                "test0/test1",
                "test0/test1/test2",
                "test0/test2",
                "test0/test2/test1",
                "test0/test2/test1/test3",
                "test1",
                "test1/test0",
            ],
        ),
        ("*/*.yaml", ["test0/test0.yaml", "test1/test0.yaml"]),
        (
            "**/*.yaml",
            [
                "test0.yaml",
                "test0/test0.yaml",
                "test0/test1/test0.yaml",
                "test0/test1/test2/test0.yaml",
                "test0/test2/test0.yaml",
                "test0/test2/test1/test0.yaml",
                "test0/test2/test1/test3/test0.yaml",
                "test1.yaml",
                "test1/test0.yaml",
                "test1/test0/test0.yaml",
            ],
        ),
        (
            "*/test1/*",
            ["test0/test1/test0.json", "test0/test1/test0.yaml", "test0/test1/test2"],
        ),
        ("*/test1/*.yaml", ["test0/test1/test0.yaml"]),
        (
            "**/test1/*",
            [
                "test0/test1/test0.json",
                "test0/test1/test0.yaml",
                "test0/test1/test2",
                "test0/test2/test1/test0.json",
                "test0/test2/test1/test0.yaml",
                "test0/test2/test1/test3",
                "test1/test0.json",
                "test1/test0.yaml",
                "test1/test0",
            ],
        ),
        (
            "**/test1/*.yaml",
            [
                "test0/test1/test0.yaml",
                "test0/test2/test1/test0.yaml",
                "test1/test0.yaml",
            ],
        ),
        ("*/test1/*/", ["test0/test1/test2"]),
        (
            "**/test1/*/",
            ["test0/test1/test2", "test0/test2/test1/test3", "test1/test0"],
        ),
        (
            "*/test1/**",
            [
                "test0/test1",
                "test0/test1/test0.json",
                "test0/test1/test0.yaml",
                "test0/test1/test2",
                "test0/test1/test2/test0.json",
                "test0/test1/test2/test0.yaml",
            ],
        ),
        (
            "**/test1/**",
            [
                "test0/test1",
                "test0/test1/test0.json",
                "test0/test1/test0.yaml",
                "test0/test1/test2",
                "test0/test1/test2/test0.json",
                "test0/test1/test2/test0.yaml",
                "test0/test2/test1",
                "test0/test2/test1/test0.json",
                "test0/test2/test1/test0.yaml",
                "test0/test2/test1/test3",
                "test0/test2/test1/test3/test0.json",
                "test0/test2/test1/test3/test0.yaml",
                "test1",
                "test1/test0.json",
                "test1/test0.yaml",
                "test1/test0",
                "test1/test0/test0.json",
                "test1/test0/test0.yaml",
            ],
        ),
        ("*/test1/**/", ["test0/test1", "test0/test1/test2"]),
        (
            "**/test1/**/",
            [
                "test0/test1",
                "test0/test1/test2",
                "test0/test2/test1",
                "test0/test2/test1/test3",
                "test1",
                "test1/test0",
            ],
        ),
        (
            "test0/*",
            ["test0/test0.json", "test0/test0.yaml", "test0/test1", "test0/test2"],
        ),
        ("test0/*.yaml", ["test0/test0.yaml"]),
        (
            "test0/**",
            [
                "test0",
                "test0/test0.json",
                "test0/test0.yaml",
                "test0/test1",
                "test0/test1/test0.json",
                "test0/test1/test0.yaml",
                "test0/test1/test2",
                "test0/test1/test2/test0.json",
                "test0/test1/test2/test0.yaml",
                "test0/test2",
                "test0/test2/test0.json",
                "test0/test2/test0.yaml",
                "test0/test2/test1",
                "test0/test2/test1/test0.json",
                "test0/test2/test1/test0.yaml",
                "test0/test2/test1/test3",
                "test0/test2/test1/test3/test0.json",
                "test0/test2/test1/test3/test0.yaml",
            ],
        ),
        ("test0/*/", ["test0/test1", "test0/test2"]),
        (
            "test0/**/",
            [
                "test0",
                "test0/test1",
                "test0/test1/test2",
                "test0/test2",
                "test0/test2/test1",
                "test0/test2/test1/test3",
            ],
        ),
        ("test0/*/*.yaml", ["test0/test1/test0.yaml", "test0/test2/test0.yaml"]),
        (
            "test0/**/*.yaml",
            [
                "test0/test0.yaml",
                "test0/test1/test0.yaml",
                "test0/test1/test2/test0.yaml",
                "test0/test2/test0.yaml",
                "test0/test2/test1/test0.yaml",
                "test0/test2/test1/test3/test0.yaml",
            ],
        ),
        (
            "test0/*/test1/*",
            [
                "test0/test2/test1/test0.json",
                "test0/test2/test1/test0.yaml",
                "test0/test2/test1/test3",
            ],
        ),
        ("test0/*/test1/*.yaml", ["test0/test2/test1/test0.yaml"]),
        (
            "test0/**/test1/*",
            [
                "test0/test1/test0.json",
                "test0/test1/test0.yaml",
                "test0/test1/test2",
                "test0/test2/test1/test0.json",
                "test0/test2/test1/test0.yaml",
                "test0/test2/test1/test3",
            ],
        ),
        (
            "test0/**/test1/*.yaml",
            ["test0/test1/test0.yaml", "test0/test2/test1/test0.yaml"],
        ),
        ("test0/*/test1/*/", ["test0/test2/test1/test3"]),
        ("test0/**/test1/*/", ["test0/test1/test2", "test0/test2/test1/test3"]),
        (
            "test0/*/test1/**",
            [
                "test0/test2/test1",
                "test0/test2/test1/test0.json",
                "test0/test2/test1/test0.yaml",
                "test0/test2/test1/test3",
                "test0/test2/test1/test3/test0.json",
                "test0/test2/test1/test3/test0.yaml",
            ],
        ),
        (
            "test0/**/test1/**",
            [
                "test0/test1",
                "test0/test1/test0.json",
                "test0/test1/test0.yaml",
                "test0/test1/test2",
                "test0/test1/test2/test0.json",
                "test0/test1/test2/test0.yaml",
                "test0/test2/test1",
                "test0/test2/test1/test0.json",
                "test0/test2/test1/test0.yaml",
                "test0/test2/test1/test3",
                "test0/test2/test1/test3/test0.json",
                "test0/test2/test1/test3/test0.yaml",
            ],
        ),
        ("test0/*/test1/**/", ["test0/test2/test1", "test0/test2/test1/test3"]),
        (
            "test0/**/test1/**/",
            [
                "test0/test1",
                "test0/test1/test2",
                "test0/test2/test1",
                "test0/test2/test1/test3",
            ],
        ),
    ],
}


class DummyTestFS(AbstractFileSystem):
    protocol = "mock"
    _file_class = AbstractBufferedFile
    _fs_contents = (
        {"name": "top_level", "type": "directory"},
        {"name": "top_level/second_level", "type": "directory"},
        {"name": "top_level/second_level/date=2019-10-01", "type": "directory"},
        {
            "name": "top_level/second_level/date=2019-10-01/a.parquet",
            "type": "file",
            "size": 100,
        },
        {
            "name": "top_level/second_level/date=2019-10-01/b.parquet",
            "type": "file",
            "size": 100,
        },
        {"name": "top_level/second_level/date=2019-10-02", "type": "directory"},
        {
            "name": "top_level/second_level/date=2019-10-02/a.parquet",
            "type": "file",
            "size": 100,
        },
        {"name": "top_level/second_level/date=2019-10-04", "type": "directory"},
        {
            "name": "top_level/second_level/date=2019-10-04/a.parquet",
            "type": "file",
            "size": 100,
        },
        {"name": "misc", "type": "directory"},
        {"name": "misc/foo.txt", "type": "file", "size": 100},
    )

    def __init__(self, fs_content=None, **kwargs):
        if fs_content is not None:
            self._fs_contents = fs_content
        super().__init__(**kwargs)

    def __getitem__(self, name):
        for item in self._fs_contents:
            if item["name"] == name:
                return item
        raise IndexError(f"{name} not found!")

    def ls(self, path, detail=True, refresh=True, **kwargs):
        if kwargs.pop("strip_proto", True):
            path = self._strip_protocol(path)

        files = not refresh and self._ls_from_cache(path)
        if not files:
            files = [
                file for file in self._fs_contents if path == self._parent(file["name"])
            ]
            files.sort(key=lambda file: file["name"])
            self.dircache[path.rstrip("/")] = files

        if detail:
            return files
        return [file["name"] for file in files]

    @classmethod
    def get_test_paths(cls, start_with=""):
        """Helper to return directory and file paths with no details"""
        all = [
            file["name"]
            for file in cls._fs_contents
            if file["name"].startswith(start_with)
        ]
        return all

    def _open(
        self,
        path,
        mode="rb",
        block_size=None,
        autocommit=True,
        cache_options=None,
        **kwargs,
    ):
        return self._file_class(
            self,
            path,
            mode,
            block_size,
            autocommit,
            cache_options=cache_options,
            **kwargs,
        )


@pytest.mark.parametrize(
    ["test_paths", "recursive", "maxdepth", "expected"],
    [
        (
            (
                "top_level/second_level",
                "top_level/sec*",
                "top_level/sec*vel",
                "top_level/*",
            ),
            True,
            None,
            [
                "top_level/second_level",
                "top_level/second_level/date=2019-10-01",
                "top_level/second_level/date=2019-10-01/a.parquet",
                "top_level/second_level/date=2019-10-01/b.parquet",
                "top_level/second_level/date=2019-10-02",
                "top_level/second_level/date=2019-10-02/a.parquet",
                "top_level/second_level/date=2019-10-04",
                "top_level/second_level/date=2019-10-04/a.parquet",
            ],
        ),
        (
            (
                "top_level/second_level",
                "top_level/sec*",
                "top_level/sec*vel",
                "top_level/*",
            ),
            False,
            None,
            [
                "top_level/second_level",
            ],
        ),
        (
            ("top_level/second_level",),
            True,
            1,
            [
                "top_level/second_level",
                "top_level/second_level/date=2019-10-01",
                "top_level/second_level/date=2019-10-02",
                "top_level/second_level/date=2019-10-04",
            ],
        ),
        (
            ("top_level/second_level",),
            True,
            2,
            [
                "top_level/second_level",
                "top_level/second_level/date=2019-10-01",
                "top_level/second_level/date=2019-10-01/a.parquet",
                "top_level/second_level/date=2019-10-01/b.parquet",
                "top_level/second_level/date=2019-10-02",
                "top_level/second_level/date=2019-10-02/a.parquet",
                "top_level/second_level/date=2019-10-04",
                "top_level/second_level/date=2019-10-04/a.parquet",
            ],
        ),
        (
            ("top_level/*", "top_level/sec*", "top_level/sec*vel", "top_level/*"),
            True,
            1,
            ["top_level/second_level"],
        ),
        (
            ("top_level/*", "top_level/sec*", "top_level/sec*vel", "top_level/*"),
            True,
            2,
            [
                "top_level/second_level",
                "top_level/second_level/date=2019-10-01",
                "top_level/second_level/date=2019-10-02",
                "top_level/second_level/date=2019-10-04",
            ],
        ),
        (
            ("top_level/**",),
            False,
            None,
            [
                "top_level",
                "top_level/second_level",
                "top_level/second_level/date=2019-10-01",
                "top_level/second_level/date=2019-10-01/a.parquet",
                "top_level/second_level/date=2019-10-01/b.parquet",
                "top_level/second_level/date=2019-10-02",
                "top_level/second_level/date=2019-10-02/a.parquet",
                "top_level/second_level/date=2019-10-04",
                "top_level/second_level/date=2019-10-04/a.parquet",
            ],
        ),
        (
            ("top_level/**",),
            True,
            None,
            [
                "top_level",
                "top_level/second_level",
                "top_level/second_level/date=2019-10-01",
                "top_level/second_level/date=2019-10-01/a.parquet",
                "top_level/second_level/date=2019-10-01/b.parquet",
                "top_level/second_level/date=2019-10-02",
                "top_level/second_level/date=2019-10-02/a.parquet",
                "top_level/second_level/date=2019-10-04",
                "top_level/second_level/date=2019-10-04/a.parquet",
            ],
        ),
        (("top_level/**",), True, 1, ["top_level", "top_level/second_level"]),
        (
            ("top_level/**",),
            True,
            2,
            [
                "top_level",
                "top_level/second_level",
                "top_level/second_level/date=2019-10-01",
                "top_level/second_level/date=2019-10-01/a.parquet",
                "top_level/second_level/date=2019-10-01/b.parquet",
                "top_level/second_level/date=2019-10-02",
                "top_level/second_level/date=2019-10-02/a.parquet",
                "top_level/second_level/date=2019-10-04",
                "top_level/second_level/date=2019-10-04/a.parquet",
            ],
        ),
        (
            ("top_level/**/a.*",),
            False,
            None,
            [
                "top_level/second_level/date=2019-10-01/a.parquet",
                "top_level/second_level/date=2019-10-02/a.parquet",
                "top_level/second_level/date=2019-10-04/a.parquet",
            ],
        ),
        (
            ("top_level/**/a.*",),
            True,
            None,
            [
                "top_level/second_level/date=2019-10-01/a.parquet",
                "top_level/second_level/date=2019-10-02/a.parquet",
                "top_level/second_level/date=2019-10-04/a.parquet",
            ],
        ),
        (
            ("top_level/**/second_level/date=2019-10-02",),
            False,
            2,
            [
                "top_level/second_level/date=2019-10-02",
            ],
        ),
        (
            ("top_level/**/second_level/date=2019-10-02",),
            True,
            2,
            [
                "top_level/second_level/date=2019-10-02",
                "top_level/second_level/date=2019-10-02/a.parquet",
            ],
        ),
        [("misc/foo.txt", "misc/*.txt"), False, None, ["misc/foo.txt"]],
        [("misc/foo.txt", "misc/*.txt"), True, None, ["misc/foo.txt"]],
        (
            ("",),
            False,
            None,
            [DummyTestFS.root_marker],
        ),
        (
            ("",),
            True,
            None,
            DummyTestFS.get_test_paths() + [DummyTestFS.root_marker],
        ),
        [
            (Path("misc/foo.txt"),),
            False,
            None,
            [f"misc{os.sep}foo.txt"],
        ],
    ],
)
def test_expand_path(test_paths, recursive, maxdepth, expected):
    """Test a number of paths and then their combination which should all yield
    the same set of expanded paths"""
    test_fs = DummyTestFS()

    # test single query
    for test_path in test_paths:
        paths = test_fs.expand_path(test_path, recursive=recursive, maxdepth=maxdepth)
        assert sorted(paths) == sorted(expected)

    # test with all queries
    paths = test_fs.expand_path(
        list(test_paths), recursive=recursive, maxdepth=maxdepth
    )
    assert sorted(paths) == sorted(expected)


def test_expand_paths_with_wrong_args():
    test_fs = DummyTestFS()

    with pytest.raises(ValueError):
        test_fs.expand_path("top_level", recursive=True, maxdepth=0)
    with pytest.raises(ValueError):
        test_fs.expand_path("top_level", maxdepth=0)
    with pytest.raises(FileNotFoundError):
        test_fs.expand_path("top_level/**/second_level/date=2019-10-02", maxdepth=1)
    with pytest.raises(FileNotFoundError):
        test_fs.expand_path("nonexistent/*")


@pytest.mark.xfail
def test_find():
    """Test .find() method on debian server (ftp, https) with constant folder"""
    filesystem, host, test_path = (
        FTPFileSystem,
        "ftp.fau.de",
        "ftp://ftp.fau.de/debian-cd/current/amd64/log/success",
    )
    test_fs = filesystem(host)
    filenames_ftp = test_fs.find(test_path)
    assert filenames_ftp

    filesystem, host, test_path = (
        HTTPFileSystem,
        "https://ftp.fau.de",
        "https://ftp.fau.de/debian-cd/current/amd64/log/success",
    )
    test_fs = filesystem()
    filenames_http = test_fs.find(test_path)
    roots = [f.rsplit("/", 1)[-1] for f in filenames_http]

    assert all(f.rsplit("/", 1)[-1] in roots for f in filenames_ftp)


def test_find_details():
    test_fs = DummyTestFS()
    filenames = test_fs.find("/")
    details = test_fs.find("/", detail=True)
    for filename in filenames:
        assert details[filename] == test_fs.info(filename)


def test_find_file():
    test_fs = DummyTestFS()

    filename = "misc/foo.txt"
    assert test_fs.find(filename) == [filename]
    assert test_fs.find(filename, detail=True) == {filename: {}}


def test_cache():
    fs = DummyTestFS()
    fs2 = DummyTestFS()
    assert fs is fs2

    assert DummyTestFS.current() is fs
    assert len(fs._cache) == 1
    del fs2
    assert len(fs._cache) == 1
    del fs

    # keeps and internal reference, doesn't get collected
    assert len(DummyTestFS._cache) == 1

    DummyTestFS.clear_instance_cache()
    assert len(DummyTestFS._cache) == 0


def test_cache_not_pickled(server):
    fs = fsspec.filesystem(
        "http",
        cache_type="readahead",
        headers={"give_length": "true", "head_ok": "true"},
    )
    filepath = server.realfile
    length = 3
    f = fs.open(filepath, mode="rb")
    assert isinstance(f, AbstractBufferedFile)
    assert not f.cache.cache  # No cache initially
    assert f.read(length=length) == data[:length]
    assert f.cache.cache == data  # Cache is populated

    # Roundtrip through pickle
    import pickle

    f2 = pickle.loads(pickle.dumps(f))
    assert not f2.cache.cache  # No cache initially
    assert (
        f2.read(length=length) == data[length : 2 * length]
    )  # Read file from previous seek point
    assert f2.cache.cache == data[length:]  # Cache is populated


def test_current():
    fs = DummyTestFS()
    fs2 = DummyTestFS(arg=1)

    assert fs is not fs2
    assert DummyTestFS.current() is fs2

    DummyTestFS()
    assert DummyTestFS.current() is fs


def test_alias():
    with pytest.warns(FutureWarning, match="add_aliases"):
        DummyTestFS(add_aliases=True)


def test_add_docs_warns():
    with pytest.warns(FutureWarning, match="add_docs"):
        AbstractFileSystem(add_docs=True)


def test_cache_options():
    fs = DummyTestFS()
    f = AbstractBufferedFile(fs, "misc/foo.txt", cache_type="bytes")
    assert f.cache.trim

    # TODO: dummy buffered file
    f = AbstractBufferedFile(
        fs, "misc/foo.txt", cache_type="bytes", cache_options={"trim": False}
    )
    assert f.cache.trim is False

    f = fs.open("misc/foo.txt", cache_type="bytes", cache_options={"trim": False})
    assert f.cache.trim is False


def test_trim_kwarg_warns():
    fs = DummyTestFS()
    with pytest.warns(FutureWarning, match="cache_options"):
        AbstractBufferedFile(fs, "misc/foo.txt", cache_type="bytes", trim=False)


def tests_file_open_error(monkeypatch):
    class InitiateError(ValueError): ...

    class UploadError(ValueError): ...

    class DummyBufferedFile(AbstractBufferedFile):
        can_initiate = False

        def _initiate_upload(self):
            if not self.can_initiate:
                raise InitiateError

        def _upload_chunk(self, final=False):
            raise UploadError

    monkeypatch.setattr(DummyTestFS, "_file_class", DummyBufferedFile)

    fs = DummyTestFS()
    with pytest.raises(InitiateError):
        with fs.open("misc/foo.txt", "wb") as stream:
            stream.write(b"hello" * stream.blocksize * 2)

    with pytest.raises(UploadError):
        with fs.open("misc/foo.txt", "wb") as stream:
            stream.can_initiate = True
            stream.write(b"hello" * stream.blocksize * 2)


def test_eq():
    fs = DummyTestFS()
    result = fs == 1
    assert result is False

    f = AbstractBufferedFile(fs, "misc/foo.txt", cache_type="bytes")
    result = f == 1
    assert result is False


def test_pickle_multiple():
    a = DummyTestFS(1)
    b = DummyTestFS(2, bar=1)

    x = pickle.dumps(a)
    y = pickle.dumps(b)

    del a, b
    DummyTestFS.clear_instance_cache()

    result = pickle.loads(x)
    assert result.storage_args == (1,)
    assert result.storage_options == {}

    result = pickle.loads(y)
    assert result.storage_args == (2,)
    assert result.storage_options == {"bar": 1}


def test_json():
    a = DummyTestFS(1)
    b = DummyTestFS(2, bar=1)

    outa = a.to_json()
    outb = b.to_json()

    assert json.loads(outb)  # is valid JSON
    assert a != b
    assert "bar" in outb

    assert DummyTestFS.from_json(outa) is a
    assert DummyTestFS.from_json(outb) is b


def test_json_path_attr():
    a = DummyTestFS(1)
    b = DummyTestFS(2, bar=Path("baz"))

    outa = a.to_json()
    outb = b.to_json()

    assert json.loads(outb)  # is valid JSON
    assert a != b
    assert "bar" in outb

    assert DummyTestFS.from_json(outa) is a
    assert DummyTestFS.from_json(outb) is b


def test_json_fs_attr():
    a = DummyTestFS(1)
    b = DummyTestFS(2, bar=Path("baz"))
    c = DummyTestFS(3, baz=b)

    outa = a.to_json()
    outb = b.to_json()
    outc = c.to_json()

    assert json.loads(outc)  # is valid JSON
    assert b != c
    assert "baz" in outc

    assert DummyTestFS.from_json(outa) is a
    assert DummyTestFS.from_json(outb) is b
    assert DummyTestFS.from_json(outc) is c


def test_json_dict_attr():
    a = DummyTestFS(1)
    b = DummyTestFS(2, bar=Path("baz"))
    c = DummyTestFS(3, baz={"key": b})

    outa = a.to_json()
    outb = b.to_json()
    outc = c.to_json()

    assert json.loads(outc)  # is valid JSON
    assert b != c
    assert "baz" in outc

    assert DummyTestFS.from_json(outa) is a
    assert DummyTestFS.from_json(outb) is b
    assert DummyTestFS.from_json(outc) is c


def test_dict():
    a = DummyTestFS(1)
    b = DummyTestFS(2, bar=1)

    outa = a.to_dict()
    outb = b.to_dict()

    assert isinstance(outa, dict)
    assert a != b
    assert "bar" in outb

    assert DummyTestFS.from_dict(outa) is a
    assert DummyTestFS.from_dict(outb) is b


def test_dict_path_attr():
    a = DummyTestFS(1)
    b = DummyTestFS(2, bar=Path("baz"))

    outa = a.to_dict()
    outb = b.to_dict()

    assert isinstance(outa, dict)
    assert a != b
    assert outb["bar"]["str"] == "baz"

    assert DummyTestFS.from_dict(outa) is a
    assert DummyTestFS.from_dict(outb) is b


def test_dict_fs_attr():
    a = DummyTestFS(1)
    b = DummyTestFS(2, bar=Path("baz"))
    c = DummyTestFS(3, baz=b)

    outa = a.to_dict()
    outb = b.to_dict()
    outc = c.to_dict()

    assert isinstance(outc, dict)
    assert b != c
    assert outc["baz"] == outb

    assert DummyTestFS.from_dict(outa) is a
    assert DummyTestFS.from_dict(outb) is b
    assert DummyTestFS.from_dict(outc) is c


def test_dict_dict_attr():
    a = DummyTestFS(1)
    b = DummyTestFS(2, bar=Path("baz"))
    c = DummyTestFS(3, baz={"key": b})

    outa = a.to_dict()
    outb = b.to_dict()
    outc = c.to_dict()

    assert isinstance(outc, dict)
    assert b != c
    assert outc["baz"]["key"] == outb

    assert DummyTestFS.from_dict(outa) is a
    assert DummyTestFS.from_dict(outb) is b
    assert DummyTestFS.from_dict(outc) is c


def test_dict_idempotent():
    a = DummyTestFS(1)

    outa = a.to_dict()

    assert DummyTestFS.from_dict(outa) is a
    assert DummyTestFS.from_dict(outa) is a


def test_dict_json_serializable():
    a = DummyTestFS(1)
    b = DummyTestFS(2, bar=Path("baz"))
    c = DummyTestFS(3, baz=b)

    outa = a.to_dict()
    outb = b.to_dict()
    outc = c.to_dict()

    json.dumps(outa)
    json.dumps(outb)
    json.dumps(outc)


def test_serialize_no_password():
    fs = DummyTestFS(1, password="admin")

    assert "password" not in fs.to_json(include_password=False)
    assert "password" not in fs.to_dict(include_password=False)


def test_serialize_with_password():
    fs = DummyTestFS(1, password="admin")

    assert "password" in fs.to_json(include_password=True)
    assert "password" in fs.to_dict(include_password=True)


def test_from_dict_valid():
    fs = DummyTestFS.from_dict({"cls": "fsspec.tests.test_spec.DummyTestFS"})
    assert isinstance(fs, DummyTestFS)

    fs = DummyTestFS.from_dict({"cls": "fsspec.tests.test_spec.DummyTestFS", "bar": 1})
    assert fs.storage_options["bar"] == 1

    fs = DummyTestFS.from_dict({"cls": "fsspec.implementations.local.LocalFileSystem"})
    assert isinstance(fs, LocalFileSystem)

    fs = DummyTestFS.from_dict(
        {
            "cls": "fsspec.implementations.local.LocalFileSystem",
            "protocol": "local",
        }
    )
    assert isinstance(fs, LocalFileSystem)


def test_from_dict_invalid():
    with pytest.raises(ValueError, match="Not a serialized AbstractFileSystem"):
        DummyTestFS.from_dict({})

    with pytest.raises(ValueError, match="Not a serialized AbstractFileSystem"):
        DummyTestFS.from_dict({"cls": "pathlib.Path"})

    with pytest.raises(ValueError, match="Not a serialized AbstractFileSystem"):
        DummyTestFS.from_dict({"protocol": "local"})  # cls must be present


def test_ls_from_cache():
    fs = DummyTestFS()
    uncached_results = fs.ls("top_level/second_level/", refresh=True)

    assert fs.ls("top_level/second_level/", refresh=False) == uncached_results

    # _strip_protocol removes everything by default though
    # for the sake of testing the _ls_from_cache interface
    # directly, we need run one time more without that call
    # to actually verify that our stripping in the client
    # function works.
    assert (
        fs.ls("top_level/second_level/", refresh=False, strip_proto=False)
        == uncached_results
    )


@pytest.mark.parametrize(
    "dt",
    [
        np.int8,
        np.int16,
        np.int32,
        np.int64,
        np.uint8,
        np.uint16,
        np.uint32,
        np.uint64,
        np.float32,
        np.float64,
    ],
)
def test_readinto_with_numpy(tmpdir, dt):
    store_path = str(tmpdir / "test_arr.npy")
    arr = np.arange(10, dtype=dt)
    arr.tofile(store_path)

    arr2 = np.empty_like(arr)
    with fsspec.open(store_path, "rb") as f:
        f.readinto(arr2)

    assert np.array_equal(arr, arr2)


@pytest.mark.parametrize(
    "dt",
    [
        np.int8,
        np.int16,
        np.int32,
        np.int64,
        np.uint8,
        np.uint16,
        np.uint32,
        np.uint64,
        np.float32,
        np.float64,
    ],
)
def test_readinto_with_multibyte(ftp_writable, tmpdir, dt):
    host, port, user, pw = ftp_writable
    ftp = FTPFileSystem(host=host, port=port, username=user, password=pw)

    with ftp.open("/out", "wb") as fp:
        arr = np.arange(10, dtype=dt)
        fp.write(arr.tobytes())

    with ftp.open("/out", "rb") as fp:
        arr2 = np.empty_like(arr)
        fp.readinto(arr2)

    assert np.array_equal(arr, arr2)


class DummyOpenFS(DummyTestFS):
    blocksize = 10

    def _open(self, path, mode="rb", **kwargs):
        stream = open(path, mode)
        stream.size = os.stat(path).st_size
        return stream


class BasicCallback(fsspec.Callback):
    def __init__(self, **kwargs):
        self.events = []
        super().__init__(**kwargs)

    def set_size(self, size):
        self.events.append(("set_size", size))

    def relative_update(self, inc=1):
        self.events.append(("relative_update", inc))


def imitate_transfer(size, chunk, *, file=True):
    events = [("set_size", size)]
    events.extend(("relative_update", size // chunk) for _ in range(chunk))
    if file:
        # The reason that there is a relative_update(0) at the
        # end is that, we don't have an early exit on the
        # implementations of get_file/put_file so it needs to
        # go through the callback to get catch by the while's
        # condition and then it will stop the transfer.
        events.append(("relative_update", 0))

    return events


def get_files(tmpdir, amount=10):
    src, dest, base = [], [], []
    for index in range(amount):
        src_path = tmpdir / f"src_{index}.txt"
        src_path.write_text("x" * 50, "utf-8")

        src.append(str(src_path))
        dest.append(str(tmpdir / f"dst_{index}.txt"))
        base.append(str(tmpdir / f"file_{index}.txt"))
    return src, dest, base


def test_dummy_callbacks_file(tmpdir):
    fs = DummyOpenFS()
    callback = BasicCallback()

    file = tmpdir / "file.txt"
    source = tmpdir / "tmp.txt"
    destination = tmpdir / "tmp2.txt"

    size = 100
    source.write_text("x" * 100, "utf-8")

    fs.put_file(source, file, callback=callback)

    # -1 here since put_file no longer has final zero-size put
    assert callback.events == imitate_transfer(size, 10)[:-1]
    callback.events.clear()

    fs.get_file(file, destination, callback=callback)
    assert callback.events == imitate_transfer(size, 10)
    callback.events.clear()

    assert destination.read_text("utf-8") == "x" * 100


def test_dummy_callbacks_files(tmpdir):
    fs = DummyOpenFS()
    callback = BasicCallback()
    src, dest, base = get_files(tmpdir)

    fs.put(src, base, callback=callback)
    assert callback.events == imitate_transfer(10, 10, file=False)
    callback.events.clear()

    fs.get(base, dest, callback=callback)
    assert callback.events == imitate_transfer(10, 10, file=False)


class BranchableCallback(BasicCallback):
    def __init__(self, source, dest=None, events=None, **kwargs):
        super().__init__(**kwargs)
        if dest:
            self.key = source, dest
        else:
            self.key = (source,)
        self.events = events or defaultdict(list)

    def branch(self, path_1, path_2, kwargs):
        from fsspec.implementations.local import make_path_posix

        path_1 = make_path_posix(path_1)
        path_2 = make_path_posix(path_2)
        kwargs["callback"] = BranchableCallback(path_1, path_2, events=self.events)

    def set_size(self, size):
        self.events[self.key].append(("set_size", size))

    def relative_update(self, inc=1):
        self.events[self.key].append(("relative_update", inc))


def test_dummy_callbacks_files_branched(tmpdir):
    fs = DummyOpenFS()
    src, dest, base = get_files(tmpdir)

    callback = BranchableCallback("top-level")

    def check_events(lpaths, rpaths):
        from fsspec.implementations.local import make_path_posix

        base_keys = zip(make_path_posix(lpaths), make_path_posix(rpaths))
        assert set(callback.events.keys()) == {("top-level",), *base_keys}
        assert callback.events["top-level",] == imitate_transfer(10, 10, file=False)

        for key in base_keys:
            assert callback.events[key] == imitate_transfer(50, 5)

    fs.put(src, base, callback=callback)
    check_events(src, base)
    callback.events.clear()

    fs.get(base, dest, callback=callback)
    check_events(base, dest)
    callback.events.clear()


def _clean_paths(paths, prefix=""):
    """
    Helper to cleanup paths results by doing the following:
      - remove the prefix provided from all paths
      - remove the trailing slashes from all paths
      - remove duplicates paths
      - sort all paths
    """
    paths_list = paths
    if isinstance(paths, dict):
        paths_list = list(paths)
    paths_list = [p.replace(prefix, "").strip("/") for p in sorted(set(paths_list))]
    if isinstance(paths, dict):
        return {p: paths[p] for p in paths_list}
    return paths_list


@pytest.fixture(scope="function")
def glob_fs():
    return DummyTestFS(fs_content=PATHS_FOR_GLOB_TESTS)


@pytest.fixture(scope="function")
def glob_files_folder(tmp_path):
    local_fs = LocalFileSystem(auto_mkdir=True)
    local_fake_dir = str(tmp_path)
    for path_info in PATHS_FOR_GLOB_TESTS:
        if path_info["type"] == "file":
            local_fs.touch(path=f"{tmp_path}/{path_info['name']}")
    return local_fake_dir


@pytest.mark.skipif(
    sys.platform.startswith("win"),
    reason="no need to run python glob posix tests on windows",
)
@pytest.mark.parametrize(
    GLOB_POSIX_TESTS["argnames"],
    GLOB_POSIX_TESTS["argvalues"],
)
def test_posix_tests_python_glob(path, expected, glob_files_folder):
    """
    Tests against python glob to check if our posix tests are accurate.
    """
    os.chdir(glob_files_folder)

    python_output = glob.glob(pathname=path, recursive=True)
    assert _clean_paths(python_output, glob_files_folder) == _clean_paths(expected)


@pytest.mark.skipif(
    sys.platform.startswith("win"),
    reason="no need to run bash stat posix tests on windows",
)
@pytest.mark.parametrize(
    GLOB_POSIX_TESTS["argnames"],
    GLOB_POSIX_TESTS["argvalues"],
)
def test_posix_tests_bash_stat(path, expected, glob_files_folder):
    """
    Tests against bash stat to check if our posix tests are accurate.
    """
    try:
        subprocess.check_output(["bash", "-c", "shopt -s globstar"])
    except FileNotFoundError:
        pytest.skip("bash is not available")
    except subprocess.CalledProcessError:
        pytest.skip("globstar option is not available")

    bash_path = (
        path.replace("\\", "\\\\")
        .replace("$", "\\$")
        .replace("(", "\\(")
        .replace(")", "\\)")
        .replace("|", "\\|")
    )
    bash_output = subprocess.run(
        [
            "bash",
            "-c",
            f"cd {glob_files_folder} && shopt -s globstar && stat -c %N {bash_path}",
        ],
        capture_output=True,
        check=False,
    )
    # Remove the last element always empty
    bash_output = bash_output.stdout.decode("utf-8").replace("'", "").split("\n")[:-1]
    assert _clean_paths(bash_output, glob_files_folder) == _clean_paths(expected)


@pytest.mark.parametrize(
    GLOB_POSIX_TESTS["argnames"],
    GLOB_POSIX_TESTS["argvalues"],
)
def test_glob_posix_rules(path, expected, glob_fs):
    output = glob_fs.glob(path=f"mock://{path}")
    assert _clean_paths(output) == _clean_paths(expected)

    detailed_output = glob_fs.glob(path=f"mock://{path}", detail=True)
    for name, info in _clean_paths(detailed_output).items():
        assert info == glob_fs[name]
