#!/usr/bin/env python3
"""
Tests for Story 4.04 (Track B): StranglerFig — Progressive Migration Wrapper

Black Box tests (BB): verify public contract from the outside
White Box tests (WB): verify internal structure and fail-safe paths

Story: 4.04
File under test: core/routing/strangler_fig.py
"""
import sys
sys.path.insert(0, '/mnt/e/genesis-system')

import pytest
from unittest.mock import AsyncMock, patch, MagicMock

# Always import a fresh WRAPPED_FUNCTIONS state per test module load.
from core.routing.strangler_fig import strangler_fig, get_migration_status, WRAPPED_FUNCTIONS


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _clear_registry():
    """Clear WRAPPED_FUNCTIONS between tests that need a clean slate."""
    WRAPPED_FUNCTIONS.clear()


# ===========================================================================
# Black Box tests
# ===========================================================================

@pytest.mark.asyncio
async def test_bb1_dispatch_fires_and_result_returned():
    """BB1: @strangler_fig wraps async function → dispatch fires, dispatched result returned."""
    _clear_registry()
    dispatch_return = {"status": "completed", "task_id": "abc-123", "output": "intercepted"}

    @strangler_fig("test_type", tier="T1")
    async def my_func(x):
        return {"value": x}

    with patch("core.interceptors.dispatch_to_swarm", new=AsyncMock(return_value=dispatch_return)):
        result = await my_func(42)

    assert result == dispatch_return, (
        f"Expected dispatch result {dispatch_return!r}, got {result!r}"
    )


@pytest.mark.asyncio
async def test_bb2_failsafe_on_dispatch_exception():
    """BB2: dispatch_to_swarm raises → original function result returned (fail-safe)."""
    _clear_registry()

    @strangler_fig("test_type")
    async def safe_func():
        return {"safe": True}

    with patch("core.interceptors.dispatch_to_swarm", new=AsyncMock(side_effect=RuntimeError("boom"))):
        result = await safe_func()

    assert result == {"safe": True}, (
        f"Expected original result on dispatch failure, got {result!r}"
    )


def test_bb3_registry_updated_after_decoration():
    """BB3: WRAPPED_FUNCTIONS has an entry for the decorated function after @strangler_fig."""
    _clear_registry()

    @strangler_fig("registry_task", tier="T0")
    async def registered_func():
        pass

    assert "registered_func" in WRAPPED_FUNCTIONS, (
        "WRAPPED_FUNCTIONS should contain 'registered_func' after decoration"
    )
    entry = WRAPPED_FUNCTIONS["registered_func"]
    assert entry["task_type"] == "registry_task"
    assert entry["tier"] == "T0"


def test_bb4_get_migration_status_returns_correct_count():
    """BB4: get_migration_status() returns correct count and function details."""
    _clear_registry()

    @strangler_fig("alpha_task")
    async def alpha(): pass

    @strangler_fig("beta_task", tier="T2")
    async def beta(): pass

    status = get_migration_status()
    assert status["total_wrapped"] == 2, (
        f"Expected 2 wrapped functions, got {status['total_wrapped']}"
    )
    assert "alpha" in status["functions"]
    assert "beta" in status["functions"]
    assert status["functions"]["beta"]["tier"] == "T2"


# ===========================================================================
# White Box tests
# ===========================================================================

@pytest.mark.asyncio
async def test_wb1_functools_wraps_preserves_name_and_doc():
    """WB1: functools.wraps preserves __name__ and __doc__ on the wrapper."""
    _clear_registry()

    @strangler_fig("doc_test")
    async def well_documented():
        """This is the original docstring."""
        return {}

    assert well_documented.__name__ == "well_documented", (
        f"__name__ should be 'well_documented', got '{well_documented.__name__}'"
    )
    assert well_documented.__doc__ == "This is the original docstring.", (
        f"__doc__ not preserved: {well_documented.__doc__!r}"
    )


@pytest.mark.asyncio
async def test_wb2_failsafe_catches_import_error():
    """WB2: Fail-safe catches ImportError when dispatch_to_swarm is not importable."""
    _clear_registry()

    @strangler_fig("import_safe_task")
    async def fragile_func():
        return {"fragile": "result"}

    # Simulate the import itself failing by patching the import machinery
    with patch.dict("sys.modules", {"core.interceptors": None}):
        result = await fragile_func()

    assert result == {"fragile": "result"}, (
        f"Fail-safe must return original result on ImportError, got {result!r}"
    )


