#!/usr/bin/python3
# Copyright (c) 2024- Stuart Prescott <stuart@debian.org>
# Released under the same terms as QtPy
"""
Check the build or autopkgtest log to find tests that were skipped that
should not be skipped.

Usage:
    ./debian/test-log-check ../python-qtpy_*.build
"""

from __future__ import annotations

from collections import defaultdict
from dataclasses import dataclass, field
from enum import Enum
import re
import shutil
import sys
from typing import Generator

# pylint: disable=invalid-name,missing-function-docstring


@dataclass
class Ignorables:
    """Class to track ignorable regexes in the test log"""

    ignores: list[str | re.Pattern[str]] = field(default_factory=list)

    def patterns(self) -> Generator[re.Pattern[str], None, None]:
        for pattern in self.ignores:
            if isinstance(pattern, str):
                yield re.compile(pattern)
            else:
                yield pattern


@dataclass
class PyTestMessage:
    """Class to record test errors/skips"""

    interpreter: str
    qt: str
    line: int
    entry: str


class LogState(Enum):
    """State of the log parser"""

    waiting_for_session_start = 0
    waiting_for_interpreter = 1
    waiting_for_session_info = 2
    waiting_for_session_summary = 3
    waiting_for_session_end = 4

    def __next__(self) -> LogState:
        return LogState((self.value + 1) % len(LogState))


session_start_re = re.compile(r"=+ test session starts =+")
session_interpreter_re = re.compile(r"platform linux -- Python ([\d.]+),")
session_info_re = re.compile(r"((pyside|pyqt)\d) ", re.I)
session_summary_re = re.compile(r"=+ short test summary info =+")
error_line_re = re.compile(r"(SKIPPED|ERROR|FAIL)")
session_end_re = re.compile(
    r"=+ (.*\d+ failed.*, )?.* passed.*, .*\d+ skipped.* in .* =+"
)


ignorables = defaultdict(Ignorables)

ignorables["PyQt5"] = Ignorables(
    [
        # Tests not for this QT_API
        "Requires PySide2",
        "Requires PyQt6",
        "Requires PySide6",
        "It is not currently implemented in PyQt5",
        "Doesn't seem to be present on PyQt5",
        "Only available by default in PySide",
        "PySide.*specific test",
        "Not available in PySide2/PyQt5",
        "Only available in Qt6 bindings",
        # Tests not for this platform
        "qtmacextras.py",
        "pip on Windows",
        # Tests needing unavailable components
        "It must be installed separately",
        "No module named 'PyQt5.QtQuick3D'",  # seems to be missing in Debian?
    ]
)

ignorables["PyQt6"] = Ignorables(
    [
        # Tests not for this QT_API
        "Requires PyQt5",
        "Requires PySide6",
        "Requires PySide2",
        "Targeted to PyQt5",
        "Not complete in PyQt6",
        "It is not currently implemented in PyQt6",
        "Doesn't seem to be present on .*PyQt6",
        "Only available by default in PySide",
        "PySide.*specific test",
        "does not exist in Qt6",
        "Only available in Qt5 bindings",
        "not available with qt 6.0",
        # Tests needing unavailable components
        "It must be installed separately",
    ]
)


ignorables["PySide6"] = Ignorables(
    [
        # Tests not for this QT_API
        "Requires PyQt5",
        "Requires PyQt6",
        "Requires PySide2",
        "Targeted to PyQt5",
        "Not complete in PySide6",
        "It is not currently implemented in PySide6",
        "not available under PySide 2/6",
        "does not exist in Qt6",
        "Only available in Qt5 bindings",
        "not available with qt 6.0",
        # Tests needing unavailable components
        "It must be installed separately",
        "No module named 'PySide6.QtAxContainer'",  # seems to be missing in Debian?
    ]
)


def process_log(filename: str) -> list[PyTestMessage]:
    """Process the log file to find the test output"""

    with open(filename, encoding="UTF-8") as logfh:
        status = LogState.waiting_for_session_start
        interpreter = ""
        qt = ""
        messages = []

        for lineno, line in enumerate(logfh.readlines()):
            if status is LogState.waiting_for_session_start:
                if session_start_re.search(line):
                    status = next(status)
                continue
            if status is LogState.waiting_for_interpreter:
                if m := session_interpreter_re.search(line):
                    interpreter = m.group(1)
                    status = next(status)
                continue
            if status is LogState.waiting_for_session_info:
                if m := session_info_re.search(line):
                    qt = m.group(1)
                    status = next(status)
                continue
            if status is LogState.waiting_for_session_summary:
                if m := session_summary_re.search(line):
                    status = next(status)
                continue
            if status is LogState.waiting_for_session_end:
                if m := error_line_re.search(line):
                    messages.append(
                        PyTestMessage(interpreter, qt, lineno, line)
                    )
                if session_end_re.search(line):
                    status = next(status)
                continue

    return messages


def print_report(messages: list[PyTestMessage]) -> None:
    """Report the log lines that were identified"""

    interpreter = ""
    qt = ""
    width, _ = shutil.get_terminal_size(fallback=(50, 1))

    for rep in messages:
        if interpreter != rep.interpreter or qt != rep.qt:
            interpreter = rep.interpreter
            qt = rep.qt
            header = f" {interpreter} {qt} "
            print(f"\n\n{header:=^{width}}")

        skip = False
        for p in ignorables[rep.qt].patterns():
            if p.search(rep.entry):
                skip = True
                break
        if not skip:
            print(rep.entry, end="")


if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage: test-log-check foo.build")
        sys.exit(1)

    logfile = sys.argv[1]

    reports = process_log(logfile)
    print_report(reports)
