import sys, os, re
import errno
import pathlib
import shutil
import functools
from pathlib import Path

from pytest import fixture


_HERE = Path(__file__).absolute().parent


# Make sure we don't use config files from system or user home
os.environ['XDG_CONFIG_DIRS'] = str(_HERE / 'config/sys')
os.environ['XDG_CONFIG_HOME'] = str(_HERE / 'config/home')


def count_files(dir_eq_count):
    found_files = []
    def count_one_dir(path):
        fcount = 0
        for entry in os.scandir(path):
            if entry.is_dir(follow_symlinks=False):
                fcount += count_one_dir(entry.path)
                continue
            fcount += 1
            found_files.append(entry.path)
        return fcount

    res = True
    for path, exp_count in dir_eq_count.items():
        got_count = count_one_dir(path)
        if got_count != exp_count:
            res = False
            print(f"Expected {exp_count} files in '{path}', 'count_files' got {got_count}", file=sys.stderr)
            print(f"   found files: {found_files}", file=sys.stderr)

    return res


def check_symlink(symlink, exp_points_to, *, broken=False):
    """Verify that 'symlink' exists and points to 'exp_points_to'"""
    points_to = os.readlink(symlink)
    if points_to != exp_points_to:
        print(f"Expected '{symlink}' to point to '{exp_points_to}', but got '{points_to}'")
        return False

    points_to_path = os.path.join(os.path.dirname(symlink), points_to)
    if not broken and not os.path.exists(points_to_path):
        print(f"'{symlink}' points to '{points_to}', but the target '{points_to_path}' does not exist.'")
        return False

    return True


# ------ duplicates_dir fixture and collaborating decorators: ---------

_generated_test_file_names = {}
_same_content_files_funcs = {}
_different_content_files_funcs = {}


underscore_test_re = re.compile('_test$')

def _test_key_shortener(key_prefix, key_postfix):
    tfunc_prefix = key_prefix.replace('test.', 'test_') + '_'
    return key_prefix + '.' + key_postfix.replace(tfunc_prefix, '')


@fixture
def duplicates_dir(request, monkeypatch):
    """Fixture expects 'same_content_files' and/or 'different_content_files' and possibly 'symlink_files' decorators to have been called for the test function."""

    key_prefix = underscore_test_re.sub('', request.node.module.__name__)
    test_key = _test_key_shortener(key_prefix, request.node.name.split('[')[0])
    out_dir = pathlib.Path(os.path.join(_HERE, 'out', test_key))

    try:
        shutil.rmtree(out_dir)
    except OSError as ex:
        if ex.errno != errno.ENOENT:
            raise
    out_dir.mkdir(parents=True)

    has_files = False
    for scf in _same_content_files_funcs.get(out_dir, []):
        scf(out_dir)
        has_files = True

    for dcf in _different_content_files_funcs.get(out_dir, []):
        dcf(out_dir)
        has_files = True

    if not has_files:
        raise Exception(duplicates_dir.__doc__)

    monkeypatch.chdir(out_dir)
    return out_dir


def _check_test_file_name(out_dir, fn):
    assert not os.path.isabs(fn), f"No absolute paths allowed, found '{fn}'"
    assert not '..' in fn, f"No .. allowed, found '{fn}'"
    fn = os.path.join(out_dir, fn)
    if fn in _generated_test_file_names[out_dir]:
        raise Exception(f"File '{fn}' is generated by more than 1 decorator, or more than once by a decorator.")
    _generated_test_file_names[out_dir].add(fn)


def _generate_test_files(out_dir, content, append_fn, *paths):
    for fn in paths:
        out_fn = os.path.join(out_dir, fn)
        pathlib.Path(os.path.dirname(out_fn)).mkdir(parents=True, exist_ok=True)
        if not (content or append_fn):
            # Create empty file
            pathlib.Path(out_fn).touch()
            continue

        with open(out_fn, 'w', encoding="utf-8") as ff:
            ff.write(content + (fn if append_fn else ''))


def _test_files_creator_decorator(func, file_content_funcs, new_file_content_func, *paths):
    key_prefix = underscore_test_re.sub('', func.__module__)
    test_key = _test_key_shortener(key_prefix, func.__name__)
    out_dir = pathlib.Path(os.path.join(_HERE, 'out', test_key))

    dcfs = file_content_funcs.setdefault(out_dir, [])
    dcfs.append(new_file_content_func)
    _generated_test_file_names.setdefault(out_dir, set())
    for fn in paths:
        _check_test_file_name(out_dir, fn)

    @functools.wraps(func)
    def wrapper(duplicates_dir, *args, **kwargs):  # 'duplicates_dir' is fixture pylint: disable=redefined-outer-name
        return func(duplicates_dir, *args, **kwargs)

    return wrapper


def same_content_files(content, *paths):
    """A decorator factory which adds a fixture to create test files with the same 'content' for the wrapped test function."""

    def _same_content_files(out_dir):
        _generate_test_files(out_dir, content, False, *paths)

    def deco(func):
        return _test_files_creator_decorator(func, _same_content_files_funcs, _same_content_files, *paths)

    return deco


def empty_files(*paths):
    """A decorator factory which adds a fixture to create empty test files for the wrapped test function."""

    def _empty_files(out_dir):
        _generate_test_files(out_dir, None, False, *paths)

    def deco(func):
        return _test_files_creator_decorator(func, _same_content_files_funcs, _empty_files, *paths)

    return deco


def different_content_files(base_content, *paths):
    """A decorator factory which adds a fixture to create test files with different content for the wrapped test function."""

    def _different_content_files(out_dir):
        _generate_test_files(out_dir, base_content, True, *paths)

    def deco(func):
        return _test_files_creator_decorator(func, _different_content_files_funcs, _different_content_files, *paths)

    return deco


def symlink_files(links, *, broken=False):
    """A decorator factory which adds a fixture to create symlinks to test file created with one of the (other) decorators.

    This must be called after other decorators creating files which are linked to, unless broken is True.
    """

    def _symlink_files(out_dir):
        for points_to_fn, symlink_fn in links:
            if not broken:
                abs_points_to = os.path.normpath(os.path.join(out_dir, os.path.dirname(symlink_fn), points_to_fn))
                gtf = _generated_test_file_names[out_dir]
                assert abs_points_to in gtf or os.path.isdir(abs_points_to), \
                    f"Expect abs symlink {abs_points_to} (from {points_to_fn}) to point one of the generated files {gtf} or a directory"
            out_fn = os.path.join(out_dir, symlink_fn)
            pathlib.Path(os.path.dirname(out_fn)).mkdir(parents=True, exist_ok=True)
            os.symlink(points_to_fn, out_fn)

    def deco(func):
        return _test_files_creator_decorator(func, _different_content_files_funcs, _symlink_files, *[symlink for (_, symlink) in links])

    return deco


def hardlink_files(links):
    """A decorator factory which adds a fixture to hard links to test file created with one of the (other) decorators.

    This must be called after other decorators creating files which are linked to.
    """

    def _hardlink_files(out_dir):
        for points_to_fn, hardlink_fn in links:
            abs_points_to = os.path.join(out_dir, points_to_fn)
            assert abs_points_to in _generated_test_file_names[out_dir]
            out_fn = os.path.join(out_dir, hardlink_fn)
            pathlib.Path(os.path.dirname(out_fn)).mkdir(parents=True, exist_ok=True)
            os.link(abs_points_to, out_fn)

    def deco(func):
        return _test_files_creator_decorator(func, _different_content_files_funcs, _hardlink_files, *[hardlink for (_, hardlink) in links])

    return deco
