# 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 os
import unittest
import pathlib
import subprocess
from importlib import metadata
from typing import Iterable
import pycodestyle

# 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."""
        cmd = [
            'ruff',
            'check',
            '--line-length', str(PEP8_MAX_LINE_LENGTH),
            '--quiet',
            '.',
        ]

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

        print(r.stdout)
        error_n = len(r.stdout.splitlines())
        self.assertEqual(0, error_n, f'Ruff found {error_n} problem(s).')

    def test020_pycodestyle(self):
        """PEP8 conformance via pycodestyle"""

        style = pycodestyle.StyleGuide(quite=True)
        result = style.check_files(self.collected_py_files)

        self.assertEqual(result.total_errors, 0,
                         f'pycodestyle found {result.total_errors} code '
                         'style error(s)/warning(s).')

    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,maxDiff',
            # Allow fstrings when logging
            '--disable=logging-fstring-interpolation',
        ]

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

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

        # Count lines except module headings
        error_n = len(list(filter(lambda line: not line.startswith('*****'),
                                  r.stdout.splitlines())))
        print(r.stdout)

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

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

        cmd = [
            'codespell',
            # Skip these files
            '--skip',
            '.git,.pytest_cache,.ruff_cache,build,*.egg-info,'
            'docs,#*.py#,.venv,venv',
            # 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=dum',
            # Allowed (ignored) words in URLs and URIs
            '--uri-ignore-words-list=master',
            '--enable-colors',
        ]

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

        error_n = int(r.stderr)
        if error_n > 0:
            print(r.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'

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

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