"""
tests/track_b/test_story_9_01.py

Story 9.01: EpochScheduler — APScheduler Cron Trigger

Black Box Tests (BB):
    BB1  force_trigger() calls run_epoch_safe() immediately
    BB2  get_next_run() returns a future Sunday datetime (or mock equivalent)
    BB3  Start + stop + restart cycle — no duplicate jobs, no exceptions
    BB4  stop() before start() → no crash (safe no-op)

White Box Tests (WB):
    WB1  Cron is configured hour=16 (UTC), not hour=2 (AEST)
    WB2  run_epoch_safe (lock version) is called, not run_epoch directly
    WB3  Job ID is 'nightly_epoch'
    WB4  events.jsonl entries written on start and stop

ALL external I/O and APScheduler are mocked — zero live scheduler, zero live I/O.

Patching strategy:
    Because AsyncIOScheduler is a lazy import inside EpochScheduler.start(),
    we patch at its original location:
        apscheduler.schedulers.asyncio.AsyncIOScheduler
    rather than at the module level.

    EVENTS_LOG_PATH redirection is done via the constructor parameter
    ``events_log_path`` — no module-level patch needed.
"""

from __future__ import annotations

import asyncio
import inspect
import json
import sys
import tempfile
from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import Optional
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

# ---------------------------------------------------------------------------
# Path setup
# ---------------------------------------------------------------------------

GENESIS_ROOT = "/mnt/e/genesis-system"
if GENESIS_ROOT not in sys.path:
    sys.path.insert(0, GENESIS_ROOT)

# ---------------------------------------------------------------------------
# Imports under test
# ---------------------------------------------------------------------------

