"""Test the config system."""
from __future__ import annotations

import contextlib
import itertools
import pathlib
import random
import string
import sys
from typing import Any, Iterable, List, cast

import pytest
from pytest import mark

# isort: split

sys.path += [str(pathlib.Path(__file__).parent.parent.resolve()), "."]
print(sys.path)

from _old_config import old_config  # noqa: E402

from config import LinuxConfig, config  # noqa: E402


def _fuzz_list(length: int) -> List[str]:
    out = []
    for _ in range(length):
        out.append(_fuzz_generators[str](random.randint(0, 1337)))

    return cast(List[str], out)


_fuzz_generators = {  # Type annotating this would be a nightmare.
    int: lambda i: random.randint(min(0, i), max(0, i)),
    # This doesn't cover unicode, or random bytes. Use them at your own peril
    str: lambda l: "".join(random.choice(string.ascii_letters + string.digits + '\r\n') for _ in range(l)),
    bool: lambda _: bool(random.choice((True, False))),
    list: _fuzz_list,
}


def _get_fuzz(_type: Any, num_values=50, value_length=(0, 10)) -> list:
    return [_fuzz_generators[_type](random.randint(*value_length)) for _ in range(num_values)]


int_tests = [0, 1, 2, 3, (1 << 32)-1, -1337]

string_tests = [
    "test", "", "this\nis\na\ntest", "orange sidewinder", "needs \\ more backslashes\\", "\\; \\n", r"\\\\ \\\\; \\\\n",
    r"entry with escapes \\ \\; \\n"
]

list_tests = [
    ["test"],
    ["multiple", "entries"],
    ["multiple", "entries", "with", "", "blanks"],
    ["entry", "that ends", "in a", ""],
    ["entry with \n", "newlines\nin", "weird\nplaces"],
    [r"entry with escapes \\ \\; \\n"],
    [r"\\\\ \\\\; \\\\n"]
]
bool_tests = [True, False]

big_int = int(0xFFFFFFFF)  # 32 bit int


def _make_params(args: List[Any], id_name: str = 'random_test_{i}') -> list:
    return [pytest.param(x, id=id_name.format(i=i)) for i, x in enumerate(args)]


def _build_test_list(static_data, random_data, random_id_name='random_test_{i}') -> Iterable:
    return itertools.chain(static_data, _make_params(random_data, id_name=random_id_name))


class TestNewConfig:
    """Test the new config with an array of hand picked and random data."""

    def __update_linuxconfig(self) -> None:
        """On linux config uses ConfigParser, which doesn't update from disk changes. Force the update here."""
        if isinstance(config, LinuxConfig) and config.config is not None:
            config.config.read(config.filename)

    @mark.parametrize("i", _build_test_list(int_tests, _get_fuzz(int, value_length=(-big_int, big_int))))
    def test_ints(self, i: int) -> None:
        """Save int and then unpack it again."""
        if sys.platform == 'win32':
            i = abs(i)

        name = f"int_test_{i}"
        config.set(name, i)
        config.save()
        self.__update_linuxconfig()
        assert i == config.get_int(name)
        config.delete(name)

    @mark.parametrize("string", _build_test_list(string_tests, _get_fuzz(str, value_length=(0, 512))))
    def test_string(self, string: str) -> None:
        """Save a string and then ask for it back."""
        name = f'str_test_{hash(string)}'
        config.set(name, string)
        config.save()
        self.__update_linuxconfig()
        assert string == config.get_str(name)
        config.delete(name)

    @mark.parametrize("lst", _build_test_list(list_tests, _get_fuzz(list)))
    def test_list(self, lst: List[str]) -> None:
        """Save a list and then ask for it back."""
        name = f'list_test_{ hash("".join(lst)) }'
        config.set(name, lst)

        config.save()
        self.__update_linuxconfig()

        assert lst == config.get_list(name)

        config.delete(name)

    @mark.parametrize('b', bool_tests)
    def test_bool(self, b: bool) -> None:
        """Save a bool and ask for it back."""
        name = str(b)
        config.set(name, b)
        config.save()
        self.__update_linuxconfig()
        assert b == config.get_bool(name)
        config.delete(name)

    def test_get_no_error(self) -> None:
        """Regression test to ensure that get() doesn't throw a TypeError."""
        name = 'test-get'
        config.set(name, '1337')
        config.save()
        self.__update_linuxconfig()
        with pytest.deprecated_call():
            res = config.get(name)

        assert res == '1337'
        config.delete(name)


