"""
tests.unit.utils.test_configparser
==================================

Test the funcs in the custom parsers in salt.utils.configparser
"""

import copy
import errno
import logging
import os

import salt.utils.configparser
import salt.utils.files
import salt.utils.platform
import salt.utils.stringutils
from tests.support.runtests import RUNTIME_VARS
from tests.support.unit import TestCase

log = logging.getLogger(__name__)


# The user.name param here is intentionally indented with spaces instead of a
# tab to test that we properly load a file with mixed indentation.
ORIG_CONFIG = """[user]
        name = Артём Анисимов
\temail = foo@bar.com
[remote "origin"]
\turl = https://github.com/terminalmage/salt.git
\tfetch = +refs/heads/*:refs/remotes/origin/*
\tpushurl = git@github.com:terminalmage/salt.git
[color "diff"]
\told = 196
\tnew = 39
[core]
\tpager = less -R
\trepositoryformatversion = 0
\tfilemode = true
\tbare = false
\tlogallrefupdates = true
[alias]
\tmodified = ! git status --porcelain | awk 'match($1, "M"){print $2}'
\tgraph = log --all --decorate --oneline --graph
\thist = log --pretty=format:\\"%h %ad | %s%d [%an]\\" --graph --date=short
[http]
\tsslverify = false""".split(
    "\n"
)


