"""
Module 4 Routing Test Suite — Black Box + White Box
Story 4.07 — Genesis Persistent Context Architecture

Covers all 6 Module 4 routing files:
  1. core/routing/tier_classifier.py  — TierClassifier, RoutingDecision
  2. core/routing/tier_router.py      — TierRouter (Redis-cached routing)
  3. core/routing/tier_executor.py    — TierExecutor (T0/T1/T2 dispatch)
  4. core/routing/strangler_fig.py    — @strangler_fig decorator + registry
  5. core/routing/routing_telemetry.py — RoutingTelemetry (distribution tracking)
  6. core/routing/tier_router_interceptor.py — TierRouterInterceptor (priority=20)

Test classes:
  BB1: TestTierClassifierBlackBox    — 10+ task types, T0/T1/T2 coverage
  BB2: TestTierRouterBlackBox        — Redis cache hit/miss, events log swallowed
  BB3: TestTierExecutorBlackBox      — T0/T1/T2 dispatch, graceful degradation
  BB4: TestStranglerFigBlackBox      — Wrapping, fallback safety, registry
  BB5: TestRoutingTelemetryBlackBox  — Distribution tracking, local + Redis counters
  WB1: TestTierClassifierWhiteBox    — T0 zero-LLM, T2 Opus model selection
  WB2: TestTierRouterWhiteBox        — Cache key format, Redis error swallow
  WB3: TestTierExecutorWhiteBox      — T2 model guard, T0-missing handler degradation
  WB4: TestInterceptorWhiteBox       — pre_execute stamps, post_execute telemetry, on_error defaults

Total target: 14+ test cases (actual: 25)
"""
import asyncio
import json
import sys
from unittest.mock import AsyncMock, MagicMock, patch, call
from pathlib import Path

sys.path.insert(0, "/mnt/e/genesis-system")

import pytest
import pytest_asyncio

from core.routing.tier_classifier import (
    TierClassifier,
    RoutingDecision,
    T0_TYPES,
    T1_TYPES,
    TIER_MODELS,
)
from core.routing.tier_router import TierRouter, CACHE_TTL
from core.routing.tier_executor import TierExecutor, T0_HANDLER_REGISTRY, register_t0_handler
from core.routing.strangler_fig import strangler_fig, get_migration_status, WRAPPED_FUNCTIONS
from core.routing.routing_telemetry import (
    RoutingTelemetry,
    T0_KEY,
    T1_KEY,
    T2_KEY,
)
from core.routing.tier_router_interceptor import TierRouterInterceptor


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_redis(get_return=None, incr_ok=True):
    """Build a mock Redis client with configurable behaviour."""
    redis = MagicMock()
    redis.get.return_value = get_return
    redis.setex.return_value = True
    redis.incr.return_value = 1
    redis.delete.return_value = 3
    if not incr_ok:
        redis.incr.side_effect = ConnectionError("Redis down")
    return redis


def _decision(tier: str) -> RoutingDecision:
    """Quickly build a RoutingDecision for use in tests."""
    models = {"T0": "python_function", "T1": "gemini-flash", "T2": "claude-opus-4-6"}
    return RoutingDecision(tier=tier, model=models[tier], rationale=f"test {tier}")


# ===========================================================================
# BB1: TierClassifier — Black Box
# ===========================================================================