@pytest.mark.asyncio
async def test_wb3_tier_included_in_task_payload_when_provided():
    """WB3: tier parameter is included in task_payload forwarded to dispatch_to_swarm."""
    _clear_registry()
    captured_payloads = []

    async def capture_dispatch(task_payload, tier=None):
        captured_payloads.append((task_payload, tier))
        return {"status": "completed"}

    @strangler_fig("tiered_task", tier="T1")
    async def tiered_func():
        return {}

    with patch("core.interceptors.dispatch_to_swarm", new=capture_dispatch):
        await tiered_func()

    assert len(captured_payloads) == 1
    payload, kw_tier = captured_payloads[0]
    assert payload.get("tier") == "T1", (
        f"tier must be in task_payload when decorator tier='T1', got: {payload!r}"
    )
    assert kw_tier == "T1", (
        f"tier kwarg to dispatch_to_swarm must be 'T1', got {kw_tier!r}"
    )


@pytest.mark.asyncio
async def test_wb4_original_args_not_forwarded_only_metadata():
    """WB4: Original function args/kwargs NOT passed to dispatch — only metadata sent."""
    _clear_registry()
    captured_payloads = []

    async def capture_dispatch(task_payload, tier=None):
        captured_payloads.append(task_payload)
        return {"status": "completed"}

    @strangler_fig("metadata_task")
    async def func_with_args(secret_arg, keyword=None):
        return {"processed": True}

    with patch("core.interceptors.dispatch_to_swarm", new=capture_dispatch):
        await func_with_args("SECRET_VALUE", keyword="SENSITIVE")

    assert len(captured_payloads) == 1
    payload = captured_payloads[0]

    # Raw args/kwargs must NOT appear in the payload
    assert "secret_arg" not in payload, "Raw positional arg name must not be in payload"
    assert "SECRET_VALUE" not in str(payload), "Raw positional arg value must not be in payload"
    assert "SENSITIVE" not in str(payload), "Raw kwarg value must not be in payload"

    # Only metadata shape is allowed
    assert payload["args_count"] == 1
    assert payload["kwargs_keys"] == ["keyword"]
    assert payload["source"] == "strangler_fig"
    assert payload["wrapped_function"] == "func_with_args"


# ===========================================================================
# Additional edge-case coverage
# ===========================================================================

@pytest.mark.asyncio
async def test_no_tier_omits_tier_from_payload():
    """When tier is None, the 'tier' key must NOT appear in task_payload."""
    _clear_registry()
    captured_payloads = []

    async def capture_dispatch(task_payload, tier=None):
        captured_payloads.append((task_payload, tier))
        return {"status": "completed"}

    @strangler_fig("no_tier_task")
    async def no_tier_func():
        return {}

    with patch("core.interceptors.dispatch_to_swarm", new=capture_dispatch):
        await no_tier_func()

    payload, kw_tier = captured_payloads[0]
    assert "tier" not in payload, (
        f"'tier' key must be absent when decorator tier is None, got {payload!r}"
    )
    assert kw_tier is None


@pytest.mark.asyncio
async def test_original_result_in_payload():
    """original_result from the wrapped function appears in task_payload."""
    _clear_registry()
    captured_payloads = []

    async def capture_dispatch(task_payload, tier=None):
        captured_payloads.append(task_payload)
        return {"status": "completed"}

    @strangler_fig("result_pass_task")
    async def func_returning_value():
        return {"key": "value", "count": 99}

    with patch("core.interceptors.dispatch_to_swarm", new=capture_dispatch):
        await func_returning_value()

    payload = captured_payloads[0]
    assert payload["original_result"] == {"key": "value", "count": 99}


def test_registry_entry_has_module_and_qualname():
    """WRAPPED_FUNCTIONS entry includes module and qualname for introspection."""
    _clear_registry()

    @strangler_fig("introspection_task")
    async def introspectable():
        pass

    entry = WRAPPED_FUNCTIONS["introspectable"]
    assert "module" in entry, "Entry must include 'module'"
    assert "qualname" in entry, "Entry must include 'qualname'"
    assert entry["qualname"].endswith("introspectable")


if __name__ == "__main__":
    import asyncio

    async def run_manual():
        """Quick manual smoke test."""
        print("Running manual smoke tests for Story 4.04...")

        # BB3 smoke
        _clear_registry()

        @strangler_fig("smoke_task", tier="T1")
        async def smoke(): return {"ok": True}

        assert "smoke" in WRAPPED_FUNCTIONS
        print("[PASS] BB3: registry updated")

        # BB4 smoke
        status = get_migration_status()
        assert status["total_wrapped"] == 1
        print("[PASS] BB4: get_migration_status() correct count")

        # BB2 fail-safe smoke
        with patch("core.interceptors.dispatch_to_swarm", new=AsyncMock(side_effect=RuntimeError)):
            result = await smoke()
        assert result == {"ok": True}
        print("[PASS] BB2: fail-safe on dispatch exception")

        # WB1 functools.wraps smoke
        assert smoke.__name__ == "smoke"
        print("[PASS] WB1: functools.wraps preserves __name__")

        print("\nAll manual smoke tests passed — Story 4.04 (Track B)")

    asyncio.run(run_manual())