class TestGitConfigParser(TestCase):
    """
    Tests for salt.utils.configparser.GitConfigParser
    """

    maxDiff = None
    orig_config = os.path.join(RUNTIME_VARS.TMP, "test_gitconfig.orig")
    new_config = os.path.join(RUNTIME_VARS.TMP, "test_gitconfig.new")
    remote = 'remote "origin"'

    def tearDown(self):
        del self.conf
        try:
            os.remove(self.new_config)
        except OSError as exc:
            if exc.errno != errno.ENOENT:
                raise

    def setUp(self):
        if not os.path.exists(self.orig_config):
            with salt.utils.files.fopen(self.orig_config, "wb") as fp_:
                fp_.write(salt.utils.stringutils.to_bytes(os.linesep.join(ORIG_CONFIG)))
        self.conf = salt.utils.configparser.GitConfigParser()
        with salt.utils.files.fopen(self.orig_config, "rb") as fp:
            self.conf._read(fp, self.orig_config)

    @classmethod
    def tearDownClass(cls):
        try:
            os.remove(cls.orig_config)
        except OSError as exc:
            if exc.errno != errno.ENOENT:
                raise

    @staticmethod
    def fix_indent(lines):
        """
        Fixes the space-indented 'user' line, because when we write the config
        object to a file space indentation will be replaced by tab indentation.
        """
        ret = copy.copy(lines)
        for i, _ in enumerate(ret):
            if ret[i].startswith(salt.utils.configparser.GitConfigParser.SPACEINDENT):
                ret[i] = ret[i].replace(
                    salt.utils.configparser.GitConfigParser.SPACEINDENT, "\t"
                )
        return ret

    @staticmethod
    def get_lines(path):
        with salt.utils.files.fopen(path, "rb") as fp_:
            return salt.utils.stringutils.to_unicode(fp_.read()).splitlines()

    def _test_write(self, mode):
        kwargs = {"mode": mode}
        if salt.utils.platform.is_windows() and "b" not in mode:
            kwargs["encoding"] = "utf-8"
        with salt.utils.files.fopen(self.new_config, **kwargs) as fp_:
            self.conf.write(fp_)
        self.assertEqual(self.get_lines(self.new_config), self.fix_indent(ORIG_CONFIG))

    def test_get(self):
        """
        Test getting an option's value
        """
        # Numeric values should be loaded as strings
        self.assertEqual(self.conf.get('color "diff"', "old"), "196")
        # Complex strings should be loaded with their literal quotes and
        # slashes intact
        self.assertEqual(
            self.conf.get("alias", "modified"),
            """! git status --porcelain | awk 'match($1, "M"){print $2}'""",
        )
        self.assertEqual(
            self.conf.get("alias", "hist"),
            salt.utils.stringutils.to_unicode(
                r"""log --pretty=format:\"%h %ad | %s%d [%an]\" --graph --date=short"""
            ),
        )

    def test_read_space_indent(self):
        """
        Test that user.name was successfully loaded despite being indented
        using spaces instead of a tab. Additionally, this tests that the value
        was loaded as a unicode type on PY2.
        """
        self.assertEqual(self.conf.get("user", "name"), "Артём Анисимов")

    def test_set_new_option(self):
        """
        Test setting a new option in an existing section
        """
        self.conf.set("http", "useragent", "myawesomeagent")
        self.assertEqual(self.conf.get("http", "useragent"), "myawesomeagent")

    def test_add_section(self):
        """
        Test adding a section and adding an item to that section
        """
        self.conf.add_section("foo")
        self.conf.set("foo", "bar", "baz")
        self.assertEqual(self.conf.get("foo", "bar"), "baz")

    def test_replace_option(self):
        """
        Test replacing an existing option
        """
        # We're also testing the normalization of key names, here. Setting
        # "sslVerify" should actually set an "sslverify" option.
        self.conf.set("http", "sslVerify", "true")
        self.assertEqual(self.conf.get("http", "sslverify"), "true")

    def test_set_multivar(self):
        """
        Test setting a multivar and then writing the resulting file
        """
        orig_refspec = "+refs/heads/*:refs/remotes/origin/*"
        new_refspec = "+refs/tags/*:refs/tags/*"
        # Make sure that the original value is a string
        self.assertEqual(self.conf.get(self.remote, "fetch"), orig_refspec)
        # Add another refspec
        self.conf.set_multivar(self.remote, "fetch", new_refspec)
        # The value should now be a list
        self.assertEqual(
            self.conf.get(self.remote, "fetch"), [orig_refspec, new_refspec]
        )
        # Write the config object to a file
        with salt.utils.files.fopen(self.new_config, "wb") as fp_:
            self.conf.write(fp_)
        # Confirm that the new file was written correctly
        expected = self.fix_indent(ORIG_CONFIG)
        expected.insert(6, f"\tfetch = {new_refspec}")
        self.assertEqual(self.get_lines(self.new_config), expected)

    def test_remove_option(self):
        """
        test removing an option, including all items from a multivar
        """
        for item in ("fetch", "pushurl"):
            self.conf.remove_option(self.remote, item)
            # To confirm that the option is now gone, a get should raise an
            # NoOptionError exception.
            self.assertRaises(
                salt.utils.configparser.NoOptionError, self.conf.get, self.remote, item
            )

    def test_remove_option_regexp(self):
        """
        test removing an option, including all items from a multivar
        """
        orig_refspec = "+refs/heads/*:refs/remotes/origin/*"
        new_refspec_1 = "+refs/tags/*:refs/tags/*"
        new_refspec_2 = "+refs/foo/*:refs/foo/*"
        # First, add both refspecs
        self.conf.set_multivar(self.remote, "fetch", new_refspec_1)
        self.conf.set_multivar(self.remote, "fetch", new_refspec_2)
        # Make sure that all three values are there
        self.assertEqual(
            self.conf.get(self.remote, "fetch"),
            [orig_refspec, new_refspec_1, new_refspec_2],
        )
        # If the regex doesn't match, no items should be removed
        self.assertFalse(
            self.conf.remove_option_regexp(
                self.remote,
                "fetch",
                salt.utils.stringutils.to_unicode(r"\d{7,10}"),
            )
        )
        # Make sure that all three values are still there (since none should
        # have been removed)
        self.assertEqual(
            self.conf.get(self.remote, "fetch"),
            [orig_refspec, new_refspec_1, new_refspec_2],
        )
        # Remove one of the values
        self.assertTrue(self.conf.remove_option_regexp(self.remote, "fetch", "tags"))
        # Confirm that the value is gone
        self.assertEqual(
            self.conf.get(self.remote, "fetch"), [orig_refspec, new_refspec_2]
        )
        # Remove the other one we added earlier
        self.assertTrue(self.conf.remove_option_regexp(self.remote, "fetch", "foo"))
        # Since the option now only has one value, it should be a string
        self.assertEqual(self.conf.get(self.remote, "fetch"), orig_refspec)
        # Remove the last remaining option
        self.assertTrue(self.conf.remove_option_regexp(self.remote, "fetch", "heads"))
        # Trying to do a get now should raise an exception
        self.assertRaises(
            salt.utils.configparser.NoOptionError, self.conf.get, self.remote, "fetch"
        )

    def test_write(self):
        """
        Test writing using non-binary filehandle
        """
        self._test_write(mode="w")

    def test_write_binary(self):
        """
        Test writing using binary filehandle
        """
        self._test_write(mode="wb")