class TestTierClassifierBlackBox:
    """Black-box tests for TierClassifier.classify() — test the public API contract."""

    def setup_method(self):
        self.clf = TierClassifier()

    # --- T0 task types (10 distinct types from T0_TYPES) ---

    def test_health_check_routes_to_T0(self):
        """BB: 'health_check' is a T0 deterministic task."""
        d = self.clf.classify({"type": "health_check"})
        assert d.tier == "T0"
        assert d.model == "python_function"
        assert d.is_cached is False

    def test_redis_read_routes_to_T0(self):
        """BB: 'redis_read' is a T0 deterministic task."""
        d = self.clf.classify({"type": "redis_read"})
        assert d.tier == "T0"

    def test_redis_write_routes_to_T0(self):
        """BB: 'redis_write' is a T0 deterministic task."""
        d = self.clf.classify({"type": "redis_write"})
        assert d.tier == "T0"

    def test_kg_lookup_routes_to_T0(self):
        """BB: 'kg_lookup' is a T0 deterministic task."""
        d = self.clf.classify({"type": "kg_lookup"})
        assert d.tier == "T0"

    def test_format_transform_routes_to_T0(self):
        """BB: 'format_transform' is a T0 deterministic task."""
        d = self.clf.classify({"type": "format_transform"})
        assert d.tier == "T0"

    def test_cache_get_routes_to_T0(self):
        """BB: 'cache_get' is a T0 deterministic task."""
        d = self.clf.classify({"type": "cache_get"})
        assert d.tier == "T0"

    def test_status_check_routes_to_T0(self):
        """BB: 'status_check' is a T0 deterministic task."""
        d = self.clf.classify({"type": "status_check"})
        assert d.tier == "T0"

    def test_T0_payload_key_crud_operation_overrides_unknown_type(self):
        """BB: T0 payload key 'crud_operation' triggers T0 for any task type."""
        d = self.clf.classify({"type": "novel_unknown_type", "crud_operation": True})
        assert d.tier == "T0"
        assert "crud_operation" in d.rationale

    def test_T0_payload_key_lookup_key_overrides_unknown_type(self):
        """BB: T0 payload key 'lookup_key' triggers T0 for any task type."""
        d = self.clf.classify({"type": "something_else", "lookup_key": "abc"})
        assert d.tier == "T0"

    # --- T1 task types ---

    def test_email_draft_routes_to_T1(self):
        """BB: 'email_draft' is a T1 parametric task."""
        d = self.clf.classify({"type": "email_draft"})
        assert d.tier == "T1"
        assert d.model == "gemini-flash"

    def test_faq_answer_routes_to_T1(self):
        """BB: 'faq_answer' is a T1 parametric task."""
        d = self.clf.classify({"type": "faq_answer"})
        assert d.tier == "T1"

    def test_T1_payload_key_template_overrides_unknown_type(self):
        """BB: T1 payload key 'template' triggers T1 for any task type."""
        d = self.clf.classify({"type": "mystery_task", "template": "greet_user"})
        assert d.tier == "T1"
        assert d.model == "gemini-flash"
        assert "template" in d.rationale

    # --- T2 default ---

    def test_unknown_type_defaults_to_T2(self):
        """BB: Any task type not in T0 or T1 defaults to T2 reasoning."""
        d = self.clf.classify({"type": "deep_analysis_task"})
        assert d.tier == "T2"
        assert d.model == "claude-opus-4-6"

    def test_missing_type_key_defaults_to_T2(self):
        """BB: Missing 'type' key falls back to T2 (unknown type)."""
        d = self.clf.classify({})
        assert d.tier == "T2"

    def test_T0_priority_wins_over_T1_payload_key(self):
        """BB: When both T0 type and T1 payload key present, T0 wins (evaluated first)."""
        d = self.clf.classify({"type": "health_check", "template": "some_template"})
        assert d.tier == "T0"

    def test_rationale_is_non_empty_string(self):
        """BB: Every classification returns a non-empty rationale string."""
        for task_type in ["health_check", "email_draft", "novel_reasoning_task"]:
            d = self.clf.classify({"type": task_type})
            assert isinstance(d.rationale, str)
            assert len(d.rationale) > 0


# ===========================================================================
# BB2: TierRouter — Black Box
# ===========================================================================

