"""
Tests of the lint-amnesty command.
"""

from collections import defaultdict
from io import StringIO
import itertools
import textwrap
import unittest

from edx_lint.cmd.amnesty import PylintError, fix_pylint, PYLINT_EXCEPTION_REGEX, parse_pylint_output


class AmnestyTest(unittest.TestCase):
    """
    Tests of lint-amnesty.
    """

    def assert_amnesty(self, input_code, errors, expected):
        """
        Assert that fix_pylint produces ``expected`` when fed ``input_code`` and the
        list of errors ``errors``.

        Arguments:
            input_code: A string of python code. Will be textwrap.dedented.
            errors: A list of PylintErrors
            expected: A string of python code. Will be textwrap.dedented.
        """
        input_code = textwrap.dedent(input_code)
        expected = textwrap.dedent(expected)

        errors_by_line = defaultdict(list)
        for error in errors:
            errors_by_line[error.linenum].append(error)

        output_lines = itertools.chain.from_iterable(
            fix_pylint(line, errors_by_line[lineno]) for lineno, line in enumerate(StringIO(input_code), start=1)
        )

        self.assertEqual(expected.split("\n"), "".join(output_lines).split("\n"))

    def assert_pylint_exception_match(self, expected, line):
        """
        Assert that PYLINT_EXCEPTION_REGEX mateches ``expected`` in ``line``.
        """
        self.assertEqual(expected, PYLINT_EXCEPTION_REGEX.search(line).group(0))

    def test_pylint_exception_re(self):
        self.assert_pylint_exception_match(
            "  # pylint: disable=some-exception, other-exception",
            "some line  # pylint: disable=some-exception, other-exception",
        )
        self.assert_pylint_exception_match(
            "  # pylint: disable=some-exception,other-exception",
            "some line  # pylint: disable=some-exception,other-exception",
        )
        self.assert_pylint_exception_match(
            "  # pylint: disable=some-exception", "some line  # pylint: disable=some-exception"
        )
        self.assert_pylint_exception_match(
            "  # pylint: disable=some-exception", "some line  # pylint: disable=some-exception#noqa"
        )
        self.assert_pylint_exception_match(
            "  # pylint: disable=some-exception", "some line  # pylint: disable=some-exception  #noqa"
        )
        self.assert_pylint_exception_match(
            "# pylint: disable=some-exception", "some line# pylint: disable=some-exception#noqa"
        )

    def test_parse_pylint_output(self):
        pylint_output = textwrap.dedent(
            '''\
            ************* Module cms.urls
            cms/urls.py:1: [C0111(missing-docstring), ] Missing module docstring
            ************* Module cms.envs.test
            cms/envs/test.py:81: [C0301(line-too-long), ] Line too long (144/120)
            ************* Module test.test_amnesty
            test/test_amnesty.py:78: [C0326(bad-whitespace), ] Exactly one space required around assignment
                    pylint_output=textwrap.dedent("""\\
                                 ^
        '''
        )
        self.assertEqual(
            [
                PylintError("cms/urls.py", 1, "C0111", "missing-docstring", "", "Missing module docstring"),
                PylintError("cms/envs/test.py", 81, "C0301", "line-too-long", "", "Line too long (144/120)"),
                PylintError(
                    "test/test_amnesty.py",
                    78,
                    "C0326",
                    "bad-whitespace",
                    "",
                    "Exactly one space required around assignment",
                ),
            ],
            list(parse_pylint_output(pylint_output.split("\n"))),
        )

    def test_add_supression(self):
        input_code = """\
            def func(arg):
                pass
        """

        errors = [PylintError("test.py", 1, "W0613", "unused-argument", "func", "Unused argument 'arg'")]

        expected = """\
            def func(arg):  # lint-amnesty, pylint: disable=unused-argument
                pass
        """

        self.assert_amnesty(input_code, errors, expected)

    def test_add_multiple_suppressions_same_line(self):
        input_code = """\
            def func(arg, arg):
                pass
        """

        errors = [
            PylintError("test.py", 1, "W0613", "unused-argument", "func", "Unused argument 'arg'"),
            PylintError("test.py", 1, "E0108", "duplicate-argument-name", "func", "Duplicate argument name 'arg'"),
        ]

        expected = """\
            def func(arg, arg):  # lint-amnesty, pylint: disable=duplicate-argument-name, unused-argument
                pass
        """

        self.assert_amnesty(input_code, errors, expected)

    def test_add_to_existing_suppression(self):
        input_code = """\
            def func(arg, arg):  # pylint: disable=unused-argument
                pass
        """

        errors = [
            PylintError("test.py", 1, "E0108", "duplicate-argument-name", "func", "Duplicate argument name 'arg'")
        ]

        expected = """\
            def func(arg, arg):  # lint-amnesty, pylint: disable=duplicate-argument-name, unused-argument
                pass
        """

        self.assert_amnesty(input_code, errors, expected)

    def test_add_to_existing_suppression_trailing_comment(self):
        input_code = """\
            def func(arg, arg):  # pylint: disable=unused-argument  # noqa
                pass
        """

        errors = [
            PylintError("test.py", 1, "E0108", "duplicate-argument-name", "func", "Duplicate argument name 'arg'")
        ]

        expected = """\
            def func(arg, arg):  # lint-amnesty, pylint: disable=duplicate-argument-name, unused-argument  # noqa
                pass
        """

        self.assert_amnesty(input_code, errors, expected)

    def test_add_suppression_trailing_comment(self):
        input_code = """\
            def func(arg, arg):  # noqa
                pass
        """

        errors = [
            PylintError("test.py", 1, "E0108", "duplicate-argument-name", "func", "Duplicate argument name 'arg'")
        ]

        expected = """\
            def func(arg, arg):  # noqa  # lint-amnesty, pylint: disable=duplicate-argument-name
                pass
        """

        self.assert_amnesty(input_code, errors, expected)

    def test_remove_useless_suppression(self):
        input_code = """\
            def func(arg, arg):  # pylint: disable=unused-argument
                pass
        """

        errors = [
            PylintError(
                "test.py", 1, "I0021", "useless-suppression", "func", "Useless suppression of 'unused-argument'"
            )
        ]

        expected = """\
            def func(arg, arg):
                pass
        """

        self.assert_amnesty(input_code, errors, expected)

    def test_remove_useless_suppression_leave_rest(self):
        input_code = """\
            def func(arg, arg):  # pylint: disable=unused-argument, duplicate-argument-name
                pass
        """

        errors = [
            PylintError(
                "test.py", 1, "I0021", "useless-suppression", "func", "Useless suppression of 'unused-argument'"
            )
        ]

        expected = """\
            def func(arg, arg):  # pylint: disable=duplicate-argument-name
                pass
        """

        self.assert_amnesty(input_code, errors, expected)

    def test_replace_useless_suppression(self):
        input_code = """\
            def func(arg, arg):  # pylint: disable=unused-argument
                pass
        """

        errors = [
            PylintError(
                "test.py", 1, "I0021", "useless-suppression", "func", "Useless suppression of 'unused-argument'"
            ),
            PylintError("test.py", 1, "E0108", "duplicate-argument-name", "func", "Duplicate argument name 'arg'"),
        ]

        expected = """\
            def func(arg, arg):  # lint-amnesty, pylint: disable=duplicate-argument-name
                pass
        """

        self.assert_amnesty(input_code, errors, expected)

    def test_fix_multiple_lines(self):
        input_code = """\
            def func_a(arg):
                pass

            def func_b(arg, arg):
                pass
        """

        errors = [
            PylintError("test.py", 1, "W0613", "unused-argument", "func", "Unused argument 'arg'"),
            PylintError("test.py", 4, "E0108", "duplicate-argument-name", "func", "Duplicate argument name 'arg'"),
        ]

        expected = """\
            def func_a(arg):  # lint-amnesty, pylint: disable=unused-argument
                pass

            def func_b(arg, arg):  # lint-amnesty, pylint: disable=duplicate-argument-name
                pass
        """

        self.assert_amnesty(input_code, errors, expected)

    def test_fix_module_docstring_missing(self):
        input_code = """\
            def func(arg):
                pass
        """

        errors = [PylintError("test.py", 1, "C0111", "missing-docstring", "func", "Missing module docstring")]

        expected = """\
            # lint-amnesty, pylint: disable=missing-docstring
            def func(arg):
                pass
        """

        self.assert_amnesty(input_code, errors, expected)

    def test_fix_module_and_func_docstring_missing(self):
        input_code = """\
            def func(arg):
                pass
        """

        errors = [
            PylintError("test.py", 1, "C0111", "missing-docstring", "func", "Missing module docstring"),
            PylintError("test.py", 1, "C0111", "missing-docstring", "func", "Missing function docstring"),
        ]

        expected = """\
            # lint-amnesty, pylint: disable=missing-docstring
            def func(arg):  # lint-amnesty, pylint: disable=missing-docstring
                pass
        """

        self.assert_amnesty(input_code, errors, expected)

    def test_fix_many_errors(self):
        input_code = """\
            def foo(self, arg, blarg, flarg, blanth, notehu, onethu, psonatehu):
                pass"""

        errors = [
            PylintError("lint_test.py", 2, "C0304", "missing-final-newline", "", "Final newline missing"),
            PylintError("lint_test.py", 1, "C0111", "missing-docstring", "", "Missing module docstring"),
            PylintError("lint_test.py", 1, "C0102", "blacklisted-name", "foo", 'Black listed name "foo"'),
            PylintError("lint_test.py", 1, "C0111", "missing-docstring", "foo", "Missing function docstring"),
            PylintError("lint_test.py", 1, "W0613", "unused-argument", "foo", "Unused argument 'notehu'"),
            PylintError("lint_test.py", 1, "W0613", "unused-argument", "foo", "Unused argument 'self'"),
            PylintError("lint_test.py", 1, "W0613", "unused-argument", "foo", "Unused argument 'psonatehu'"),
            PylintError("lint_test.py", 1, "W0613", "unused-argument", "foo", "Unused argument 'blanth'"),
            PylintError("lint_test.py", 1, "W0613", "unused-argument", "foo", "Unused argument 'onethu'"),
            PylintError("lint_test.py", 1, "W0613", "unused-argument", "foo", "Unused argument 'arg'"),
            PylintError("lint_test.py", 1, "W0613", "unused-argument", "foo", "Unused argument 'blarg'"),
            PylintError("lint_test.py", 1, "W0613", "unused-argument", "foo", "Unused argument 'flarg'"),
        ]

        expected = """\
            # lint-amnesty, pylint: disable=missing-docstring
            def foo(self, arg, blarg, flarg, blanth, notehu, onethu, psonatehu):  # lint-amnesty, pylint: disable=blacklisted-name, missing-docstring, unused-argument
                pass  # lint-amnesty, pylint: disable=missing-final-newline"""

        self.assert_amnesty(input_code, errors, expected)