from core.epoch.epoch_scheduler import (  # noqa: E402
    EpochScheduler,
    EVENTS_LOG_PATH,
    _JOB_ID,
    _CRON_DAY_OF_WEEK,
    _CRON_HOUR,
    _CRON_MINUTE,
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

_APSCHEDULER_PATCH = "apscheduler.schedulers.asyncio.AsyncIOScheduler"


def _make_runner(epoch_result=None):
    """Build a mock EpochRunner with an async run_epoch_safe."""
    runner = MagicMock()
    runner.run_epoch_safe = AsyncMock(return_value=epoch_result)
    return runner


def _run(coro):
    """Run an async coroutine synchronously in tests."""
    return asyncio.get_event_loop().run_until_complete(coro)


def _next_sunday_utc() -> datetime:
    """Compute the next Sunday at 16:00 UTC from now."""
    now = datetime.now(timezone.utc)
    days_until_sunday = (6 - now.weekday()) % 7  # Monday=0 … Sunday=6
    if days_until_sunday == 0 and now.hour >= 16:
        days_until_sunday = 7  # already past this Sunday's slot
    next_sun = now + timedelta(days=days_until_sunday)
    return next_sun.replace(hour=16, minute=0, second=0, microsecond=0)


def _make_mock_scheduler(next_run_time: Optional[datetime] = None):
    """
    Return a (mock_instance, mock_job) pair that stands in for
    an APScheduler AsyncIOScheduler instance.

    ``mock_instance.get_job()`` returns a mock job whose
    ``next_run_time`` equals *next_run_time*.
    """
    mock_job = MagicMock()
    mock_job.next_run_time = next_run_time or _next_sunday_utc()

    mock_sch = MagicMock()
    mock_sch.get_job.return_value = mock_job
    mock_sch.start.return_value = None
    mock_sch.shutdown.return_value = None
    mock_sch.add_job.return_value = mock_job

    return mock_sch, mock_job


def _make_scheduler(runner=None, tmp_path=None, next_run_time=None):
    """
    Build an EpochScheduler with a mocked APScheduler.

    Returns (EpochScheduler, mock_apscheduler_instance).
    """
    if runner is None:
        runner = _make_runner()

    events_path = str(tmp_path / "events.jsonl") if tmp_path else "/tmp/test_events.jsonl"
    scheduler = EpochScheduler(runner, events_log_path=events_path)

    mock_sch, mock_job = _make_mock_scheduler(next_run_time=next_run_time)
    mock_cls = MagicMock(return_value=mock_sch)

    return scheduler, mock_cls, mock_sch, mock_job


# ---------------------------------------------------------------------------
# BB Tests — Black Box
# ---------------------------------------------------------------------------


class TestBB1_ForceTrigger:
    """BB1: force_trigger() calls run_epoch_safe() immediately."""

    def test_force_trigger_calls_run_epoch_safe(self):
        runner = _make_runner()
        scheduler = EpochScheduler(runner)

        _run(scheduler.force_trigger())

        runner.run_epoch_safe.assert_called_once()

    def test_force_trigger_works_without_start(self):
        """force_trigger() does not require start() to have been called."""
        runner = _make_runner()
        scheduler = EpochScheduler(runner)

        # Should not raise even though _scheduler is None
        _run(scheduler.force_trigger())

        runner.run_epoch_safe.assert_called_once()

    def test_force_trigger_is_coroutine_function(self):
        """force_trigger() is declared async and can be awaited."""
        runner = _make_runner()
        scheduler = EpochScheduler(runner)

        assert inspect.iscoroutinefunction(scheduler.force_trigger), (
            "force_trigger must be an async method"
        )

    def test_force_trigger_multiple_times_calls_run_epoch_safe_each_time(self):
        runner = _make_runner()
        scheduler = EpochScheduler(runner)

        _run(scheduler.force_trigger())
        _run(scheduler.force_trigger())
        _run(scheduler.force_trigger())

        assert runner.run_epoch_safe.call_count == 3, (
            f"Expected 3 calls to run_epoch_safe, got {runner.run_epoch_safe.call_count}"
        )


class TestBB2_GetNextRun:
    """BB2: get_next_run() returns a future Sunday datetime or mock equivalent."""

    def test_get_next_run_returns_none_before_start(self):
        runner = _make_runner()
        scheduler = EpochScheduler(runner)

        result = scheduler.get_next_run()

        assert result is None, (
            f"get_next_run() should return None before start(), got: {result!r}"
        )

    def test_get_next_run_returns_datetime_after_start(self, tmp_path):
        scheduler, mock_cls, mock_sch, mock_job = _make_scheduler(tmp_path=tmp_path)
        expected_next = mock_job.next_run_time

        with patch(_APSCHEDULER_PATCH, mock_cls):
            scheduler.start()

        result = scheduler.get_next_run()

        assert result is not None, "get_next_run() should return a datetime after start()"
        assert isinstance(result, datetime), (
            f"get_next_run() should return a datetime, got {type(result)}"
        )

    def test_get_next_run_returns_value_from_job(self, tmp_path):
        expected_next = _next_sunday_utc()
        scheduler, mock_cls, mock_sch, mock_job = _make_scheduler(
            tmp_path=tmp_path, next_run_time=expected_next
        )

        with patch(_APSCHEDULER_PATCH, mock_cls):
            scheduler.start()

        result = scheduler.get_next_run()

        assert result == expected_next, (
            f"get_next_run() should return job.next_run_time; "
            f"expected={expected_next!r}, got={result!r}"
        )

    def test_get_next_run_returns_none_when_job_missing(self, tmp_path):
        runner = _make_runner()
        events_path = str(tmp_path / "events.jsonl")
        scheduler = EpochScheduler(runner, events_log_path=events_path)

        mock_sch = MagicMock()
        mock_sch.get_job.return_value = None  # Job not found
        mock_cls = MagicMock(return_value=mock_sch)

        with patch(_APSCHEDULER_PATCH, mock_cls):
            scheduler.start()

        result = scheduler.get_next_run()

        assert result is None, (
            f"get_next_run() should return None when the job is not found, got: {result!r}"
        )


class TestBB3_StartStopRestart:
    """BB3: Start + stop + restart cycle — no duplicate jobs, no exceptions."""

    def test_start_does_not_raise(self, tmp_path):
        scheduler, mock_cls, mock_sch, _ = _make_scheduler(tmp_path=tmp_path)

        with patch(_APSCHEDULER_PATCH, mock_cls):
            scheduler.start()  # Must not raise

        mock_sch.start.assert_called_once()

    def test_stop_does_not_raise(self, tmp_path):
        scheduler, mock_cls, mock_sch, _ = _make_scheduler(tmp_path=tmp_path)

        with patch(_APSCHEDULER_PATCH, mock_cls):
            scheduler.start()
            scheduler.stop()  # Must not raise

        mock_sch.shutdown.assert_called_once_with(wait=False)

    def test_restart_uses_replace_existing_true(self, tmp_path):
        """Each start() must call add_job with replace_existing=True."""
        runner = _make_runner()
        events_path = str(tmp_path / "events.jsonl")
        scheduler = EpochScheduler(runner, events_log_path=events_path)

        # First start + stop
        mock_sch1 = MagicMock()
        mock_sch1.get_job.return_value = MagicMock(next_run_time=_next_sunday_utc())
        mock_cls1 = MagicMock(return_value=mock_sch1)

        with patch(_APSCHEDULER_PATCH, mock_cls1):
            scheduler.start()
            scheduler.stop()

        # Second start (restart)
        mock_sch2 = MagicMock()
        mock_sch2.get_job.return_value = MagicMock(next_run_time=_next_sunday_utc())
        mock_cls2 = MagicMock(return_value=mock_sch2)

        with patch(_APSCHEDULER_PATCH, mock_cls2):
            scheduler.start()
            scheduler.stop()

        # Each start() must call add_job exactly once with replace_existing=True
        for label, mock_sch in (("first", mock_sch1), ("second", mock_sch2)):
            calls = mock_sch.add_job.call_args_list
            assert len(calls) == 1, (
                f"Expected exactly 1 add_job call on {label} start, got {len(calls)}"
            )
            kw = calls[0].kwargs
            assert kw.get("replace_existing") is True, (
                f"add_job on {label} start must have replace_existing=True; kwargs: {kw}"
            )


class TestBB4_StopBeforeStart:
    """BB4: stop() before start() → no crash (safe no-op)."""

    def test_stop_before_start_is_no_op(self):
        runner = _make_runner()
        scheduler = EpochScheduler(runner)

        # _scheduler is None — stop() must not raise
        scheduler.stop()  # No exception expected


# ---------------------------------------------------------------------------
# WB Tests — White Box
# ---------------------------------------------------------------------------


class TestWB1_CronIsUTCHour16:
    """WB1: Cron hour=16 (UTC), not hour=2 (AEST)."""

    def test_cron_hour_constant_is_16(self):
        assert _CRON_HOUR == 16, (
            f"_CRON_HOUR must be 16 (UTC), got {_CRON_HOUR}. "
            "UTC 16:00 = AEST 02:00 Mon."
        )

    def test_cron_minute_constant_is_0(self):
        assert _CRON_MINUTE == 0, f"_CRON_MINUTE must be 0, got {_CRON_MINUTE}"

    def test_cron_day_of_week_is_sunday(self):
        assert _CRON_DAY_OF_WEEK == "sun", (
            f"_CRON_DAY_OF_WEEK must be 'sun', got {_CRON_DAY_OF_WEEK!r}"
        )

    def test_add_job_uses_correct_cron_params(self, tmp_path):
        scheduler, mock_cls, mock_sch, _ = _make_scheduler(tmp_path=tmp_path)

        with patch(_APSCHEDULER_PATCH, mock_cls):
            scheduler.start()

        calls = mock_sch.add_job.call_args_list
        assert len(calls) == 1
        kw = calls[0].kwargs

        assert kw.get("hour") == 16, (
            f"add_job hour must be 16 (UTC), got {kw.get('hour')!r}"
        )
        assert kw.get("minute") == 0, (
            f"add_job minute must be 0, got {kw.get('minute')!r}"
        )
        assert kw.get("day_of_week") == "sun", (
            f"add_job day_of_week must be 'sun', got {kw.get('day_of_week')!r}"
        )


class TestWB2_RunEpochSafeCalled:
    """WB2: run_epoch_safe (lock version) is called, not run_epoch directly."""

    def test_force_trigger_calls_run_epoch_safe_not_run_epoch(self):
        runner = _make_runner()
        # Attach run_epoch to confirm it is NOT called
        runner.run_epoch = AsyncMock(return_value=None)
        scheduler = EpochScheduler(runner)

        _run(scheduler.force_trigger())

        runner.run_epoch_safe.assert_called_once()
        runner.run_epoch.assert_not_called()

    def test_scheduler_job_target_is_run_epoch_safe(self, tmp_path):
        runner = _make_runner()
        scheduler, mock_cls, mock_sch, _ = _make_scheduler(runner=runner, tmp_path=tmp_path)

        with patch(_APSCHEDULER_PATCH, mock_cls):
            scheduler.start()

        calls = mock_sch.add_job.call_args_list
        assert len(calls) == 1

        # First positional argument to add_job is the job callable
        job_fn = calls[0].args[0]
        assert job_fn is runner.run_epoch_safe, (
            f"Scheduler job target must be runner.run_epoch_safe, got {job_fn!r}"
        )


class TestWB3_JobID:
    """WB3: Job ID is 'nightly_epoch'."""

    def test_job_id_constant(self):
        assert _JOB_ID == "nightly_epoch", (
            f"_JOB_ID must be 'nightly_epoch', got {_JOB_ID!r}"
        )

    def test_add_job_uses_nightly_epoch_id(self, tmp_path):
        scheduler, mock_cls, mock_sch, _ = _make_scheduler(tmp_path=tmp_path)

        with patch(_APSCHEDULER_PATCH, mock_cls):
            scheduler.start()

        kw = mock_sch.add_job.call_args_list[0].kwargs
        assert kw.get("id") == "nightly_epoch", (
            f"add_job id must be 'nightly_epoch', got {kw.get('id')!r}"
        )

    def test_get_job_queries_by_nightly_epoch_id(self, tmp_path):
        scheduler, mock_cls, mock_sch, _ = _make_scheduler(tmp_path=tmp_path)

        with patch(_APSCHEDULER_PATCH, mock_cls):
            scheduler.start()

        scheduler.get_next_run()

        mock_sch.get_job.assert_called_with("nightly_epoch")


class TestWB4_EventsJSONL:
    """WB4: events.jsonl entries written on start and stop."""

    def test_start_writes_epoch_scheduler_started(self, tmp_path):
        scheduler, mock_cls, mock_sch, _ = _make_scheduler(tmp_path=tmp_path)

        with patch(_APSCHEDULER_PATCH, mock_cls):
            scheduler.start()

        events_path = Path(scheduler.events_log_path)
        assert events_path.exists(), "events.jsonl should be created on start()"

        lines = [l.strip() for l in events_path.read_text().splitlines() if l.strip()]
        assert len(lines) >= 1

        event_types = [json.loads(l)["event_type"] for l in lines]
        assert "epoch_scheduler_started" in event_types, (
            f"'epoch_scheduler_started' not in events: {event_types}"
        )

    def test_stop_writes_epoch_scheduler_stopped(self, tmp_path):
        scheduler, mock_cls, mock_sch, _ = _make_scheduler(tmp_path=tmp_path)

        with patch(_APSCHEDULER_PATCH, mock_cls):
            scheduler.start()
            scheduler.stop()

        events_path = Path(scheduler.events_log_path)
        lines = [l.strip() for l in events_path.read_text().splitlines() if l.strip()]
        event_types = [json.loads(l)["event_type"] for l in lines]
        assert "epoch_scheduler_stopped" in event_types, (
            f"'epoch_scheduler_stopped' not in events: {event_types}"
        )

    def test_events_have_iso_timestamps(self, tmp_path):
        scheduler, mock_cls, mock_sch, _ = _make_scheduler(tmp_path=tmp_path)

        with patch(_APSCHEDULER_PATCH, mock_cls):
            scheduler.start()
            scheduler.stop()

        events_path = Path(scheduler.events_log_path)
        lines = [l.strip() for l in events_path.read_text().splitlines() if l.strip()]
        for line in lines:
            entry = json.loads(line)
            ts = entry.get("timestamp", "")
            assert ts, f"Event entry missing timestamp: {entry}"
            dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
            assert dt.tzinfo is not None, f"Event timestamp must be timezone-aware: {ts!r}"

    def test_no_event_written_on_stop_before_start(self, tmp_path):
        """stop() before start() must not write any events."""
        events_path = str(tmp_path / "events.jsonl")
        runner = _make_runner()
        scheduler = EpochScheduler(runner, events_log_path=events_path)

        scheduler.stop()  # _scheduler is None → no event written

        assert not Path(events_path).exists(), (
            "events.jsonl should NOT be created when stop() is called before start()"
        )

    def test_oserror_on_event_write_does_not_propagate(self, tmp_path):
        """I/O errors writing events.jsonl must not propagate."""
        events_path = str(tmp_path / "events.jsonl")
        runner = _make_runner()
        scheduler = EpochScheduler(runner, events_log_path=events_path)

        # Patch os.makedirs to raise OSError
        with patch("core.epoch.epoch_scheduler.os.makedirs", side_effect=OSError("Permission denied")):
            # Must not raise
            scheduler._log_event("epoch_scheduler_started")


# ---------------------------------------------------------------------------
# Additional edge-case tests
# ---------------------------------------------------------------------------


def test_events_log_path_module_constant():
    """EVENTS_LOG_PATH constant must reference the observability events file."""
    assert "events.jsonl" in EVENTS_LOG_PATH, (
        f"EVENTS_LOG_PATH should contain 'events.jsonl', got: {EVENTS_LOG_PATH!r}"
    )
    assert "observability" in EVENTS_LOG_PATH, (
        f"EVENTS_LOG_PATH should be under data/observability/, got: {EVENTS_LOG_PATH!r}"
    )


def test_package_init_exports_epoch_scheduler():
    """core.epoch __init__ must export EpochScheduler and EVENTS_LOG_PATH."""
    from core.epoch import EpochScheduler as ES, EVENTS_LOG_PATH as ELP

    assert ES is EpochScheduler, "EpochScheduler not re-exported from core.epoch"
    assert isinstance(ELP, str), "EVENTS_LOG_PATH should be a string"
    assert "events.jsonl" in ELP, (
        f"EVENTS_LOG_PATH should contain 'events.jsonl', got: {ELP!r}"
    )


def test_scheduler_not_started_after_init():
    """_scheduler must be None immediately after __init__."""
    runner = _make_runner()
    scheduler = EpochScheduler(runner)

    assert scheduler._scheduler is None, (
        f"_scheduler should be None before start(); got {scheduler._scheduler!r}"
    )


def test_get_next_run_after_stop_job_gone(tmp_path):
    """After stop(), if get_job returns None → get_next_run() returns None."""
    runner = _make_runner()
    events_path = str(tmp_path / "events.jsonl")
    scheduler = EpochScheduler(runner, events_log_path=events_path)

    mock_sch = MagicMock()
    mock_sch.get_job.return_value = None  # Simulates job gone after stop
    mock_cls = MagicMock(return_value=mock_sch)

    with patch(_APSCHEDULER_PATCH, mock_cls):
        scheduler.start()
        scheduler.stop()

    result = scheduler.get_next_run()
    assert result is None, (
        f"get_next_run() should return None when job is gone, got: {result!r}"
    )


def test_start_creates_new_scheduler_each_call(tmp_path):
    """Each call to start() creates a brand-new AsyncIOScheduler instance."""
    runner = _make_runner()
    events_path = str(tmp_path / "events.jsonl")
    scheduler = EpochScheduler(runner, events_log_path=events_path)

    call_count = 0

    def make_mock_sch(*args, **kwargs):
        nonlocal call_count
        call_count += 1
        mock_sch = MagicMock()
        mock_sch.get_job.return_value = MagicMock(next_run_time=_next_sunday_utc())
        return mock_sch

    with patch(_APSCHEDULER_PATCH, side_effect=make_mock_sch):
        scheduler.start()
        scheduler.stop()
        scheduler.start()
        scheduler.stop()

    assert call_count == 2, (
        f"AsyncIOScheduler should be instantiated twice (once per start); got {call_count}"
    )


# ---------------------------------------------------------------------------
# Standalone runner
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    import traceback

    def _tmp() -> Path:
        return Path(tempfile.mkdtemp())

    test_fns = [
        # BB1
        ("BB1: force_trigger calls run_epoch_safe", TestBB1_ForceTrigger().test_force_trigger_calls_run_epoch_safe),
        ("BB1: force_trigger works without start", TestBB1_ForceTrigger().test_force_trigger_works_without_start),
        ("BB1: force_trigger is async", TestBB1_ForceTrigger().test_force_trigger_is_coroutine_function),
        ("BB1: force_trigger x3 = 3 calls", TestBB1_ForceTrigger().test_force_trigger_multiple_times_calls_run_epoch_safe_each_time),
        # BB2
        ("BB2: get_next_run None before start", TestBB2_GetNextRun().test_get_next_run_returns_none_before_start),
        ("BB2: get_next_run datetime after start", lambda: TestBB2_GetNextRun().test_get_next_run_returns_datetime_after_start(_tmp())),
        ("BB2: get_next_run from job.next_run_time", lambda: TestBB2_GetNextRun().test_get_next_run_returns_value_from_job(_tmp())),
        ("BB2: get_next_run None when job missing", lambda: TestBB2_GetNextRun().test_get_next_run_returns_none_when_job_missing(_tmp())),
        # BB3
        ("BB3: start does not raise", lambda: TestBB3_StartStopRestart().test_start_does_not_raise(_tmp())),
        ("BB3: stop does not raise", lambda: TestBB3_StartStopRestart().test_stop_does_not_raise(_tmp())),
        ("BB3: restart — replace_existing=True", lambda: TestBB3_StartStopRestart().test_restart_uses_replace_existing_true(_tmp())),
        # BB4
        ("BB4: stop before start is no-op", TestBB4_StopBeforeStart().test_stop_before_start_is_no_op),
        # WB1
        ("WB1: _CRON_HOUR is 16", TestWB1_CronIsUTCHour16().test_cron_hour_constant_is_16),
        ("WB1: _CRON_MINUTE is 0", TestWB1_CronIsUTCHour16().test_cron_minute_constant_is_0),
        ("WB1: _CRON_DAY_OF_WEEK is 'sun'", TestWB1_CronIsUTCHour16().test_cron_day_of_week_is_sunday),
        ("WB1: add_job uses correct cron params", lambda: TestWB1_CronIsUTCHour16().test_add_job_uses_correct_cron_params(_tmp())),
        # WB2
        ("WB2: force_trigger calls safe, not raw", TestWB2_RunEpochSafeCalled().test_force_trigger_calls_run_epoch_safe_not_run_epoch),
        ("WB2: scheduler job target is run_epoch_safe", lambda: TestWB2_RunEpochSafeCalled().test_scheduler_job_target_is_run_epoch_safe(_tmp())),
        # WB3
        ("WB3: job ID constant is 'nightly_epoch'", TestWB3_JobID().test_job_id_constant),
        ("WB3: add_job uses 'nightly_epoch' id", lambda: TestWB3_JobID().test_add_job_uses_nightly_epoch_id(_tmp())),
        ("WB3: get_job queries by 'nightly_epoch'", lambda: TestWB3_JobID().test_get_job_queries_by_nightly_epoch_id(_tmp())),
        # WB4
        ("WB4: start writes scheduler_started event", lambda: TestWB4_EventsJSONL().test_start_writes_epoch_scheduler_started(_tmp())),
        ("WB4: stop writes scheduler_stopped event", lambda: TestWB4_EventsJSONL().test_stop_writes_epoch_scheduler_stopped(_tmp())),
        ("WB4: events have ISO timestamps", lambda: TestWB4_EventsJSONL().test_events_have_iso_timestamps(_tmp())),
        ("WB4: no event written on stop-before-start", lambda: TestWB4_EventsJSONL().test_no_event_written_on_stop_before_start(_tmp())),
        ("WB4: OSError does not propagate", lambda: TestWB4_EventsJSONL().test_oserror_on_event_write_does_not_propagate(_tmp())),
        # Edge cases
        ("EDGE: EVENTS_LOG_PATH module constant", test_events_log_path_module_constant),
        ("EDGE: package exports EpochScheduler + EVENTS_LOG_PATH", test_package_init_exports_epoch_scheduler),
        ("EDGE: _scheduler None after init", test_scheduler_not_started_after_init),
        ("EDGE: get_next_run None after stop", lambda: test_get_next_run_after_stop_job_gone(_tmp())),
        ("EDGE: start creates new scheduler each call", lambda: test_start_creates_new_scheduler_each_call(_tmp())),
    ]

    import tempfile
    passed = 0
    total = len(test_fns)
    for name, fn in test_fns:
        try:
            fn()
            print(f"  [PASS] {name}")
            passed += 1
        except Exception as exc:  # noqa: BLE001
            print(f"  [FAIL] {name}: {exc}")
            traceback.print_exc()

    print(f"\n{passed}/{total} tests passed")
    if passed == total:
        print("ALL TESTS PASSED -- Story 9.01 (Track B)")
    else:
        sys.exit(1)