class TestTierRouterBlackBox:
    """Black-box tests for TierRouter.route() — cache hit/miss, no Redis fallback."""

    def test_route_without_redis_returns_valid_decision(self):
        """BB: Router without Redis still classifies correctly."""
        router = TierRouter()
        d = router.route({"type": "health_check", "task_id": "t-001"})
        assert d.tier == "T0"
        assert d.is_cached is False

    def test_route_cache_miss_sets_is_cached_false(self):
        """BB: Cache miss path returns is_cached=False."""
        redis = _make_redis(get_return=None)  # None = cache miss
        router = TierRouter(redis_client=redis)
        d = router.route({"type": "email_draft", "task_id": "t-002"})
        assert d.tier == "T1"
        assert d.is_cached is False

    def test_route_cache_hit_returns_is_cached_true(self):
        """BB: Cache hit returns is_cached=True without calling classifier."""
        cached_data = json.dumps({"tier": "T1", "model": "gemini-flash",
                                   "rationale": "cached reason"}).encode()
        redis = _make_redis(get_return=cached_data)
        router = TierRouter(redis_client=redis)

        d = router.route({"type": "email_draft", "task_id": "t-003"})
        assert d.tier == "T1"
        assert d.is_cached is True
        assert d.rationale == "cached reason"

    def test_redis_get_exception_falls_through_to_classifier(self):
        """BB: Redis.get() exception is swallowed; classification proceeds normally."""
        redis = MagicMock()
        redis.get.side_effect = ConnectionError("Redis down")
        router = TierRouter(redis_client=redis)

        d = router.route({"type": "health_check", "task_id": "t-004"})
        # Still gets classified — Redis failure is non-fatal
        assert d.tier == "T0"
        assert d.is_cached is False

    def test_route_writes_to_redis_on_cache_miss(self):
        """BB: On cache miss, router calls setex to persist the decision."""
        redis = _make_redis(get_return=None)
        router = TierRouter(redis_client=redis)
        router.route({"type": "health_check"})
        redis.setex.assert_called_once()
        # Verify TTL is CACHE_TTL
        args = redis.setex.call_args[0]
        assert args[1] == CACHE_TTL

    def test_route_log_event_failure_does_not_raise(self):
        """BB: Even if observability log can't be written, route() returns normally."""
        router = TierRouter()
        with patch("core.routing.tier_router.open", side_effect=OSError("disk full")):
            d = router.route({"type": "health_check"})
        assert d.tier == "T0"


# ===========================================================================
# BB3: TierExecutor — Black Box
# ===========================================================================

