# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)

"""Tests for config/user.py."""

# Standard library imports
import configparser as cp
import os

# Third party imports
import pytest

# Local imports
from spyder.config.main import CONF_VERSION, DEFAULTS
from spyder.config.user import NoDefault, UserConfig
from spyder.utils.fixtures import tmpconfig


# --- Default config tests
# ----------------------------------------------------------------------------
@pytest.mark.parametrize(
    "test_input, expected",
    [
        (('sec', 'opt', 'val'), '[sec][opt] = val\n'),
        (('sec', 'opt', 50), '[sec][opt] = 50\n'),
        (('sec', 'opt', [50]), '[sec][opt] = [50]\n'),
        (('sec', 'opt', (50, 2)), '[sec][opt] = (50, 2)\n'),
        (('sec', 'opt', {50}), '[sec][opt] = {50}\n'),
        (('sec', 'opt', {'k': 50}), "[sec][opt] = {'k': 50}\n"),
        (('sec', 'opt', False), '[sec][opt] = False\n'),
        (('sec', 'opt', True), '[sec][opt] = True\n'),
        (('sec space', 'opt', True), '[sec space][opt] = True\n'),
        (('sec space', 'opt space', True), '[sec space][opt space] = True\n'),
    ]
)
def test_default_config_set(defaultconfig, capsys, test_input, expected):
    section, option, value = test_input
    defaultconfig._set(section, option, value, verbose=True)
    assert defaultconfig.sections() == [section]

    captured = capsys.readouterr()
    assert captured.out == expected


def test_default_config_save_write(defaultconfig):
    defaultconfig._save()


def test_default_config_set_defaults(defaultconfig):
    defaults = [('main2', {'opt': 1})]
    defaultconfig.set_defaults(defaults)
    assert 'main2' in defaultconfig.sections()


# --- User config tests
# ============================================================================
# --- Helpers and checkers
@pytest.mark.parametrize('value,expected', [
    ('3.2.1', '3.2'),
    ('3.2.0', '3.2'),
])
def test_userconfig_get_minor_version(value, expected):
    result = UserConfig._get_minor_version(value)
    assert result == expected


@pytest.mark.parametrize('value,expected', [
    ('3.2.1', '3'),
    ('0.2.0', '0'),
])
def test_userconfig_get_major_version(value, expected):
    result = UserConfig._get_major_version(value)
    assert result == expected


@pytest.mark.parametrize('test_version', [
    ('abc'),
    ('x.x.x'),
    ('1.0'),
    ('-1.0'),
    ('-1.0.0'),
    (''),
])
def test_userconfig_check_version(tmpdir, test_version, userconfig):
    name = 'spyder-test'
    path = str(tmpdir)
    with pytest.raises(ValueError):
        UserConfig(name=name, path=path, defaults=DEFAULTS,
                   load=False, version=test_version, raw_mode=True)

    # Check that setting a wrong version also runs the version checks
    with pytest.raises(ValueError):
        userconfig.set_version(test_version)


def test_userconfig_check_defaults(tmpdir, capsys):
    name = 'foobar'
    path = str(tmpdir)
    conf = UserConfig(name=name, path=path, defaults={},
                      load=False, version='1.0.0', backup=False,
                      raw_mode=True)

    conf._check_defaults({})
    conf._check_defaults({'option2': 'value2'})
    conf._check_defaults([])
    conf._check_defaults([('sec', {'opt1': 'val1'})])

    with pytest.raises(AssertionError):
        conf._check_defaults([(123, {'opt1': 'val1'})])

    with pytest.raises(AssertionError):
        conf._check_defaults([('sec', {123: 'val1'})])

    with pytest.raises(ValueError):
        conf._check_defaults({1, 2, 3})

    with pytest.raises(ValueError):
        conf._check_defaults('asd')


def test_userconfig_check_section_option(userconfig):
    section = userconfig._check_section_option(None, 'version')
    assert section == userconfig.DEFAULT_SECTION_NAME

    section = userconfig._check_section_option(None, 'opt')
    assert section == userconfig.DEFAULT_SECTION_NAME

    section = userconfig._check_section_option('sec', 'opt')
    assert section == 'sec'

    # Check with invalid section
    with pytest.raises(RuntimeError):
        section = userconfig._check_section_option(123, 'opt')

    # Check with invalid option
    with pytest.raises(RuntimeError):
        section = userconfig._check_section_option(None, 123)


def test_userconfig_load_from_ini(tmpdir, capsys):
    # Test error on loading
    name = 'foobar'
    path = str(tmpdir)
    fpath = os.path.join(path, '{}.ini'.format(name))
    with open(fpath, 'w') as fh:
        fh.write('[sec\n')

    conf = UserConfig(name=name, path=path, defaults=DEFAULTS,
                      load=True, version=CONF_VERSION, backup=False,
                      raw_mode=True)
    assert conf.get_config_fpath() == fpath

    captured = capsys.readouterr()
    assert 'Warning' in captured.out


