# Copyright (C) 2026 GoodData Corporation

"""Streaming-oriented pytest output filtering."""

from __future__ import annotations

import re
from dataclasses import dataclass, field

_SUMMARY_LINE_RE = re.compile(
    r"(^=+ )?(?:\d+\s+\w+(?:,\s*)?)+(?:\s+in\s+\d+(?:\.\d+)?s)?(?:\s*=+)?$",
    re.IGNORECASE,
)
_SECTION_LINE_RE = re.compile(r"^=+\s*(?P<section>[^=]+?)\s*=+$")
_FAILED_TEST_RE = re.compile(r"^FAILED\s+(?P<name>\S+)")
_LOCATION_LINE_RE = re.compile(r"^[\w./\\-]+\.py:\d+:")
_TEST_PROGRESS_RE = re.compile(
    r"^(?P<nodeid>\S+?(?:\.py(?:::\S+)?)?)\s+"
    r"(?P<status>PASSED|FAILED|ERROR|SKIPPED|XFAIL|XPASS)\b"
    r"(?:.*\[\s*\d+%\])?$",
    re.IGNORECASE,
)
_GENERIC_ERROR_RE = re.compile(r"\b(failed|error|errors|traceback|assertionerror|exception)\b", re.IGNORECASE)
_PYTEST_HINT_RE = re.compile(r"\b(pytest|collected\s+\d+\s+item|short test summary info)\b", re.IGNORECASE)


@dataclass(frozen=True, slots=True)
class StreamDecision:
    important: bool
    error: bool


@dataclass(slots=True)
class PytestStreamFilter:
    """Classify live command output into important vs noise."""

    section: str | None = None
    last_summary_line: str | None = None
    failed_tests: list[str] = field(default_factory=list)
    saw_pytest_signal: bool = False

    def classify(self, line: str) -> StreamDecision:
        stripped = line.strip()
        if not stripped:
            return StreamDecision(important=False, error=False)

        section_match = _SECTION_LINE_RE.match(stripped)
        if section_match:
            raw_section = section_match.group("section").strip().lower()
            self.section = raw_section
            if "failures" in raw_section or "errors" in raw_section or "short test summary info" in raw_section:
                self.saw_pytest_signal = True
                return StreamDecision(important=True, error=True)
            return StreamDecision(important=False, error=False)

        if _PYTEST_HINT_RE.search(stripped):
            self.saw_pytest_signal = True

        if _SUMMARY_LINE_RE.match(stripped) and any(
            token in stripped.lower()
            for token in ("passed", "failed", "error", "errors", "skipped", "xfailed", "xpassed")
        ):
            self.last_summary_line = stripped
            self.section = None
            return StreamDecision(
                important=True,
                error=("failed" in stripped.lower() or "error" in stripped.lower()),
            )

        failed_match = _FAILED_TEST_RE.match(stripped)
        if failed_match:
            self.saw_pytest_signal = True
            test_name = failed_match.group("name")
            if test_name not in self.failed_tests:
                self.failed_tests.append(test_name)
            return StreamDecision(important=True, error=True)

        progress_match = _TEST_PROGRESS_RE.match(stripped)
        if progress_match:
            self.saw_pytest_signal = True
            status = progress_match.group("status").upper()
            if status in {"FAILED", "ERROR"}:
                test_name = progress_match.group("nodeid")
                if test_name not in self.failed_tests:
                    self.failed_tests.append(test_name)
            return StreamDecision(important=True, error=(status in {"FAILED", "ERROR"}))

        if stripped.startswith("E   "):
            return StreamDecision(important=True, error=True)

        if _LOCATION_LINE_RE.match(stripped) and self.section is not None:
            return StreamDecision(important=True, error=("failures" in self.section or "errors" in self.section))

        if self.section is not None and (
            "failures" in self.section or "errors" in self.section or "short test summary info" in self.section
        ):
            return StreamDecision(important=True, error=True)

        if stripped.lower().startswith("collected "):
            self.saw_pytest_signal = True
            return StreamDecision(important=True, error=False)

        if _GENERIC_ERROR_RE.search(stripped):
            return StreamDecision(important=True, error=True)

        return StreamDecision(important=False, error=False)