class TestTierExecutorBlackBox:
    """Black-box tests for TierExecutor.execute() — correct dispatch by tier."""

    @pytest.mark.asyncio
    async def test_T0_health_check_returns_ok_no_dispatch_fn(self):
        """BB: T0 health_check executes pure-Python handler — no dispatch_fn needed."""
        executor = TierExecutor()  # no dispatch_fn
        result = await executor.execute(
            {"type": "health_check", "task_id": "t-100"},
            _decision("T0"),
        )
        assert result["status"] == "ok"
        assert result["tier"] == "T0"
        assert result["model"] == "python_function"
        assert result["output"]["healthy"] is True

    @pytest.mark.asyncio
    async def test_T0_log_event_returns_logged_true(self):
        """BB: T0 log_event handler returns logged=True in output."""
        executor = TierExecutor()
        result = await executor.execute(
            {"type": "log_event", "task_id": "t-101"},
            _decision("T0"),
        )
        assert result["output"]["logged"] is True

    @pytest.mark.asyncio
    async def test_T1_without_dispatch_fn_returns_stub(self):
        """BB: T1 without dispatch_fn returns structured stub with correct model."""
        executor = TierExecutor()
        result = await executor.execute(
            {"type": "email_draft", "task_id": "t-200"},
            _decision("T1"),
        )
        assert result["tier"] == "T1"
        assert result["model"] == "gemini-flash"
        assert result["status"] == "ok"
        assert result["output"] is None

    @pytest.mark.asyncio
    async def test_T2_without_dispatch_fn_returns_stub(self):
        """BB: T2 without dispatch_fn returns structured stub with Opus model."""
        executor = TierExecutor()
        result = await executor.execute(
            {"type": "novel_task", "task_id": "t-300"},
            _decision("T2"),
        )
        assert result["tier"] == "T2"
        assert result["model"] == "claude-opus-4-6"
        assert result["status"] == "ok"

    @pytest.mark.asyncio
    async def test_T1_calls_dispatch_fn_with_gemini_flash_model(self):
        """BB: T1 path calls dispatch_fn with model=gemini-flash."""
        dispatch_fn = AsyncMock(return_value={"status": "ok", "output": "done"})
        executor = TierExecutor(dispatch_fn=dispatch_fn)

        await executor.execute({"type": "email_draft", "task_id": "t-201"}, _decision("T1"))

        dispatch_fn.assert_awaited_once()
        enriched_call = dispatch_fn.call_args[0][0]
        assert enriched_call["model"] == "gemini-flash"
        assert enriched_call["tier"] == "T1"

    @pytest.mark.asyncio
    async def test_T2_calls_dispatch_fn_with_opus_model(self):
        """BB: T2 path calls dispatch_fn with model=claude-opus-4-6."""
        dispatch_fn = AsyncMock(return_value={"status": "ok", "output": "done"})
        executor = TierExecutor(dispatch_fn=dispatch_fn)

        await executor.execute({"type": "deep_task", "task_id": "t-301"}, _decision("T2"))

        dispatch_fn.assert_awaited_once()
        enriched_call = dispatch_fn.call_args[0][0]
        assert enriched_call["model"] == "claude-opus-4-6"
        assert enriched_call["tier"] == "T2"

    @pytest.mark.asyncio
    async def test_T0_unregistered_type_degrades_to_T1(self):
        """BB: T0 decision with no registered handler gracefully degrades to T1 stub."""
        executor = TierExecutor()  # no dispatch_fn
        # Use a task_type that is in T0_TYPES conceptually but not registered
        # We'll temporarily use a type not in T0_HANDLER_REGISTRY
        result = await executor.execute(
            {"type": "cache_get", "task_id": "t-150"},
            _decision("T0"),
        )
        # cache_get has no handler → degrades to T1 stub
        assert result["tier"] == "T1"
        assert result["model"] == "gemini-flash"
        assert result["status"] == "ok"


# ===========================================================================
# BB4: StranglerFig — Black Box
# ===========================================================================

class TestStranglerFigBlackBox:
    """Black-box tests for @strangler_fig decorator and get_migration_status."""

    @pytest.mark.asyncio
    async def test_wrapped_function_returns_original_result_on_dispatch_failure(self):
        """BB: When dispatch_to_swarm raises, wrapper returns original function result.

        dispatch_to_swarm is imported dynamically inside the wrapper via
        'from core.interceptors import dispatch_to_swarm'. Patching at the
        core.interceptors module level intercepts it at import time.
        """

        @strangler_fig("test_capture_bb", tier="T1")
        async def _capture(data: dict) -> dict:
            return {"captured": True, "data": data}

        # Patch the symbol at its definition site so the dynamic import sees the mock
        with patch("core.interceptors.dispatch_to_swarm",
                   side_effect=RuntimeError("swarm unreachable")):
            result = await _capture({"id": "bb-001"})

        assert result == {"captured": True, "data": {"id": "bb-001"}}

    @pytest.mark.asyncio
    async def test_wrapped_function_returns_dispatch_result_on_success(self):
        """BB: When dispatch_to_swarm succeeds, wrapper returns its result."""
        expected = {"status": "ok", "tier": "T1"}

        @strangler_fig("test_dispatch_success_bb", tier="T1")
        async def _my_fn() -> dict:
            return {"original": True}

        with patch("core.interceptors.dispatch_to_swarm",
                   new_callable=AsyncMock, return_value=expected):
            result = await _my_fn()

        assert result == expected

    def test_get_migration_status_tracks_wrapped_functions(self):
        """BB: get_migration_status() reflects all functions decorated with @strangler_fig."""

        @strangler_fig("status_test_task_bb", tier="T0")
        async def _tracked_fn() -> None:
            pass

        status = get_migration_status()
        assert "total_wrapped" in status
        assert "_tracked_fn" in status["functions"]
        assert status["functions"]["_tracked_fn"]["task_type"] == "status_test_task_bb"
        assert status["functions"]["_tracked_fn"]["tier"] == "T0"
        assert status["total_wrapped"] >= 1

    def test_strangler_fig_preserves_function_name(self):
        """BB: Decorator preserves __name__ via functools.wraps."""

        @strangler_fig("fn_name_preservation", tier="T2")
        async def _name_preserved() -> None:
            pass

        assert _name_preserved.__name__ == "_name_preserved"

    @pytest.mark.asyncio
    async def test_strangler_fig_no_tier_still_works(self):
        """BB: @strangler_fig without tier= argument still wraps and falls back safely."""

        @strangler_fig("no_tier_task")
        async def _no_tier() -> str:
            return "original_value"

        # dispatch_to_swarm is imported dynamically inside wrapper body —
        # patch at the definition site so the local import sees the mock.
        with patch("core.interceptors.dispatch_to_swarm",
                   side_effect=ImportError("interceptors not wired")):
            result = await _no_tier()

        assert result == "original_value"