# --- Public API
def test_userconfig_get_version(userconfig, tmpconfig):
    assert tmpconfig.get_version() == CONF_VERSION
    assert userconfig.get_version() == '1.0.0'
    userconfig.remove_option('main', 'version')
    assert userconfig.get_version() == '0.0.0'


def test_userconfig_set_version(userconfig):
    version = '1000.1000.1000'
    userconfig.set_version(version)
    assert userconfig.get_version() == version


def test_userconfig_reset_to_defaults(tmpdir):
    name = 'foobar'
    path = str(tmpdir)
    defaults = [('main', {'opt': False}), ('test', {'opt': False})]
    conf = UserConfig(name=name, path=path, defaults=defaults,
                      load=False, version='1.0.0', backup=False,
                      raw_mode=True)
    # Skip section, should go to default
    assert conf.defaults == defaults
    conf.set(None, 'opt', True)
    assert conf.get(None, 'opt') is True
    conf.reset_to_defaults()
    assert conf.get(None, 'opt') is False

    # Provide section, should go to sectio
    assert conf.defaults == defaults
    conf.set('test', 'opt', True)
    assert conf.get('test', 'opt') is True
    conf.reset_to_defaults()
    assert conf.get('test', 'opt') is False


def test_userconfig_set_as_defaults(tmpdir):
    name = 'foobar'
    path = str(tmpdir)
    conf = UserConfig(name=name, path=path, defaults={},
                      load=False, version='1.0.0', backup=False,
                      raw_mode=True)

    # TODO: Is this expected? this seems inconsistent
    assert conf.defaults == [('main', {})]
    conf.set_as_defaults()
    assert conf.defaults == []


def test_userconfig_get_default(userconfig, tmpconfig):
    # Not existing and no defaults
    value = userconfig.get_default('other_section', 'other_option')
    assert value == NoDefault

    # Existing and no defaults
    value = userconfig.get_default('section', 'option')
    assert value == NoDefault

    # Existing and defaults
    value = tmpconfig.get_default('main', 'window/is_maximized')
    assert value is True


class TestUserConfigGet:

    @pytest.mark.parametrize(
        'defaults,value',
        [
            # Valid values
            ([('test', {'opt': 'value'})], 'value'),
            ([('test', {'opt': u'"éàÇÃãéèï"'})], u'"éàÇÃãéèï"'),
            ([('test', {'opt': 'éàÇÃãéèï'})], u'éàÇÃãéèï'),
            ([('test', {'opt': True})], True),
            ([('test', {'opt': UserConfig})], repr(UserConfig)),
            ([('test', {'opt': 123})], 123),
            ([('test', {'opt': 123.123})], 123.123),
            ([('test', {'opt': [1]})], [1]),
            ([('test', {'opt': {'key': 'val'}})], {'key': 'val'}),
        ]
    )
    def test_userconfig_get(self, defaults, value, tmpdir):
        name = 'foobar'
        path = str(tmpdir)
        conf = UserConfig(name=name, path=path, defaults=defaults,
                          load=False, version='1.0.0', backup=False,
                          raw_mode=True)

        assert conf.get('test', 'opt') == value

    @pytest.mark.parametrize(
        'defaults,default,raises',
        [
            # Valid values
            ([('test2', {'opt': 'value'})], 'val', True),
            ([('test2', {'opt': 'value'})], 'val', False),
            ([('test', {'opt': 'value'})], 'val', False),
        ]
    )
    def test_userconfig_get2(self, defaults, default, raises, tmpdir):
        name = 'foobar'
        path = str(tmpdir)
        conf = UserConfig(name=name, path=path, defaults=defaults,
                          load=False, version='1.0.0', backup=False,
                          raw_mode=True)

        if raises:
            with pytest.raises(cp.NoSectionError):
                conf.get('test', 'opt')
        else:
            conf.get('test', 'opt', default)

    def test_userconfig_get_string_from_inifile(self, userconfig):
        assert userconfig.get('section', 'option') == 'value'

    def test_userconfig_get_does_not_eval_functions(self, userconfig):
        # Regression test for spyder-ide/spyder#3354.
        userconfig.set('section', 'option', 'print("foo")')
        assert userconfig.get('section', 'option') == 'print("foo")'


def test_userconfig_set_default(userconfig):
    value = userconfig.get_default('section', 'option')
    assert value == NoDefault

    # TODO: Is this expected behavior? should. If no defaults are provided
    # then they cannot be set individually until set_as_defaults() is ran?
    default_value = 'foobar'
    value = userconfig.set_default('section', 'option', default_value)
    value = userconfig.get_default('section', 'option')
    assert value == NoDefault

    userconfig.set_as_defaults()
    value = userconfig.get_default('section', 'option')
    expected = 'value'
    assert value == expected
    value = userconfig.set_default('section', 'option', default_value)
    value = userconfig.get_default('section', 'option')
    assert value == default_value


