# SPDX-FileCopyrightText: 2013-2026 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only

"""Internal functions for test finding and setup."""


import logging
import operator
import os
import re
import sys
from collections.abc import Callable, Iterable
from typing import Any, Self


__all__ = [
    'LOG_BASE',
    'TEST_BASE',
    'UCSVersion',
    'get_sections',
    'get_tests',
    'setup_debug',
    'setup_environment',
    'strip_indent',
]

TEST_BASE = os.environ.get('UCS_TESTS', '/usr/share/ucs-test')
RE_SECTION = re.compile(r'^[0-9]{2}_(.+)$')
RE_PREFIX = re.compile(r'^[0-9]{2,3}_?(.+)')
RE_SUFFIX = re.compile(r'(?:~|\.(?:lib|sh|py[co]|bak|mo|po|png|jpg|jpeg|xml|csv|inst|uinst))$')
LOG_BASE = '/var/log/univention/test_%d.log'
S4CONNECTOR_INIT_SCRIPT = '/etc/init.d/univention-s4-connector'
INF = sys.maxsize


def setup_environment() -> None:
    """Setup runtime environment."""
    os.environ['TESTLIBPATH'] = '/usr/share/ucs-test/lib'
    os.environ['PYTHONUNBUFFERED'] = '1'


def setup_debug(level: int) -> None:
    """Setup Python logging."""
    level = _TAB.get(level, logging.DEBUG)
    FORMAT = '%(asctime)-15s ' + logging.BASIC_FORMAT
    logging.basicConfig(stream=sys.stderr, level=level, format=FORMAT)


_TAB = {  # pylint: disable-msg=W0612
    None: logging.WARNING,
    0: logging.WARNING,
    1: logging.INFO,
    2: logging.DEBUG,
}


def strip_indent(text: str) -> str:
    """Strip common indent."""
    lines = text.splitlines()
    while lines and not lines[0].strip():
        del lines[0]
    while lines and not lines[-1].strip():
        del lines[-1]
    indent = min(len(line) - len(line.lstrip()) for line in lines if line.lstrip())
    return '\n'.join(line[indent:] for line in lines)


def get_sections() -> dict[str, str]:
    """Return dictionary section-name -> section-directory."""
    section_dirs = os.listdir(TEST_BASE)
    sections = {dirname[3:]: TEST_BASE + os.path.sep + dirname for dirname in section_dirs if RE_SECTION.match(dirname)}
    return sections


def get_tests(sections: Iterable[str]) -> dict[str, list[str]]:
    """Return dictionary of section -> [filenames]."""
    result = {}
    logger = logging.getLogger('test.find')

    all_sections = get_sections()

    for section in sections:
        dirname = all_sections[section]
        logger.debug("Processing directory %s", dirname)
        tests = []

        files = os.listdir(dirname)
        for filename in sorted(files):
            fname = os.path.join(dirname, filename)
            if not RE_PREFIX.match(filename):
                logger.debug("Skipped file %s", fname)
                continue
            if RE_SUFFIX.search(filename):
                logger.debug("Skipped file %s", fname)
                continue
            if not os.path.exists(fname):
                logger.debug("Skipped file %s", fname)
                continue
            logger.debug("Adding file %s", fname)
            tests.append(fname)

        if tests:
            result[section] = tests
    return result