# ===========================================================================
# BB5: RoutingTelemetry — Black Box
# ===========================================================================

class TestRoutingTelemetryBlackBox:
    """Black-box tests for RoutingTelemetry — distribution accumulation."""

    def test_local_counters_accumulate_correctly(self):
        """BB: record() increments local counters when no Redis is supplied."""
        tel = RoutingTelemetry()  # no Redis
        tel.record(_decision("T0"))
        tel.record(_decision("T0"))
        tel.record(_decision("T1"))
        tel.record(_decision("T2"))

        dist = tel.get_distribution()
        assert dist["t0"] == 2
        assert dist["t1"] == 1
        assert dist["t2"] == 1

    def test_t0_t1_pct_calculated_correctly(self):
        """BB: t0_t1_pct = (t0+t1)/total * 100, rounded to 1 decimal."""
        tel = RoutingTelemetry()
        # 3 T0 + 1 T1 + 1 T2 = 5 total → 80.0%
        for _ in range(3):
            tel.record(_decision("T0"))
        tel.record(_decision("T1"))
        tel.record(_decision("T2"))

        dist = tel.get_distribution()
        assert dist["t0_t1_pct"] == 80.0

    def test_empty_distribution_returns_zero_pct(self):
        """BB: No recorded tasks → t0_t1_pct = 0.0 (no division by zero)."""
        tel = RoutingTelemetry()
        dist = tel.get_distribution()
        assert dist["t0_t1_pct"] == 0.0
        assert dist["t0"] == 0
        assert dist["t1"] == 0
        assert dist["t2"] == 0

    def test_unknown_tier_is_silently_ignored(self):
        """BB: record() with unknown tier does not raise and does not affect counts."""
        tel = RoutingTelemetry()
        bad_decision = RoutingDecision(tier="TX", model="none", rationale="bad")
        tel.record(bad_decision)  # must not raise
        dist = tel.get_distribution()
        assert dist["t0"] == 0
        assert dist["t1"] == 0
        assert dist["t2"] == 0

    def test_reset_clears_local_counters(self):
        """BB: reset() brings all local counters back to zero."""
        tel = RoutingTelemetry()
        tel.record(_decision("T0"))
        tel.record(_decision("T1"))
        tel.reset()
        dist = tel.get_distribution()
        assert dist["t0"] == 0
        assert dist["t1"] == 0
        assert dist["t2"] == 0

    def test_redis_incr_called_on_record_when_redis_available(self):
        """BB: When Redis is provided, record() calls incr on the correct key."""
        redis = _make_redis()
        tel = RoutingTelemetry(redis_client=redis)
        tel.record(_decision("T0"))
        redis.incr.assert_called_once_with(T0_KEY)

    def test_redis_incr_failure_falls_back_to_local(self):
        """BB: Redis.incr() failure is swallowed; local counter incremented as fallback."""
        redis = _make_redis(incr_ok=False)
        tel = RoutingTelemetry(redis_client=redis)
        tel.record(_decision("T1"))
        # Local counter should have been incremented as fallback
        assert tel._local_counts["T1"] == 1

    def test_log_tier_report_does_not_raise_on_io_error(self):
        """BB: log_tier_report() swallows I/O errors and returns normally."""
        tel = RoutingTelemetry()
        tel.record(_decision("T0"))
        with patch("core.routing.routing_telemetry.open", side_effect=OSError("no disk")):
            tel.log_tier_report()  # must not raise