class TestUserConfigSet:

    @pytest.mark.parametrize(
        'defaults,value',
        [
            # Valid values
            ([('test', {'opt': 'value'})], 'other'),
            ([('test', {'opt': 'éàÇÃãéèï'})], u'ãéèï'),
            ([('test', {'opt': True})], False),
            ([('test', {'opt': UserConfig})], dict),
            ([('test', {'opt': 123})], 345),
            ([('test', {'opt': 123.123})], 345.345),
            ([('test', {'opt': [1]})], [1]),
            ([('test', {'opt': {'key': 'val'}})], {'key2': 'val2'}),
            # Value not in defaults
            ([('test', {'opt1': 'value'})], 'other'),
        ]
    )
    def test_userconfig_set_valid(self, defaults, value, tmpdir):
        name = 'foobar'
        path = str(tmpdir)
        conf = UserConfig(name=name, path=path, defaults=defaults,
                          load=False, version='1.0.0', backup=False,
                          raw_mode=True)
        conf.set('test', 'opt', value)

    @pytest.mark.parametrize(
        'defaults,value',
        [
            ([('test', {'opt': 123})], 'no'),
            ([('test', {'opt': 123.123})], 'n9'),
            ([('test', {'opt': 123.123})], 'n9'),
        ]
    )
    def test_userconfig_set_invalid(self, defaults, value, tmpdir):
        name = 'foobar'
        path = str(tmpdir)
        conf = UserConfig(name=name, path=path, defaults=defaults,
                         load=False, version='1.0.0', backup=False,
                          raw_mode=True)

        with pytest.raises(ValueError):
            conf.set('test', 'opt', value)

    def test_userconfig_set_with_string(self, userconfig):
        userconfig.set('section', 'option', 'new value')
        with open(userconfig.get_config_fpath()) as inifile:
            ini_contents = inifile.read()

        expected = '[main]\nversion = 1.0.0\n\n'
        expected += "[section]\noption = new value\n\n"

        assert ini_contents == expected

    def test_userconfig_set_percentage_string(self, userconfig):
        """Test to set an option with a '%'."""
        userconfig.set('section', 'option', '%value')
        assert userconfig.get('section', 'option') == '%value'


def test_userconfig_remove_section(userconfig):
    assert 'section' in userconfig.sections()
    userconfig.remove_section('section')
    assert 'section' not in userconfig.sections()


def test_userconfig_remove_option(userconfig):
    assert userconfig.get('section', 'option') == 'value'
    userconfig.remove_option('section', 'option')
    with pytest.raises(cp.NoOptionError):
        userconfig.get('section', 'option')


def test_userconfig_cleanup(userconfig):
    configpath = userconfig.get_config_fpath()
    assert os.path.isfile(configpath)
    userconfig.cleanup()
    assert not os.path.isfile(configpath)


@pytest.mark.no_reset_conf
def test_invalid_shortcuts(tmp_path):
    name = 'invalid-shortcuts'
    path = str(tmp_path)

    for defaults in [
        # Shortcut is not of the form context/name
        [('shortcuts', {'foo': "Ctrl+I"})],
        # Shortcut contains more than one slash, which breaks the way we parse
        # them
        [('shortcuts', {'editor/foo/bar': "Ctrl+I"})],
    ]:
        with pytest.raises(ValueError):
            UserConfig(
                name=name,
                path=path,
                defaults=defaults,
                load=False,
                version="1.0.0",
                backup=False,
                raw_mode=True,
            )


# --- SpyderUserConfig tests
# ============================================================================
# --- Compatibility API
class TestSpyderConfigApplyPatches:

    def test_spyderconfig_apply_configuration_patches_42(
            self, spyderconfig_patches_42):
        # Check that the value is updated
        value = spyderconfig_patches_42.get('ipython_console',
                                            'startup/run_lines')
        expected_value = 'value1; value2'
        assert value == expected_value

    def test_spyderconfig_apply_configuration_patches_45(
            self, spyderconfig_patches_45):
        # Check that the value is not updated
        value = spyderconfig_patches_45.get('ipython_console',
                                            'startup/run_lines')
        expected_value = 'value1,value2'
        assert value == expected_value


def test_spyderconfig_get_defaults_path_name_from_version(spyderconfig):
    func = spyderconfig.get_defaults_path_name_from_version
    _, name = func('50.0.0')
    assert name == 'defaults-50.0.0'

    path, name = func('51.0.0')
    assert name == 'defaults-spyder-test-51.0.0'
    assert path.endswith('defaults')

    path, name = func('53.0.0')
    assert name == 'defaults-spyder-test-53.0.0'
    assert path.endswith('defaults')


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