# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt

"""Functional/non regression tests for pylint."""

from __future__ import annotations

import re
import sys
from os.path import abspath, dirname, join

import pytest

from pylint.testutils import UPDATE_FILE, UPDATE_OPTION, _get_tests_info, linter
from pylint.testutils.reporter_for_tests import GenericTestReporter
from pylint.testutils.utils import _test_cwd

TESTS_DIR = dirname(abspath(__file__))
INPUT_DIR = join(TESTS_DIR, "input")
MSG_DIR = join(TESTS_DIR, "messages")


FILTER_RGX = None
INFO_TEST_RGX = re.compile(r"^func_i\d\d\d\d$")


def exception_str(
    self: Exception, ex: Exception  # pylint: disable=unused-argument
) -> str:
    """Function used to replace default __str__ method of exception instances
    This function is not typed because it is legacy code.
    """
    return f"in {ex.file}\n:: {', '.join(ex.args)}"  # type: ignore[attr-defined] # Defined in the caller


class LintTestUsingModule:
    INPUT_DIR: str | None = None
    DEFAULT_PACKAGE = "input"
    package = DEFAULT_PACKAGE
    linter = linter
    module: str | None = None
    depends: list[tuple[str, str]] | None = None
    output: str | None = None

    def _test_functionality(self) -> None:
        tocheck = [self.package + "." + self.module] if self.module else []
        if self.depends:
            tocheck += [
                self.package + f".{name.replace('.py', '')}" for name, _ in self.depends
            ]
        # given that TESTS_DIR could be treated as a namespace package
        # when under the current directory, cd to it so that "tests." is not
        # prepended to module names in the output of cyclic-import
        with _test_cwd(TESTS_DIR):
            self._test(tocheck)

    def _check_result(self, got: str) -> None:
        error_msg = (
            f"Wrong output for '{self.output}':\n"
            "You can update the expected output automatically with: '"
            f"python tests/test_func.py {UPDATE_OPTION}'\n\n"
        )
        assert self._get_expected() == got, error_msg

    def _test(self, tocheck: list[str]) -> None:
        if self.module and INFO_TEST_RGX.match(self.module):
            self.linter.enable("I")
        else:
            self.linter.disable("I")
        try:
            self.linter.check(tocheck)
        except Exception as ex:
            print(f"Exception: {ex} in {tocheck}:: {', '.join(ex.args)}")
            # This is legacy code we're trying to remove, not worth it to type correctly
            ex.file = tocheck  # type: ignore[attr-defined]
            print(ex)
            # This is legacy code we're trying to remove, not worth it to type correctly
            ex.__str__ = exception_str  # type: ignore[assignment]
            raise
        assert isinstance(self.linter.reporter, GenericTestReporter)
        self._check_result(self.linter.reporter.finalize())

    def _has_output(self) -> bool:
        return isinstance(self.module, str) and not self.module.startswith(
            "func_noerror_"
        )

    def _get_expected(self) -> str:
        if self._has_output() and self.output:
            with open(self.output, encoding="utf-8") as fobj:
                return fobj.read().strip() + "\n"
        else:
            return ""


class LintTestUpdate(LintTestUsingModule):
    def _check_result(self, got: str) -> None:
        if not self._has_output():
            return
        try:
            expected = self._get_expected()
        except OSError:
            expected = ""
        if got != expected:
            with open(self.output or "", "w", encoding="utf-8") as f:
                f.write(got)


def gen_tests(
    filter_rgx: str | re.Pattern[str] | None,
) -> list[tuple[str, str, list[tuple[str, str]]]]:
    if filter_rgx:
        is_to_run = re.compile(filter_rgx).search
    else:
        is_to_run = (  # noqa: E731, We're going to throw all this anyway
            lambda x: 1  # type: ignore[assignment] # pylint: disable=unnecessary-lambda-assignment
        )
    tests: list[tuple[str, str, list[tuple[str, str]]]] = []
    for module_file, messages_file in _get_tests_info(INPUT_DIR, MSG_DIR, "func_", ""):
        if not is_to_run(module_file) or module_file.endswith((".pyc", "$py.class")):
            continue
        base = module_file.replace(".py", "").split("_")[1]
        dependencies = _get_tests_info(INPUT_DIR, MSG_DIR, base, ".py")
        tests.append((module_file, messages_file, dependencies))
    if UPDATE_FILE.exists():
        return tests
    assert len(tests) < 13, "Please do not add new test cases here." + "\n".join(
        str(k) for k in tests if not k[2]
    )
    return tests


TEST_WITH_EXPECTED_DEPRECATION = ["func_excess_escapes.py"]


@pytest.mark.parametrize(
    "module_file,messages_file,dependencies",
    gen_tests(FILTER_RGX),
    ids=[o[0] for o in gen_tests(FILTER_RGX)],
)
def test_functionality(
    module_file: str,
    messages_file: str,
    dependencies: list[tuple[str, str]],
    recwarn: pytest.WarningsRecorder,
) -> None:
    __test_functionality(module_file, messages_file, dependencies)
    if recwarn.list:
        if module_file in TEST_WITH_EXPECTED_DEPRECATION and sys.version_info.minor > 5:
            assert any(
                "invalid escape sequence" in str(i.message)
                for i in recwarn.list
                if issubclass(i.category, DeprecationWarning)
            )


def __test_functionality(
    module_file: str, messages_file: str, dependencies: list[tuple[str, str]]
) -> None:
    lint_test = LintTestUpdate() if UPDATE_FILE.exists() else LintTestUsingModule()
    lint_test.module = module_file.replace(".py", "")
    lint_test.output = messages_file
    lint_test.depends = dependencies or None
    lint_test.INPUT_DIR = INPUT_DIR
    lint_test._test_functionality()


if __name__ == "__main__":
    if UPDATE_OPTION in sys.argv:
        UPDATE_FILE.touch()
        sys.argv.remove(UPDATE_OPTION)
    if len(sys.argv) > 1:
        FILTER_RGX = sys.argv[1]
        del sys.argv[1]
    try:
        pytest.main(sys.argv)
    finally:
        if UPDATE_FILE.exists():
            UPDATE_FILE.unlink()