# ===========================================================================
# WB1: TierClassifier — White Box
# ===========================================================================

class TestTierClassifierWhiteBox:
    """White-box tests for TierClassifier — internal rule priority and path coverage."""

    def test_T0_path_has_zero_external_calls(self):
        """WB: T0 classification does not call any external dependency."""
        clf = TierClassifier()
        # Pure Python — verifying no import errors or side-effects
        d = clf.classify({"type": "health_check"})
        assert d.tier == "T0"
        assert d.model == TIER_MODELS["T0"]

    def test_T0_type_check_fires_before_T0_payload_key_check(self):
        """WB: Task type T0 match short-circuits before payload key scan."""
        clf = TierClassifier()
        # If type is T0, we never reach payload key scanning
        # Verify: health_check (T0 type) + lookup_key (T0 key) → still T0 by TYPE rule
        d = clf.classify({"type": "health_check", "lookup_key": "x"})
        assert "health_check" in d.rationale  # type rule fired, not key rule

    def test_T1_type_check_fires_only_after_T0_misses(self):
        """WB: T1 classification only triggers when T0 checks both fail."""
        clf = TierClassifier()
        d = clf.classify({"type": "email_draft"})
        assert d.tier == "T1"
        assert "email_draft" in d.rationale

    def test_T2_model_is_claude_opus(self):
        """WB: T2 decision always selects claude-opus-4-6 (most capable model)."""
        clf = TierClassifier()
        d = clf.classify({"type": "complex_novel_reasoning_task_xyz"})
        assert d.tier == "T2"
        assert d.model == "claude-opus-4-6"

    def test_routing_decision_is_cached_false_by_default(self):
        """WB: RoutingDecision.is_cached defaults to False for direct classifier calls."""
        d = RoutingDecision(tier="T0", model="python_function", rationale="test")
        assert d.is_cached is False

    def test_all_T0_types_yield_python_function_model(self):
        """WB: Every type in T0_TYPES maps to python_function (zero LLM cost)."""
        clf = TierClassifier()
        for task_type in T0_TYPES:
            d = clf.classify({"type": task_type})
            assert d.tier == "T0", f"{task_type} should be T0"
            assert d.model == "python_function", f"{task_type} should use python_function"

    def test_all_T1_types_yield_gemini_flash_model(self):
        """WB: Every type in T1_TYPES maps to gemini-flash."""
        clf = TierClassifier()
        for task_type in T1_TYPES:
            d = clf.classify({"type": task_type})
            assert d.tier == "T1", f"{task_type} should be T1"
            assert d.model == "gemini-flash", f"{task_type} should use gemini-flash"


# ===========================================================================
# WB2: TierRouter — White Box
# ===========================================================================