class TestOldNewConfig:
    """Tests going through the old config and out the new config."""

    KEY_PREFIX = 'oldnew_'

    def teardown_method(self) -> None:
        """
        Teardown for all config tests to save out configs.

        It is expected that tests have cleaned up whatever keys they have aded.
        This is here to ensure that a save out happens for configs that need it.
        """
        old_config.save()
        config.save()

    def cleanup_entry(self, entry: str) -> None:
        """Remove the given key, on both sides if on linux."""
        config.delete(entry)
        if sys.platform == 'linux':
            old_config.delete(entry)

    def __update_linuxconfig(self) -> None:
        """On linux config uses ConfigParser, which doesn't update from disk changes. Force the update here."""
        if isinstance(config, LinuxConfig) and config.config is not None:
            config.config.read(config.filename)

    @mark.parametrize("i", _build_test_list(int_tests, _get_fuzz(int, 50, (-big_int, big_int))))
    def test_int(self, i: int) -> None:
        """Save an int though the old config, recall it using the new config."""
        if sys.platform == 'win32':
            i = abs(i)

        name = self.KEY_PREFIX + f'int_{i}'
        old_config.set(name, i)
        old_config.save()

        self.__update_linuxconfig()

        res = config.get_int(name)
        with contextlib.ExitStack() as stack:
            stack.callback(self.cleanup_entry, name)
            assert res == i

    @mark.parametrize("string", _build_test_list(string_tests, _get_fuzz(str, value_length=(0, 512))))
    def test_string(self, string: str) -> None:
        """Save a string though the old config, recall it using the new config."""
        string = string.replace("\r", "")  # The old config does _not_ support \r in its entries. We do.
        name = self.KEY_PREFIX + f'string_{hash(string)}'
        old_config.set(name, string)
        old_config.save()

        self.__update_linuxconfig()

        res = config.get_str(name)
        with contextlib.ExitStack() as stack:
            stack.callback(self.cleanup_entry, name)
            assert res == string

    @mark.parametrize("lst", _build_test_list(list_tests, _get_fuzz(list)))
    def test_list(self, lst: List[str]) -> None:
        """Save a list though the old config, recall it using the new config."""
        lst = [x.replace("\r", "") for x in lst]  # OldConfig on linux fails to store these correctly
        if sys.platform == 'win32':
            # old conf on windows replaces empty entries with spaces as a workaround for a bug. New conf does not
            # So insert those spaces here, to ensure that it works otherwise.
            lst = [e if len(e) > 0 else ' ' for e in lst]

        name = self.KEY_PREFIX + f'list_test_{ hash("".join(lst)) }'
        old_config.set(name, lst)
        old_config.save()

        self.__update_linuxconfig()

        res = config.get_list(name)
        with contextlib.ExitStack() as stack:
            stack.callback(self.cleanup_entry, name)
            assert res == lst

    @mark.skipif(sys.platform == 'win32', reason="Old Windows config does not support bool types")
    @mark.parametrize("b", bool_tests)
    def test_bool(self, b: bool) -> None:
        """Save a bool though the old config, recall it using the new config."""
        name = str(b)
        old_config.set(name, b)
        old_config.save()
        self.__update_linuxconfig()
        with contextlib.ExitStack() as stack:
            stack.callback(self.cleanup_entry, name)
            assert config.get_bool(name) == b