#!/usr/bin/env python3
"""
Tests for Story 6.07 (Track B): BulkheadGuard — asyncio.gather Exception Isolation

Black Box tests (BB): verify the public contract from the outside —
    run_with_bulkhead isolates exceptions, returns correct results for each
    agent, never raises, handles empty inputs, computes success rates correctly.

White Box tests (WB): verify internal mechanics — asyncio.gather called with
    return_exceptions=True, ColdLedger.write_event called on critical failure,
    no crash when cold_ledger is None, BulkheadResult.error contains the
    exception message string.

Package test:
    PKG1: from core.coherence import BulkheadGuard, BulkheadResult works.

Story: 6.07
File under test: core/coherence/bulkhead.py

ALL tests use async coroutines or mocks — NO real I/O performed.
"""

from __future__ import annotations

import asyncio
import sys
from unittest.mock import AsyncMock, MagicMock, patch

sys.path.insert(0, "/mnt/e/genesis-system")

import pytest

from core.coherence.bulkhead import (
    BulkheadGuard,
    BulkheadResult,
    CRITICAL_THRESHOLD,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def run(coro):
    """Run a coroutine synchronously (no pytest-asyncio required)."""
    return asyncio.get_event_loop().run_until_complete(coro)


async def _succeeding(value: dict | None = None) -> dict | None:
    """Coroutine that succeeds and returns value."""
    return value or {"status": "ok"}


async def _failing(message: str = "boom") -> dict:
    """Coroutine that raises RuntimeError."""
    raise RuntimeError(message)


def _make_cold_ledger() -> MagicMock:
    """Return a mock ColdLedger with async write_event."""
    ledger = MagicMock()
    ledger.write_event = AsyncMock(return_value=None)
    return ledger


# ===========================================================================
# BLACK BOX TESTS
# ===========================================================================


def test_bb1_mixed_tasks_correct_success_failure_split():
    """
    BB1: 4 tasks (3 success, 1 exception) →
         3 BulkheadResult(success=True), 1 BulkheadResult(success=False).
    """
    guard = BulkheadGuard()
    tasks = [
        ("agent-1", _succeeding({"x": 1})),
        ("agent-2", _succeeding({"x": 2})),
        ("agent-fail", _failing("test boom")),
        ("agent-3", _succeeding({"x": 3})),
    ]
    results = run(guard.run_with_bulkhead(tasks))

    assert len(results) == 4

    successes = [r for r in results if r.success]
    failures = [r for r in results if not r.success]

    assert len(successes) == 3
    assert len(failures) == 1
    assert failures[0].agent_id == "agent-fail"
    assert failures[0].error is not None
    assert "test boom" in failures[0].error


def test_bb2_all_tasks_fail_success_rate_is_zero():
    """
    BB2: All tasks fail → get_success_rate() returns 0.0.
    """
    guard = BulkheadGuard()
    tasks = [
        ("agent-a", _failing("err-a")),
        ("agent-b", _failing("err-b")),
    ]
    results = run(guard.run_with_bulkhead(tasks))

    assert guard.get_success_rate(results) == 0.0
    assert all(not r.success for r in results)


def test_bb3_run_with_bulkhead_never_raises():
    """
    BB3: run_with_bulkhead does not raise even when all tasks fail.
    """
    guard = BulkheadGuard()
    tasks = [
        ("agent-x", _failing("catastrophic failure")),
        ("agent-y", _failing("another crash")),
    ]
    try:
        results = run(guard.run_with_bulkhead(tasks))
        # Must return a list, not propagate
        assert isinstance(results, list)
    except Exception as exc:
        pytest.fail(f"run_with_bulkhead raised unexpectedly: {exc}")


def test_bb4_all_tasks_succeed_success_rate_is_one():
    """
    BB4: All tasks succeed → get_success_rate() returns 1.0.
    """
    guard = BulkheadGuard()
    tasks = [
        ("agent-1", _succeeding({"v": 1})),
        ("agent-2", _succeeding({"v": 2})),
        ("agent-3", _succeeding({"v": 3})),
    ]
    results = run(guard.run_with_bulkhead(tasks))

    assert guard.get_success_rate(results) == 1.0
    assert all(r.success for r in results)


def test_bb5_empty_task_list_returns_empty_and_rate_one():
    """
    BB5: Empty task list → returns [], get_success_rate returns 1.0.
    """
    guard = BulkheadGuard()
    results = run(guard.run_with_bulkhead([]))

    assert results == []
    assert guard.get_success_rate(results) == 1.0


# ===========================================================================
# WHITE BOX TESTS
# ===========================================================================


def test_wb1_gather_called_with_return_exceptions_true():
    """
    WB1: asyncio.gather is called with return_exceptions=True so that
         exceptions are collected, not re-raised.
    """
    guard = BulkheadGuard()
    tasks = [
        ("agent-a", _succeeding()),
        ("agent-b", _failing()),
    ]

    original_gather = asyncio.gather

    captured_kwargs: dict = {}

    async def spy_gather(*coros, **kwargs):
        captured_kwargs.update(kwargs)
        return await original_gather(*coros, **kwargs)

    with patch("core.coherence.bulkhead.asyncio.gather", side_effect=spy_gather):
        run(guard.run_with_bulkhead(tasks))

    assert captured_kwargs.get("return_exceptions") is True, (
        "asyncio.gather must be called with return_exceptions=True"
    )


def test_wb2_success_rate_below_threshold_triggers_cold_ledger_write():
    """
    WB2: success_rate < 0.5 triggers ColdLedger.write_event with
         event_type='swarm_critical_failure'.
    """
    ledger = _make_cold_ledger()
    guard = BulkheadGuard(cold_ledger=ledger)

    # 0 successes out of 2 = 0.0 < CRITICAL_THRESHOLD (0.5)
    tasks = [
        ("agent-a", _failing("err-a")),
        ("agent-b", _failing("err-b")),
    ]
    run(guard.run_with_bulkhead(tasks))

    ledger.write_event.assert_awaited_once()
    call_kwargs = ledger.write_event.call_args
    # Check event_type argument (positional or keyword)
    args, kwargs = call_kwargs
    event_type = kwargs.get("event_type") or (args[0] if args else None)
    assert event_type == "swarm_critical_failure"


def test_wb3_no_cold_ledger_no_crash_on_critical_failure():
    """
    WB3: cold_ledger=None → no crash when success_rate < CRITICAL_THRESHOLD.
         BulkheadGuard gracefully skips ledger write.
    """
    guard = BulkheadGuard(cold_ledger=None)
    tasks = [
        ("agent-x", _failing("crash-x")),
        ("agent-y", _failing("crash-y")),
    ]
    try:
        results = run(guard.run_with_bulkhead(tasks))
        assert isinstance(results, list)
    except Exception as exc:
        pytest.fail(f"BulkheadGuard crashed without ColdLedger: {exc}")


def test_wb4_bulkhead_result_error_contains_exception_message():
    """
    WB4: BulkheadResult.error for a failing task contains the str(exception)
         of the raised exception.
    """
    guard = BulkheadGuard()
    error_message = "specific error payload"
    tasks = [("agent-err", _failing(error_message))]

    results = run(guard.run_with_bulkhead(tasks))

    assert len(results) == 1
    r = results[0]
    assert r.success is False
    assert r.error is not None
    assert error_message in r.error


# ===========================================================================
# PACKAGE EXPORT TEST
# ===========================================================================


def test_pkg1_package_exports_bulkhead_guard_and_result():
    """PKG1: from core.coherence import BulkheadGuard, BulkheadResult works."""
    from core.coherence import BulkheadGuard as BG, BulkheadResult as BR, CRITICAL_THRESHOLD as CT

    assert BG is BulkheadGuard
    assert BR is BulkheadResult
    assert CT == 0.5


# ===========================================================================
# ADDITIONAL EDGE CASE TESTS
# ===========================================================================


def test_bb_success_rate_exactly_at_threshold_does_not_emit_critical():
    """
    Edge: success_rate == CRITICAL_THRESHOLD (0.5) does NOT trigger critical
    event — threshold is strictly less-than.
    2 tasks, 1 success = 0.5 exactly → no ledger write.
    """
    ledger = _make_cold_ledger()
    guard = BulkheadGuard(cold_ledger=ledger)

    # Exactly 50% success — not below threshold
    tasks = [
        ("agent-ok", _succeeding()),
        ("agent-fail", _failing("half")),
    ]
    run(guard.run_with_bulkhead(tasks))

    ledger.write_event.assert_not_awaited()


def test_bb_results_preserve_order():
    """
    Edge: results are returned in the same order as input tasks.
    """
    guard = BulkheadGuard()
    tasks = [
        ("agent-first", _succeeding({"n": 1})),
        ("agent-second", _failing("oops")),
        ("agent-third", _succeeding({"n": 3})),
    ]
    results = run(guard.run_with_bulkhead(tasks))

    assert results[0].agent_id == "agent-first"
    assert results[1].agent_id == "agent-second"
    assert results[2].agent_id == "agent-third"


def test_bb_single_task_success():
    """Single task that succeeds → success=True, result set, error=None."""
    guard = BulkheadGuard()
    payload = {"key": "value"}
    results = run(guard.run_with_bulkhead([("solo", _succeeding(payload))]))

    assert len(results) == 1
    assert results[0].success is True
    assert results[0].result == payload
    assert results[0].error is None


def test_bb_single_task_failure():
    """Single task that fails → success=False, result=None, error set."""
    guard = BulkheadGuard()
    results = run(guard.run_with_bulkhead([("solo", _failing("down"))]))

    assert len(results) == 1
    assert results[0].success is False
    assert results[0].result is None
    assert "down" in results[0].error


# ===========================================================================
# Standalone runner (pytest preferred, fallback to direct execution)
# ===========================================================================

if __name__ == "__main__":
    import traceback

    tests = [
        ("BB1: 3 success, 1 failure → correct split", test_bb1_mixed_tasks_correct_success_failure_split),
        ("BB2: All fail → success_rate == 0.0", test_bb2_all_tasks_fail_success_rate_is_zero),
        ("BB3: run_with_bulkhead never raises", test_bb3_run_with_bulkhead_never_raises),
        ("BB4: All succeed → success_rate == 1.0", test_bb4_all_tasks_succeed_success_rate_is_one),
        ("BB5: Empty list → [], rate 1.0", test_bb5_empty_task_list_returns_empty_and_rate_one),
        ("WB1: gather called with return_exceptions=True", test_wb1_gather_called_with_return_exceptions_true),
        ("WB2: rate < 0.5 triggers ColdLedger write_event", test_wb2_success_rate_below_threshold_triggers_cold_ledger_write),
        ("WB3: cold_ledger=None no crash on critical failure", test_wb3_no_cold_ledger_no_crash_on_critical_failure),
        ("WB4: BulkheadResult.error contains exception message", test_wb4_bulkhead_result_error_contains_exception_message),
        ("PKG1: Package exports BulkheadGuard, BulkheadResult, CRITICAL_THRESHOLD", test_pkg1_package_exports_bulkhead_guard_and_result),
        ("EDGE: rate == 0.5 does NOT emit critical", test_bb_success_rate_exactly_at_threshold_does_not_emit_critical),
        ("EDGE: results preserve input order", test_bb_results_preserve_order),
        ("EDGE: single success", test_bb_single_task_success),
        ("EDGE: single failure", test_bb_single_task_failure),
    ]

    passed = 0
    total = len(tests)
    for name, fn in tests:
        try:
            fn()
            print(f"  [PASS] {name}")
            passed += 1
        except Exception as exc:
            print(f"  [FAIL] {name}: {exc}")
            traceback.print_exc()

    print(f"\n{passed}/{total} tests passed")
    if passed == total:
        print("ALL TESTS PASSED -- Story 6.07 (Track B)")
    else:
        sys.exit(1)