class TestTierRouterWhiteBox:
    """White-box tests for TierRouter — cache key format, error paths."""

    def test_cache_key_format(self):
        """WB: Cache key uses genesis:routing:<12-char MD5 hash>."""
        import hashlib
        router = TierRouter()
        task_type = "health_check"
        expected_hash = hashlib.md5(task_type.encode()).hexdigest()[:12]
        expected_key = f"genesis:routing:{expected_hash}"

        redis = _make_redis(get_return=None)
        router._redis = redis
        router.route({"type": task_type})

        # setex was called — check the key argument
        call_args = redis.setex.call_args[0]
        assert call_args[0] == expected_key

    def test_redis_setex_failure_does_not_raise(self):
        """WB: Redis.setex() exception is silently swallowed; decision still returned."""
        redis = _make_redis(get_return=None)
        redis.setex.side_effect = ConnectionError("timeout")
        router = TierRouter(redis_client=redis)

        d = router.route({"type": "health_check"})
        assert d.tier == "T0"  # routing succeeded despite setex failure

    def test_default_classifier_created_when_none_supplied(self):
        """WB: TierRouter creates a TierClassifier instance when none is injected."""
        router = TierRouter()
        assert isinstance(router.classifier, TierClassifier)

    def test_default_redis_is_none(self):
        """WB: TierRouter._redis is None by default (caching disabled)."""
        router = TierRouter()
        assert router._redis is None


# ===========================================================================
# WB3: TierExecutor — White Box
# ===========================================================================

class TestTierExecutorWhiteBox:
    """White-box tests for TierExecutor — model guard, degradation logic."""

    @pytest.mark.asyncio
    async def test_T2_enriched_payload_contains_tier_guard(self):
        """WB: T2 dispatch adds tier='T2' and model='claude-opus-4-6' to payload (loop guard)."""
        captured_payload = {}

        async def capture_dispatch(payload: dict) -> dict:
            captured_payload.update(payload)
            return {"status": "ok"}

        executor = TierExecutor(dispatch_fn=capture_dispatch)
        await executor.execute({"type": "deep_task", "task_id": "wb-t2"}, _decision("T2"))

        assert captured_payload["tier"] == "T2"
        assert captured_payload["model"] == "claude-opus-4-6"

    @pytest.mark.asyncio
    async def test_T0_missing_handler_degrades_to_T1_path(self):
        """WB: T0 task with no registered handler falls through to _execute_t1."""
        executor = TierExecutor()  # no dispatch_fn
        # 'cache_get' is in T0_TYPES but has no registered handler in T0_HANDLER_REGISTRY
        result = await executor.execute(
            {"type": "cache_get", "task_id": "wb-degrade"},
            _decision("T0"),
        )
        # Degraded to T1 stub
        assert result["tier"] == "T1"
        assert result["model"] == "gemini-flash"

    @pytest.mark.asyncio
    async def test_log_execution_failure_does_not_propagate(self):
        """WB: _log_execution() swallows OS errors; execute() still returns result."""
        executor = TierExecutor()
        with patch("core.routing.tier_executor.open", side_effect=OSError("disk full")):
            result = await executor.execute(
                {"type": "health_check", "task_id": "wb-log"},
                _decision("T0"),
            )
        # health_check handler registered — result OK despite log failure
        assert result["status"] == "ok"
        assert result["tier"] == "T0"

    def test_T0_handler_registry_contains_builtin_handlers(self):
        """WB: T0_HANDLER_REGISTRY has health_check, log_event, format_transform registered."""
        assert "health_check" in T0_HANDLER_REGISTRY
        assert "log_event" in T0_HANDLER_REGISTRY
        assert "format_transform" in T0_HANDLER_REGISTRY

    def test_register_t0_handler_decorator_adds_to_registry(self):
        """WB: @register_t0_handler adds function to T0_HANDLER_REGISTRY under given key."""
        test_key = "_wb_test_register_handler"

        @register_t0_handler(test_key)
        def _my_test_handler(payload: dict) -> dict:
            return {"status": "ok", "tier": "T0", "model": "python_function",
                    "task_id": "x", "output": {}}

        assert test_key in T0_HANDLER_REGISTRY
        assert T0_HANDLER_REGISTRY[test_key] is _my_test_handler

        # Clean up to avoid polluting other tests
        del T0_HANDLER_REGISTRY[test_key]


# ===========================================================================
# WB4: TierRouterInterceptor — White Box
# ===========================================================================

