"""PassSuppressFormatter — a custom Behave formatter that suppresses output
for passing and skipped scenarios, reducing CI log noise from ~100,000 lines
to ≤ 10 lines for an all-passing suite.

This module is embedded in the same directory as ``run_behave_parallel.py``
so that ``_install_behave_parallel()`` in ``noxfile.py`` can copy both files
into the temporary ``behave_parallel`` package.
"""

from __future__ import annotations

import io
from typing import Any

from behave.formatter.base import (
    Formatter as _BehaveFormatter,  # type: ignore[import-untyped]
)

# Sentinel set of statuses whose output should be suppressed.
# Every status not in this set (e.g. "failed", "undefined") causes the
# buffered scenario output to be flushed to the real output stream.
_SUPPRESS_STATUSES: frozenset[str] = frozenset({"passed", "skipped"})


class PassSuppressFormatter(_BehaveFormatter):
    """Behave formatter that suppresses output for passing scenarios.

    All per-scenario output is buffered in a :class:`io.StringIO`.  When a
    scenario ends (signalled by the next :meth:`scenario` call or by
    :meth:`eof`):

    * **Failed / errored scenario** — the buffer is flushed to the real
      output stream so developers and CI tools see the full step-level
      details.
    * **Passed / skipped scenario** — the buffer is silently discarded.

    The result for an all-passing suite is zero formatter output, leaving
    only the ``_print_overall_summary`` block emitted by the runner as
    visible output (~5-10 lines regardless of suite size).

    Coverage mode (``BEHAVE_PARALLEL_COVERAGE=1``) bypasses this formatter
    entirely via ``_make_runner()``, which falls back to behave's default
    format so that slipcover can instrument a single sequential process
    without output interference.
    """

    name: str = "pass_suppress"
    description: str = "Suppress passing scenario output; show failures fully"

    def __init__(self, stream_opener: Any, config: Any) -> None:
        super().__init__(stream_opener, config)
        # Ensure the real output stream is open (opens it if stream_opener
        # was constructed with only a filename, not a pre-opened stream).
        self.stream = self.open()

        # Per-scenario buffering state.
        self._current_scenario: Any = None
        self._scenario_buf: io.StringIO = io.StringIO()

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _finalize_previous_scenario(self) -> None:
        """Flush the scenario buffer to the real stream if scenario failed.

        Called at the start of each new scenario and at end-of-feature so
        the previous scenario's buffered output is either committed or
        discarded based on the scenario's final status.
        """
        if self._current_scenario is None:
            return
        status: Any = getattr(self._current_scenario, "status", None)
        status_name: str = (
            getattr(status, "name", str(status)) if status is not None else ""
        )
        if status_name not in _SUPPRESS_STATUSES:
            output = self._scenario_buf.getvalue()
            if output:
                self.stream.write(output)
                self.stream.flush()
        # Reset the buffer regardless — the next scenario starts fresh.
        self._scenario_buf = io.StringIO()

    # ------------------------------------------------------------------
    # Formatter interface (behave lifecycle hooks)
    # ------------------------------------------------------------------

    def uri(self, uri: str) -> None:
        """Called before each feature file; no output needed."""

    def feature(self, feature: Any) -> None:
        """Called at feature start; no output needed."""

    def background(self, background: Any) -> None:
        """Called when a feature background is declared; no output needed."""

    def scenario(self, scenario: Any) -> None:
        """Start buffering output for *scenario*, finalising the previous one."""
        self._finalize_previous_scenario()
        self._current_scenario = scenario
        # Write the scenario header into the new buffer.
        self._scenario_buf.write(f"\n  {scenario.keyword}: {scenario.name}\n")

    def step(self, step: Any) -> None:
        """Step announced before execution; no output needed at this point."""

    def match(self, match: Any) -> None:
        """Step matched; no output needed."""

    def result(self, step: Any) -> None:
        """Record a step result in the per-scenario buffer."""
        status: Any = getattr(step, "status", None)
        status_name: str = (
            getattr(status, "name", "unknown") if status is not None else "unknown"
        )
        self._scenario_buf.write(f"    {step.keyword} {step.name} ... {status_name}\n")
        error_msg: str | None = getattr(step, "error_message", None)
        if error_msg:
            self._scenario_buf.write(f"{error_msg}\n")

    def eof(self) -> None:
        """End of feature file: finalise the current (last) scenario."""
        self._finalize_previous_scenario()
        self._current_scenario = None