class UCSVersion:  # pylint: disable-msg=R0903  # noqa: PLW1641
    """
    UCS version.

    >>> UCSVersion("1.0") < UCSVersion("2.0")
    True
    >>> UCSVersion("1.0") < UCSVersion("1.0")
    False
    >>> UCSVersion("1.0") <= UCSVersion("1.0")
    True
    >>> UCSVersion("2.0") <= UCSVersion("1.0")
    False
    >>> UCSVersion("1.0") == UCSVersion("1.0")
    True
    >>> UCSVersion("1.0") == UCSVersion("2.0")
    False
    >>> UCSVersion("1.0") != UCSVersion("2.0")
    True
    >>> UCSVersion("1.0") != UCSVersion("1.0")
    False
    >>> UCSVersion("1.0") >= UCSVersion("1.0")
    True
    >>> UCSVersion("1.0") >= UCSVersion("2.0")
    False
    >>> UCSVersion("2.0") > UCSVersion("1.0")
    True
    >>> UCSVersion("1.0") > UCSVersion("1.0")
    False
    >>> UCSVersion("1.0") == UCSVersion((1, 0, INF, INF))
    True
    >>> UCSVersion("1.0-0-0") == UCSVersion((1, 0, 0, 0))
    True
    >>> UCSVersion("")
    Traceback (most recent call last):
        ...
    ValueError: Version does not match: ""
    >>> UCSVersion("0")
    Traceback (most recent call last):
        ...
    ValueError: Version does not match: "0"
    >>> UCSVersion("1")
    Traceback (most recent call last):
        ...
    ValueError: Version does not match: "1"
    >>> UCSVersion("1.2")
    UCSVersion('=1.2')
    >>> UCSVersion("1.2-3")
    UCSVersion('=1.2-3')
    >>> UCSVersion("1.2-3-4")
    UCSVersion('=1.2-3-4')
    >>> UCSVersion("1.2-3-4-5")
    Traceback (most recent call last):
        ...
    ValueError: Version does not match: "1.2-3-4-5"
    >>> UCSVersion(None)
    Traceback (most recent call last):
        ...
    TypeError: None
    >>> UCSVersion(1)
    Traceback (most recent call last):
        ...
    TypeError: 1
    >>> UCSVersion(1.5)
    Traceback (most recent call last):
        ...
    TypeError: 1.5
    """

    RE_VERSION = re.compile(r"^(<|<<|<=|=|==|>=|>|>>)?([1-9][0-9]*)\.([0-9]+)(?:-([0-9]*)(?:-([0-9]+))?)?$")

    @classmethod
    def _parse(cls, ver: str, default_op: str = '=') -> tuple[Callable[[Any, Any], Any], tuple[int, int, int, int]]:
        """
        Parse UCS-version range and return two-tuple (operator, version)
        >>> UCSVersion._parse('11.22')  # doctest: +ELLIPSIS
        (<built-in function eq>, (11, 22, ..., ...))
        >>> UCSVersion._parse('11.22-33')  # doctest: +ELLIPSIS
        (<built-in function eq>, (11, 22, 33, ...))
        >>> UCSVersion._parse('11.22-33-44')
        (<built-in function eq>, (11, 22, 33, 44))
        >>> UCSVersion._parse('<1.2-3')  # doctest: +ELLIPSIS
        (<built-in function lt>, (1, 2, 3, ...))
        >>> UCSVersion._parse('<<1.2-3')  # doctest: +ELLIPSIS
        (<built-in function lt>, (1, 2, 3, ...))
        >>> UCSVersion._parse('<=1.2-3')  # doctest: +ELLIPSIS
        (<built-in function le>, (1, 2, 3, ...))
        >>> UCSVersion._parse('=1.2-3')  # doctest: +ELLIPSIS
        (<built-in function eq>, (1, 2, 3, ...))
        >>> UCSVersion._parse('==1.2-3')  # doctest: +ELLIPSIS
        (<built-in function eq>, (1, 2, 3, ...))
        >>> UCSVersion._parse('>=1.2-3')  # doctest: +ELLIPSIS
        (<built-in function ge>, (1, 2, 3, ...))
        >>> UCSVersion._parse('>>1.2-3')  # doctest: +ELLIPSIS
        (<built-in function gt>, (1, 2, 3, ...))
        >>> UCSVersion._parse('>1.2-3')  # doctest: +ELLIPSIS
        (<built-in function gt>, (1, 2, 3, ...))
        """
        match = cls.RE_VERSION.match(ver)
        if not match:
            raise ValueError(f'Version does not match: "{ver}"')
        rel = match.group(1) or default_op
        parts: tuple[int, int, int, int] = tuple(int(_) if _ else INF for _ in match.groups()[1:])  # type: ignore
        if rel in ('<', '<<'):
            return (operator.lt, parts)
        if rel in ('<=',):
            return (operator.le, parts)
        if rel in ('=', '=='):
            return (operator.eq, parts)
        if rel in ('>=',):
            return (operator.ge, parts)
        if rel in ('>', '>>'):
            return (operator.gt, parts)
        raise ValueError(f'Unknown version match: "{ver}"')

    def __init__(self, ver: str | tuple[int, int, int, int]) -> None:
        if isinstance(ver, str):
            self.rel, self.ver = self._parse(ver)
        elif isinstance(ver, tuple):
            self.rel = operator.eq
            assert all(isinstance(_, int) for _ in ver)
            self.ver = ver
        else:
            raise TypeError(ver)

    def __str__(self) -> str:
        rel = {
            operator.lt: '<',
            operator.le: '<=',
            operator.eq: '=',
            operator.ge: '>=',
            operator.gt: '>',
        }[self.rel]
        ver = '%d.%d' % self.ver[0:2]  # type: ignore
        skipped = 0
        for part in self.ver[2:]:
            skipped += 1
            if part != INF:
                ver += '%s%d' % ('-' * skipped, part)
                skipped = 0
        return f'{rel}{ver}'

    def __repr__(self) -> str:
        return f'{self.__class__.__name__}({self.__str__()!r})'

    def __lt__(self, other: object) -> object:
        return self.ver < other.ver if isinstance(other, UCSVersion) else NotImplemented

    def __le__(self, other: object) -> object:
        return self.ver <= other.ver if isinstance(other, UCSVersion) else NotImplemented

    def __eq__(self, other: object) -> bool:
        return self.ver == other.ver if isinstance(other, UCSVersion) else False

    def __ne__(self, other: object) -> bool:
        return self.ver != other.ver if isinstance(other, UCSVersion) else False

    def __ge__(self, other: object) -> object:
        return self.ver >= other.ver if isinstance(other, UCSVersion) else NotImplemented

    def __gt__(self, other: object) -> object:
        return self.ver > other.ver if isinstance(other, UCSVersion) else NotImplemented

    def match(self, other: Self) -> bool:
        """
        Check if other matches the criterion.
        >>> UCSVersion('>1.2-3').match(UCSVersion('1.2-4'))
        True
        >>> UCSVersion('>1.2-3').match(UCSVersion('1.2-3-4'))
        False
        >>> UCSVersion('>1.2-3-5').match(UCSVersion('1.2-3-4'))
        False
        >>> UCSVersion('>=1.2-3').match(UCSVersion('1.2-3-4'))
        True
        """
        parts = [
            (other_ver, self_ver)
            for self_ver, other_ver in zip(self.ver, other.ver)
            if INF not in (self_ver, other_ver)
        ]
        return self.rel(*zip(*parts))  # pylint: disable-msg=W0142


if __name__ == '__main__':
    import doctest
    doctest.testmod()