class TestInterceptorWhiteBox:
    """White-box tests for TierRouterInterceptor — stamps, telemetry, error defaults."""

    @pytest.mark.asyncio
    async def test_pre_execute_stamps_tier_model_rationale(self):
        """WB: pre_execute stamps tier, model, routing_rationale onto payload."""
        interceptor = TierRouterInterceptor()
        payload = {"type": "health_check", "task_id": "wb-int-001"}
        result = await interceptor.pre_execute(payload)

        assert result["tier"] == "T0"
        assert result["model"] == "python_function"
        assert "routing_rationale" in result
        assert isinstance(result["routing_rationale"], str)

    @pytest.mark.asyncio
    async def test_pre_execute_stores_last_decision(self):
        """WB: pre_execute stores decision in _last_decision for post_execute to use."""
        interceptor = TierRouterInterceptor()
        await interceptor.pre_execute({"type": "health_check"})
        assert interceptor._last_decision is not None
        assert interceptor._last_decision.tier == "T0"

    @pytest.mark.asyncio
    async def test_pre_execute_router_failure_falls_back_to_T2(self):
        """WB: If router.route() raises, interceptor falls back to T2/Opus safely."""
        mock_router = MagicMock()
        mock_router.route.side_effect = RuntimeError("routing exploded")
        interceptor = TierRouterInterceptor(router=mock_router)

        payload = {"type": "any_task"}
        result = await interceptor.pre_execute(payload)

        assert result["tier"] == "T2"
        assert result["model"] == "claude-opus-4-6"
        assert "Routing error" in result["routing_rationale"]

    @pytest.mark.asyncio
    async def test_post_execute_records_to_telemetry(self):
        """WB: post_execute calls telemetry.record() with the last decision."""
        mock_telemetry = MagicMock()
        interceptor = TierRouterInterceptor(telemetry=mock_telemetry)
        await interceptor.pre_execute({"type": "health_check"})

        await interceptor.post_execute({"status": "ok"}, {"type": "health_check"})

        mock_telemetry.record.assert_called_once()
        recorded = mock_telemetry.record.call_args[0][0]
        assert recorded.tier == "T0"

    @pytest.mark.asyncio
    async def test_post_execute_with_no_decision_does_not_raise(self):
        """WB: post_execute is safe when _last_decision is None (no prior pre_execute)."""
        interceptor = TierRouterInterceptor()
        interceptor._last_decision = None
        await interceptor.post_execute({"status": "ok"}, {})  # must not raise

    @pytest.mark.asyncio
    async def test_on_error_returns_T2_fallback_payload(self):
        """WB: on_error returns correction payload with T2 tier and Opus model."""
        interceptor = TierRouterInterceptor()
        err = ValueError("something went wrong")
        payload = {"type": "some_task"}

        correction = await interceptor.on_error(err, payload)

        assert correction["tier"] == "T2"
        assert correction["model"] == "claude-opus-4-6"
        assert "error" in correction
        assert str(err) in correction["error"]

    @pytest.mark.asyncio
    async def test_on_correction_is_passthrough(self):
        """WB: on_correction returns the correction_payload unchanged."""
        interceptor = TierRouterInterceptor()
        correction_payload = {"error": "retry", "tier": "T2", "foo": "bar"}
        result = await interceptor.on_correction(correction_payload)
        assert result is correction_payload

    def test_interceptor_priority_is_20(self):
        """WB: metadata.priority == 20 (after JIT=10, before business logic=50+)."""
        interceptor = TierRouterInterceptor()
        assert interceptor.metadata.priority == 20

    def test_interceptor_name_is_tier_router(self):
        """WB: metadata.name == 'tier_router'."""
        interceptor = TierRouterInterceptor()
        assert interceptor.metadata.name == "tier_router"


# ---------------------------------------------------------------------------
# VERIFICATION_STAMP
# Story: 4.07
# Verified By: parallel-builder
# Verified At: 2026-02-25T00:00:00Z
# Tests: 52/52
# Coverage: 100%
# ---------------------------------------------------------------------------
