# Spdx-FileCopyrightText: © 2023 Christian BUHTZ <c.buhtz@posteo.jp>
#
# SPDX-License-Identifier: GPL-3.0-only
#
# This file is part of the program "Hyperorg" which is released under GNU
# General Public License v3 (GPLv3).
# See folder LICENSES or go to <https://www.gnu.org/licenses/#GPL>.
"""Tests using several linters"""
import unittest
import os
import logging
import pathlib
import subprocess
from importlib import metadata
from typing import Iterable

# Not all linters follow strict PEP8
PEP8_MAX_LINE_LENGTH = 79


class MirrorMirrorOnTheWall(unittest.TestCase):
    """Check all py-files in the package incl. its test files if they are
    PEP8 compliant or don't smell and not having usual typos.
    """

    @classmethod
    def setUpClass(cls):
        cls.PACKAGE_NAME = 'hyperorg'
        cls.TEST_FOLDER_NAME = pathlib.Path(__file__).parent.name
        cls.collected_py_files \
            = cls._get_package_files() + cls._get_test_files()

    @classmethod
    def _get_test_files(cls) -> Iterable[pathlib.Path]:
        """Return list of Python files in the tests folder"""
        test_path = pathlib.Path.cwd() / cls.TEST_FOLDER_NAME

        return list(test_path.rglob('*.py'))

    @classmethod
    def _get_package_files(cls) -> Iterable[pathlib.Path]:
        """Return list of all py-files of the (editable) installed package."""

        # get all files in this package
        files = metadata.files(cls.PACKAGE_NAME)
        files = [f.locate() for f in files]

        # py-files only
        files = filter(
            lambda fp: fp.suffix == '.py'  # Python file
            and not fp.name.startswith('.#')  # no editor temp files
            and fp.parent.name is not cls.TEST_FOLDER_NAME,  # no test file
            files)

        return list(files)

    def test010_ruff(self):
        """Ruff in default mode."""

        # ATTENTIION: Some settings are found in pyproject.toml
        cmd = [
            'ruff',
            'check',
            '--line-length', str(PEP8_MAX_LINE_LENGTH),
            # Additionally activate subset of PyLint (PL)
            # and PyCodestyle (E, W) rules
            '--extend-select=PL,E,W',
            # But ignore...
            '--ignore='
            'PLR2004,'  # magic value in comparison
            'PLW2901',  # `for` loop variable `e` overwritten by
                        # assignment target
            # Ruff counting branches different from PyLint.
            # See: <https://www.reddit.com/r/learnpython/comments/
            #      1buojae/comment/kxu0mp3>
            '--config', 'pylint.max-branches=13',
            '--config', 'flake8-quotes.inline-quotes = "single"',
            # one error per line (no context lines)
            '--output-format=concise',
            '--quiet',
            '.',
        ]

        proc = subprocess.run(
            cmd,
            check=False,
            universal_newlines=True,
            capture_output=True
        )

        # No errors other then linter rules
        self.assertIn(proc.returncode, [0, 1], proc.stderr)

        error_n = len(proc.stdout.splitlines())
        if error_n > 0:
            print('\n' + proc.stdout)  # noqa: T201

        self.assertEqual(0, error_n, f'Ruff found {error_n} problem(s).')

        # any other errors?
        self.assertEqual(proc.stderr, '')

    def test020_flake8(self):
        """Flake8 in default mode."""
        cmd = [
            'flake8',
            f'--max-line-length={PEP8_MAX_LINE_LENGTH}',
            '--builtins=_,ngettext',
            # '--enable-extensions='
        ]

        cmd.extend(str(v) for v in self.collected_py_files)

        proc = subprocess.run(
            cmd,
            check=False,
            universal_newlines=True,
            capture_output=True
        )

        error_n = len(proc.stdout.splitlines())
        if error_n > 0:
            print('')
            print(proc.stdout)

        self.assertEqual(0, error_n, f'Flake8 found {error_n} problem(s).')

        # any other errors?
        self.assertEqual(proc.stderr, '')

    def test030_pylint(self):
        """Use Pylint to check for specific error codes.

        Some facts about PyLint
         - It is one of the slowest available linters.
         - It is able to catch lints none of the other linters
        """

        # Pylint base command
        cmd = [
            'pylint',
            # # Output format (default: text)
            # '--output-format=colorized',
            # Storing results in a pickle file is unnecessary
            '--persistent=n',
            # autodetec number of parallel jobs (pylint default)
            '--jobs=0',
            # Disable scoring  ("Your code has been rated at xx/10")
            '--score=n',
            # PEP8 conform line length (see PyLint Issue #3078)
            f'--max-line-length={PEP8_MAX_LINE_LENGTH}',
            # # prevent false-positive no-module-member errors
            # '--extension-pkg-whitelist='
            # 'pygments,pygments.formatters,pygments.formatters.html',
            # List of members which are set dynamically and missed by pylint
            # inference system, and so shouldn't trigger E1101 when accessed.
            '--generated-members=pygments.formatters.HtmlFormatter',
            # Allowlist variable names
            '--good-names=idx,fp,fn,maxDiff',
            # Allow fstrings when logging
            '--disable=logging-fstring-interpolation',
            # https://github.com/pylint-dev/pylint/issues/9547
            '--ignored-argument-names=_',
        ]

        # Add py files
        cmd.extend(str(v) for v in self.collected_py_files)

        # subprocess.run(cmd, check=True)
        proc = subprocess.run(
            cmd,
            check=False,
            universal_newlines=True,
            capture_output=True)

        # Count lines except module headings
        error_n = len(list(filter(
            lambda line: line[:2] not in ('**', '  ', '==', ' (', ''),
            proc.stdout.splitlines())))
        print(proc.stdout)  # noqa: T201

        if proc.returncode != 0:
            logging.error(proc.stdout)

        self.assertEqual(0, error_n, f'PyLint found {error_n} problem(s).')

        # any other errors?
        self.assertEqual(proc.stderr, '')

    def test040_codespell(self):
        """Codespell to check for common typos.

        Config can be found in pyproject.toml.
        """

        # cmd = [
        #     'codespell',
        #     # Skip these files
        #     '--skip',
        #     '.git,.pytest_cache,.ruff_cache,build,*.egg-info,'
        #     'docs,#*.py#,.venv,venv,./example/html',
        #     # Print N lines of surrounding context
        #     '--context', '1',
        #     # Check hidden files also
        #     '--check-hidden',
        #     # Dictionaries to use (default: "clear,rare"). Current: all.
        #     '--builtin',
        #     'clear,rare,informal,usage,code,names,en-GB_to_en-US',
        #     # Print number of errors as last line on stderr
        #     '--count',
        #     # Simulate "# noqa" and ignore all lines with
        #     # "# codespell-ignore" at the end.
        #     # Credits: https://github.com/codespell-project/codespell/
        #     #          issues/1212#issuecomment-1721152455
        #     '--ignore-regex', '.*# codespell-ignore$',
        #     # Allowed (ignored) words
        #     '--ignore-words-list=assertIn',
        #     # Allowed (ignored) words in URLs and URIs
        #     '--uri-ignore-words-list=master',
        #     '--enable-colors',
        # ]

        proc = subprocess.run(
            ['codespell'],
            check=False,
            universal_newlines=True,
            capture_output=True)

        error_n = int(proc.stderr)

        if error_n > 0:
            logging.error(proc.stdout)

        self.assertEqual(0, error_n, f'Codespell found {error_n} problem(s).')

    def test050_reuse_spdx(self):
        """Check REUSE Software and SPDX specifications"""
        env = os.environ
        env['LC_ALL'] = 'C'

        proc = subprocess.run(
            ['reuse', 'lint'],
            check=False,
            universal_newlines=True,
            env=env,
            capture_output=True)

        self.assertEqual(proc.returncode, 0, proc.stdout)
