# 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

from __future__ import annotations

import os
from collections.abc import Iterator
from pathlib import Path

from pylint.testutils.functional.test_file import FunctionalTestFile

REASONABLY_DISPLAYABLE_VERTICALLY = 40
"""'Wet finger' number of python files that are reasonable to have in a functional test
directory.

'Wet finger' as in 'in my settings there are precisely this many'.
Initially the total number of files then we started counting only the python files to
avoid moving a lot of files.
"""

IGNORED_PARENT_DIRS = {
    "deprecated_relative_import",
    "ext",
    "regression",
    "regression_02",
    "regression_03",
    "used_02",
}
"""Direct parent directories that should be ignored."""

IGNORED_PARENT_PARENT_DIRS = {
    "docparams",
    "deprecated_relative_import",
    "ext",
}
"""Parents of direct parent directories that should be ignored."""


def get_functional_test_files_from_directory(
    input_dir: Path | str,
    max_file_per_directory: int = REASONABLY_DISPLAYABLE_VERTICALLY,
) -> list[FunctionalTestFile]:
    """Get all functional tests in the input_dir."""
    suite = []

    _check_functional_tests_structure(Path(input_dir), max_file_per_directory)

    for dirpath, dirnames, filenames in os.walk(input_dir):
        if dirpath.endswith("__pycache__"):
            continue
        dirnames.sort()
        filenames.sort()
        for filename in filenames:
            if filename != "__init__.py" and filename.endswith(".py"):
                suite.append(FunctionalTestFile(dirpath, filename))
    return suite


def _check_functional_tests_structure(
    directory: Path, max_file_per_directory: int
) -> None:
    """Check if test directories follow correct file/folder structure.

    Ignore underscored directories or files.
    """
    if Path(directory).stem.startswith("_"):
        return

    files: set[Path] = set()
    dirs: set[Path] = set()

    def _get_files_from_dir(
        path: Path, violations: list[tuple[Path, int]]
    ) -> list[Path]:
        """Return directories and files from a directory and handles violations."""
        files_without_leading_underscore = list(
            p
            for p in path.iterdir()
            if not (p.stem.startswith("_") or (p.is_file() and p.suffix != ".py"))
        )
        if len(files_without_leading_underscore) > max_file_per_directory:
            violations.append((path, len(files_without_leading_underscore)))
        return files_without_leading_underscore

    def walk(path: Path) -> Iterator[Path]:
        violations: list[tuple[Path, int]] = []
        violations_msgs: set[str] = set()
        parent_dir_files = _get_files_from_dir(path, violations)
        error_msg = (
            "The following directory contains too many functional tests files:\n"
        )
        for _file_or_dir in parent_dir_files:
            if _file_or_dir.is_dir():
                yield _file_or_dir.resolve()
                try:
                    yield from walk(_file_or_dir)
                except AssertionError as e:
                    violations_msgs.add(str(e).replace(error_msg, ""))
            else:
                yield _file_or_dir.resolve()
        if violations or violations_msgs:
            _msg = error_msg
            for offending_file, number in violations:
                _msg += f"- {offending_file}: {number} when the max is {max_file_per_directory}\n"
            for error_msg in violations_msgs:
                _msg += error_msg
            raise AssertionError(_msg)

    # Collect all sub-directories and files in directory
    for file_or_dir in walk(directory):
        if file_or_dir.is_dir():
            dirs.add(file_or_dir)
        elif file_or_dir.suffix == ".py":
            files.add(file_or_dir)

    directory_does_not_exists: list[tuple[Path, Path]] = []
    misplaced_file: list[Path] = []
    for file in files:
        possible_dir = file.parent / file.stem.split("_")[0]
        if possible_dir.exists():
            directory_does_not_exists.append((file, possible_dir))
        # Exclude some directories as they follow a different structure
        if (
            not len(file.parent.stem) == 1  # First letter sub-directories
            and file.parent.stem not in IGNORED_PARENT_DIRS
            and file.parent.parent.stem not in IGNORED_PARENT_PARENT_DIRS
        ):
            if not file.stem.startswith(file.parent.stem):
                misplaced_file.append(file)

    if directory_does_not_exists or misplaced_file:
        msg = "The following functional tests are disorganized:\n"
        for file, possible_dir in directory_does_not_exists:
            msg += (
                f"- In '{directory}', '{file.relative_to(directory)}' "
                f"should go in '{possible_dir.relative_to(directory)}'\n"
            )
        for file in misplaced_file:
            msg += (
                f"- In '{directory}', {file.relative_to(directory)} should go in a directory"
                f" that starts with the first letters"
                f" of '{file.stem}' (not '{file.parent.stem}')\n"
            )
        raise AssertionError(msg)
