"""Helper for behave_parallel_log_filtering.robot integration tests.

Each function exercises a specific aspect of the parallel runner's
conditional log replay logic and prints a sentinel on success.
"""

from __future__ import annotations

import importlib.util
import io
import sys
from contextlib import redirect_stdout
from pathlib import Path
from types import ModuleType


def _load_runner_module() -> ModuleType:
    """Load scripts/run_behave_parallel.py as a module via importlib.

    **CWD requirement**: This function resolves the script path relative to
    the current working directory (``Path("scripts") / "run_behave_parallel.py"``).
    It must be called from the repository root (i.e. the directory that
    contains the ``scripts/`` folder).  The Robot integration tests satisfy
    this by passing ``cwd=${WORKSPACE}`` to ``Run Process``.  If you call
    this helper from a different directory, either ``cd`` to the repo root
    first or modify the path to be anchored relative to ``__file__``.
    """
    script_path = Path("scripts") / "run_behave_parallel.py"
    # Ensure the scripts/ directory is on sys.path so that
    # ``run_behave_parallel.py`` can ``from behave_pass_suppress_formatter
    # import PassSuppressFormatter``.  This is necessary when the helper is
    # invoked outside of a nox session (e.g. integration tests), where the
    # behave_parallel package created by noxfile.py for unit_tests is not
    # available.
    scripts_dir = str(script_path.parent.resolve())
    if scripts_dir not in sys.path:
        sys.path.insert(0, scripts_dir)
    spec = importlib.util.spec_from_file_location(
        "run_behave_parallel", str(script_path)
    )
    if spec is None or spec.loader is None:
        msg = f"Cannot load {script_path}"
        raise ImportError(msg)
    mod = importlib.util.module_from_spec(spec)
    sys.modules[spec.name] = mod  # Prevent re-execution on subsequent calls
    spec.loader.exec_module(mod)
    return mod


_mod = _load_runner_module()

# Re-export Summary type for annotation use below.
Summary = _mod.Summary


def _passing_summary(features: int = 1, scenarios: int = 1) -> Summary:
    return {
        "features": {
            "passed": features,
            "failed": 0,
            "errors": 0,
            "skipped": 0,
        },
        "scenarios": {
            "passed": scenarios,
            "failed": 0,
            "errors": 0,
            "skipped": 0,
        },
        "steps": {
            "passed": scenarios * 3,
            "failed": 0,
            "errors": 0,
            "skipped": 0,
        },
        "duration": 1.0,
    }


def _failed_summary() -> Summary:
    s = _mod._empty_summary()
    s["scenarios"]["failed"] = 1
    return s


# ---- Test functions ----


def test_passing_chunk_suppressed() -> None:
    """Verify passing chunk output is not replayed."""
    summary = _passing_summary()
    assert not _mod._chunk_has_failures(summary)
    assert not _mod._chunk_no_scenarios_ran(summary)
    print("passing-chunk-suppressed-ok")


def test_failed_chunk_replayed() -> None:
    """Verify failed chunk triggers log replay."""
    summary = _failed_summary()
    assert _mod._chunk_has_failures(summary)
    print("failed-chunk-replayed-ok")


def test_crashed_chunk_detected() -> None:
    """Verify crashed chunk (empty summary) is detected."""
    summary = _mod._empty_summary()
    assert _mod._chunk_no_scenarios_ran(summary)
    print("crashed-chunk-detected-ok")


def test_worker_crash_handling() -> None:
    """Verify worker crash returns a traceback in stderr and a failed summary.

    Uses a real crash: passing a path that does not exist causes behave's
    feature loader to raise a real exception inside _worker_run_features,
    exercising the actual exception handler without any mocking.  The crash
    summary must have features.errors == 1 so _has_failures() propagates
    the failure through the merged total even when other workers passed.

    This test relies on behave raising an exception for non-existent feature
    file paths — specifically, behave's feature loader raises an exception
    (e.g. ConfigError or IOError) rather than returning an empty run result.
    This is verified, documented behave behavior.  If this test starts
    failing, check whether behave changed its file-not-found behavior
    (look at behave.runner.Runner.load_step_modules and
    behave.runner_util.collect_feature_locations).
    """
    failed, _stdout, stderr, summary = _mod._worker_run_features(
        (["__nonexistent_feature_path_that_will_crash__.feature"], [])
    )

    assert failed is True, f"Expected failed=True, got {failed}"
    assert "WORKER CRASH:" in stderr, f"Expected 'WORKER CRASH:' in stderr: {stderr!r}"
    # features.errors must be 1 so _has_failures() catches partial crashes.
    assert summary["features"]["errors"] == 1, (
        f"Expected features.errors=1, got: {summary['features']['errors']}"
    )
    print("worker-crash-handling-ok")


def test_mixed_results_filtering() -> None:
    """Verify only failed chunks contribute output in aggregation.

    Calls _aggregate_worker_results() directly so the test exercises the
    real production code rather than a reimplementation of the loop.
    """
    results: list[tuple[bool, str, str, Summary]] = [
        (False, "PASS_OUT", "", _passing_summary()),
        (True, "FAIL_OUT", "", _failed_summary()),
    ]

    # Capture stdout to inspect what _aggregate_worker_results replays.
    captured = io.StringIO()
    with redirect_stdout(captured):
        _mod._aggregate_worker_results(results)

    replayed = captured.getvalue()
    assert "FAIL_OUT" in replayed, f"Expected FAIL_OUT in output: {replayed!r}"
    assert "PASS_OUT" not in replayed, f"Expected PASS_OUT NOT in output: {replayed!r}"
    print("mixed-results-filtering-ok")


def test_summary_merge_preserved() -> None:
    """Verify summary merge semantics are unchanged."""
    s1 = _passing_summary(features=2, scenarios=4)
    s2 = _failed_summary()
    merged = _mod._merge_summaries([s1, s2])
    assert merged["features"]["passed"] == 2
    assert merged["scenarios"]["passed"] == 4
    assert merged["scenarios"]["failed"] == 1
    print("summary-merge-preserved-ok")


if __name__ == "__main__":
    cmd = sys.argv[1] if len(sys.argv) > 1 else ""
    dispatch = {
        "passing-chunk-suppressed": test_passing_chunk_suppressed,
        "failed-chunk-replayed": test_failed_chunk_replayed,
        "crashed-chunk-detected": test_crashed_chunk_detected,
        "worker-crash-handling": test_worker_crash_handling,
        "mixed-results-filtering": test_mixed_results_filtering,
        "summary-merge-preserved": test_summary_merge_preserved,
    }
    fn = dispatch.get(cmd)
    if fn is None:
        print(f"Unknown command: {cmd!r}", file=sys.stderr)
        print(f"Available: {', '.join(sorted(dispatch))}", file=sys.stderr)
        sys.exit(1)
    fn()
