"""
Tests for the Action Monad.

Verifies the three Monad Laws:
1. Left Identity:   return a >>= f  ≡  f a
2. Right Identity:  m >>= return    ≡  m
3. Associativity:   (m >>= f) >>= g ≡  m >>= (λx. f x >>= g)

Also tests Action composition utilities.
"""

import asyncio

import pytest

from mcard.ptr.core.action import (
    Action,
    ActionContext,
    ActionEffect,
    ActionResult,
    identity_action,
    kleisli_compose,
    parallel,
    sequence,
    verify_associativity,
    verify_left_identity,
    verify_right_identity,
)


async def async_pure(value):
    """Async helper to return a pure ActionResult."""
    return ActionResult.pure(value)


async def async_fail(error):
    """Async helper to return a failed ActionResult."""
    return ActionResult.fail(error)


@pytest.fixture
def ctx():
    """Create a test ActionContext."""
    return ActionContext(
        session_id="test-session",
        agent_id="test-agent",
        config={"debug": True},
        params={"input_value": 42},
    )


class TestActionResult:
    """Tests for ActionResult."""

    def test_pure_creates_success(self):
        """pure should create a successful result."""
        result = ActionResult.pure(42)
        assert result.success is True
        assert result.value == 42
        assert result.error is None

    def test_fail_creates_failure(self):
        """fail should create a failed result."""
        result = ActionResult.fail("Something went wrong")
        assert result.success is False
        assert result.value is None
        assert result.error == "Something went wrong"

    def test_map_transforms_value(self):
        """map should apply function to value."""
        result = ActionResult.pure(10).map(lambda x: x * 2)
        assert result.success is True
        assert result.value == 20

    def test_map_propagates_failure(self):
        """map should not apply function on failure."""
        result = ActionResult.fail("error").map(lambda x: x * 2)
        assert result.success is False
        assert result.error == "error"


class TestActionEffect:
    """Tests for ActionEffect."""

    def test_empty_effect(self):
        """empty should create an empty effect."""
        effect = ActionEffect.empty()
        assert effect.memory_updates == []
        assert effect.tool_calls == []
        assert effect.logs == []
        assert effect.tokens == {"prompt": 0, "completion": 0}

    def test_effect_addition(self):
        """Adding effects should combine them."""
        e1 = ActionEffect(logs=["log1"], tokens={"prompt": 10, "completion": 5})
        e2 = ActionEffect(logs=["log2"], tokens={"prompt": 20, "completion": 10})

        combined = e1 + e2

        assert combined.logs == ["log1", "log2"]
        assert combined.tokens == {"prompt": 30, "completion": 15}


class TestActionContext:
    """Tests for ActionContext."""

    def test_with_params(self, ctx):
        """with_params should create new context with merged params."""
        new_ctx = ctx.with_params({"extra": "value"})

        # Original unchanged
        assert "extra" not in ctx.params

        # New has merged params
        assert new_ctx.params["input_value"] == 42
        assert new_ctx.params["extra"] == "value"


class TestActionMonad:
    """Core tests for Action monad operations."""

    @pytest.mark.asyncio
    async def test_pure_returns_value(self, ctx):
        """pure should lift a value into the action."""
        action = Action.pure(42)
        result = await action.execute(ctx)

        assert result.success is True
        assert result.value == 42

    @pytest.mark.asyncio
    async def test_fail_returns_error(self, ctx):
        """fail should create a failing action."""
        action = Action.fail("Test error")
        result = await action.execute(ctx)

        assert result.success is False
        assert result.error == "Test error"

    @pytest.mark.asyncio
    async def test_bind_chains_actions(self, ctx):
        """bind should chain actions together."""
        action = Action.pure(10).bind(lambda x: Action.pure(x * 2))
        result = await action.execute(ctx)

        assert result.success is True
        assert result.value == 20

    @pytest.mark.asyncio
    async def test_bind_propagates_failure(self, ctx):
        """bind should propagate failures."""
        action = Action.fail("error").bind(lambda x: Action.pure(x * 2))
        result = await action.execute(ctx)

        assert result.success is False
        assert result.error == "error"

    @pytest.mark.asyncio
    async def test_map_transforms_result(self, ctx):
        """map should apply a pure function to the result."""
        action = Action.pure(5).map(lambda x: x + 3)
        result = await action.execute(ctx)

        assert result.success is True
        assert result.value == 8

    @pytest.mark.asyncio
    async def test_then_sequences_actions(self, ctx):
        """then should sequence actions, ignoring the first result."""
        action = Action.pure("ignored").then(Action.pure("result"))
        result = await action.execute(ctx)

        assert result.success is True
        assert result.value == "result"

    @pytest.mark.asyncio
    async def test_ask_returns_context(self, ctx):
        """ask should return the current context."""
        action = Action.ask()
        result = await action.execute(ctx)

        assert result.success is True
        assert result.value.session_id == "test-session"
        assert result.value.agent_id == "test-agent"

    @pytest.mark.asyncio
    async def test_asks_applies_function(self, ctx):
        """asks should apply a function to the context."""
        action = Action.asks(lambda c: c.params["input_value"])
        result = await action.execute(ctx)

        assert result.success is True
        assert result.value == 42

    @pytest.mark.asyncio
    async def test_tell_emits_effect(self, ctx):
        """tell should emit an effect."""
        effect = ActionEffect(logs=["test log"])
        action = Action.tell(effect)
        result = await action.execute(ctx)

        assert result.success is True
        assert "test log" in result.effects.logs

    @pytest.mark.asyncio
    async def test_log_convenience(self, ctx):
        """log should be a convenient way to emit log messages."""
        action = Action.log("Hello, World!")
        result = await action.execute(ctx)

        assert result.success is True
        assert "Hello, World!" in result.effects.logs

    @pytest.mark.asyncio
    async def test_from_sync_wraps_function(self, ctx):
        """from_sync should wrap a synchronous function."""
        action = Action.from_sync(lambda c: c.params["input_value"] * 2)
        result = await action.execute(ctx)

        assert result.success is True
        assert result.value == 84

    @pytest.mark.asyncio
    async def test_from_async_wraps_coroutine(self, ctx):
        """from_async should wrap an async function."""

        async def async_fn(c):
            await asyncio.sleep(0.001)
            return c.params["input_value"] + 8

        action = Action.from_async(async_fn)
        result = await action.execute(ctx)

        assert result.success is True
        assert result.value == 50


class TestMonadLaws:
    """Verify the three Monad Laws."""

    @pytest.mark.asyncio
    async def test_left_identity(self, ctx):
        """Left Identity: pure(a).bind(f) == f(a)"""
        a = 21
        f = lambda x: Action.pure(x * 2)

        left = await Action.pure(a).bind(f).execute(ctx)
        right = await f(a).execute(ctx)

        assert left.success == right.success
        assert left.value == right.value == 42

    @pytest.mark.asyncio
    async def test_left_identity_with_helper(self, ctx):
        """Left Identity verified with helper function."""
        result = await verify_left_identity(21, lambda x: Action.pure(x * 2), ctx)
        assert result is True

    @pytest.mark.asyncio
    async def test_right_identity(self, ctx):
        """Right Identity: m.bind(pure) == m"""
        m = Action.pure(100)

        left = await m.bind(Action.pure).execute(ctx)
        right = await m.execute(ctx)

        assert left.success == right.success
        assert left.value == right.value == 100

    @pytest.mark.asyncio
    async def test_right_identity_with_helper(self, ctx):
        """Right Identity verified with helper function."""
        m = Action.pure(100)
        result = await verify_right_identity(m, ctx)
        assert result is True

    @pytest.mark.asyncio
    async def test_associativity(self, ctx):
        """Associativity: (m.bind(f)).bind(g) == m.bind(λx. f(x).bind(g))"""
        m = Action.pure(5)
        f = lambda x: Action.pure(x + 3)
        g = lambda x: Action.pure(x * 2)

        # (m >>= f) >>= g
        left = await m.bind(f).bind(g).execute(ctx)

        # m >>= (λx. f x >>= g)
        right = await m.bind(lambda x: f(x).bind(g)).execute(ctx)

        assert left.success == right.success
        assert left.value == right.value == 16  # (5 + 3) * 2

    @pytest.mark.asyncio
    async def test_associativity_with_helper(self, ctx):
        """Associativity verified with helper function."""
        m = Action.pure(5)
        f = lambda x: Action.pure(x + 3)
        g = lambda x: Action.pure(x * 2)

        result = await verify_associativity(m, f, g, ctx)
        assert result is True


class TestActionComposition:
    """Tests for action composition utilities."""

    @pytest.mark.asyncio
    async def test_sequence_executes_in_order(self, ctx):
        """sequence should execute actions in order and collect results."""
        actions = [
            Action.pure(1),
            Action.pure(2),
            Action.pure(3),
        ]

        result = await sequence(actions).execute(ctx)

        assert result.success is True
        assert result.value == [1, 2, 3]

    @pytest.mark.asyncio
    async def test_sequence_stops_on_failure(self, ctx):
        """sequence should stop on first failure."""
        actions = [
            Action.pure(1),
            Action.fail("error"),
            Action.pure(3),
        ]

        result = await sequence(actions).execute(ctx)

        assert result.success is False
        assert result.error == "error"

    @pytest.mark.asyncio
    async def test_sequence_empty(self, ctx):
        """sequence of empty list should return empty list."""
        result = await sequence([]).execute(ctx)

        assert result.success is True
        assert result.value == []

    @pytest.mark.asyncio
    async def test_parallel_executes_concurrently(self, ctx):
        """parallel should execute actions concurrently."""

        async def slow_value(v):
            await asyncio.sleep(0.01)
            return v

        actions = [Action.from_async(lambda c, v=i: slow_value(v)) for i in range(3)]

        result = await parallel(actions).execute(ctx)

        assert result.success is True
        assert set(result.value) == {0, 1, 2}

    @pytest.mark.asyncio
    async def test_parallel_empty(self, ctx):
        """parallel of empty list should return empty list."""
        result = await parallel([]).execute(ctx)

        assert result.success is True
        assert result.value == []

    @pytest.mark.asyncio
    async def test_kleisli_compose(self, ctx):
        """kleisli_compose should compose two Kleisli arrows."""
        f = lambda x: Action.pure(x + 1)
        g = lambda x: Action.pure(x * 2)

        # g ∘ f
        composed = kleisli_compose(f, g)

        result = await composed(5).execute(ctx)

        assert result.success is True
        assert result.value == 12  # (5 + 1) * 2

    @pytest.mark.asyncio
    async def test_identity_action(self, ctx):
        """identity_action should be the identity for Kleisli composition."""
        f = lambda x: Action.pure(x * 2)

        # identity . f == f
        left = await kleisli_compose(f, identity_action())(5).execute(ctx)
        right = await f(5).execute(ctx)

        assert left.value == right.value == 10


class TestActionEffectAccumulation:
    """Tests for effect accumulation across action chains."""

    @pytest.mark.asyncio
    async def test_effects_accumulate_through_bind(self, ctx):
        """Effects should accumulate through bind operations."""
        action = (
            Action.log("step 1")
            .then(Action.log("step 2"))
            .then(Action.log("step 3"))
            .then(Action.pure("done"))
        )

        result = await action.execute(ctx)

        assert result.success is True
        assert len(result.effects.logs) == 3
        assert result.effects.logs == ["step 1", "step 2", "step 3"]

    @pytest.mark.asyncio
    async def test_execution_time_tracked(self, ctx):
        """Execution time should be tracked in effects."""

        async def slow_action(c):
            await asyncio.sleep(0.05)
            return "done"

        action = Action.from_async(slow_action)
        result = await action.execute(ctx)

        assert result.success is True
        assert result.effects.execution_time >= 50  # At least 50ms


if __name__ == "__main__":
    pytest.main([__file__, "-v"])


# ============ ContractAction Tests ============

from mcard.ptr.core.action import (
    ActionContract,
    Condition,
    ContractAction,
)


class TestCondition:
    """Tests for Condition dataclass."""

    def test_condition_check_returns_true(self):
        """Condition check should return True when satisfied."""
        cond = Condition(name="positive", expression="x > 0", check=lambda x: x > 0)
        assert cond.check(5) is True

    def test_condition_check_returns_false(self):
        """Condition check should return False when not satisfied."""
        cond = Condition(name="positive", expression="x > 0", check=lambda x: x > 0)
        assert cond.check(-1) is False


class TestActionContract:
    """Tests for ActionContract dataclass."""

    def test_contract_with_preconditions(self):
        """Contract should hold preconditions."""
        contract = ActionContract(
            preconditions=[
                Condition(
                    name="has_session",
                    expression="ctx.session_id != ''",
                    check=lambda c: c.session_id != "",
                )
            ],
            postconditions=[],
        )
        assert len(contract.preconditions) == 1
        assert contract.preconditions[0].name == "has_session"

    def test_contract_with_postconditions(self):
        """Contract should hold postconditions."""
        contract = ActionContract(
            preconditions=[],
            postconditions=[
                Condition(
                    name="success",
                    expression="result.success",
                    check=lambda r: r.success,
                )
            ],
        )
        assert len(contract.postconditions) == 1


class TestContractAction:
    """Tests for ContractAction class."""

    @pytest.fixture
    def ctx(self):
        """Create a test ActionContext."""
        return ActionContext(
            session_id="test-session",
            agent_id="test-agent",
            config={"debug": True},
            params={"input_value": 42, "authorized": True},
        )

    @pytest.fixture
    def basic_contract(self):
        """Create a basic contract with simple conditions."""
        return ActionContract(
            preconditions=[
                Condition(
                    name="has_session",
                    expression="ctx.session_id != ''",
                    check=lambda c: c.session_id != "",
                )
            ],
            postconditions=[
                Condition(
                    name="success",
                    expression="result.success",
                    check=lambda r: r.success,
                )
            ],
        )

    @pytest.mark.asyncio
    async def test_contract_property(self, ctx, basic_contract):
        """Contract property should return the contract."""
        action = ContractAction(run=lambda c: async_pure(42), contract=basic_contract)
        assert action.contract == basic_contract

    @pytest.mark.asyncio
    async def test_pure_with_contract(self, ctx):
        """pure_with_contract should create successful action."""
        action = ContractAction.pure_with_contract(42)
        result = await action.execute(ctx)

        assert result.success is True
        assert result.value == 42

    @pytest.mark.asyncio
    async def test_from_async_with_contract(self, ctx, basic_contract):
        """from_async_with_contract should wrap async function."""

        async def compute(c):
            return c.params["input_value"] * 2

        action = ContractAction.from_async_with_contract(compute, basic_contract)
        result = await action.execute(ctx)

        assert result.success is True
        assert result.value == 84


class TestPreconditionVerification:
    """Tests for precondition verification."""

    @pytest.fixture
    def ctx(self):
        return ActionContext(
            session_id="test-session",
            agent_id="test-agent",
            params={"authorized": True},
        )

    @pytest.mark.asyncio
    async def test_all_preconditions_satisfied(self, ctx):
        """Should pass when all preconditions are satisfied."""
        contract = ActionContract(
            preconditions=[
                Condition(
                    name="has_session",
                    expression="ctx.session_id != ''",
                    check=lambda c: c.session_id != "",
                ),
                Condition(
                    name="has_agent",
                    expression="ctx.agent_id != ''",
                    check=lambda c: c.agent_id != "",
                ),
            ],
            postconditions=[],
        )

        action = ContractAction(run=lambda c: async_pure(42), contract=contract)
        verification = action.verify_preconditions(ctx)

        assert verification.phase == "pre"
        assert verification.all_satisfied is True
        assert len(verification.conditions) == 2
        assert all(c.satisfied for c in verification.conditions)

    @pytest.mark.asyncio
    async def test_precondition_not_satisfied(self, ctx):
        """Should fail when a precondition is not satisfied."""
        contract = ActionContract(
            preconditions=[
                Condition(
                    name="has_session",
                    expression="ctx.session_id != ''",
                    check=lambda c: c.session_id != "",
                ),
                Condition(
                    name="has_secret",
                    expression="ctx.secrets.api_key exists",
                    check=lambda c: bool(c.secrets.get("api_key")),
                ),
            ],
            postconditions=[],
        )

        action = ContractAction(run=lambda c: async_pure(42), contract=contract)
        verification = action.verify_preconditions(ctx)

        assert verification.all_satisfied is False
        assert verification.conditions[0].satisfied is True
        assert verification.conditions[1].satisfied is False

    @pytest.mark.asyncio
    async def test_precondition_error_handling(self, ctx):
        """Should handle errors in precondition checks gracefully."""
        contract = ActionContract(
            preconditions=[
                Condition(
                    name="throws_error",
                    expression="will throw",
                    check=lambda c: (_ for _ in ()).throw(Exception("Check failed")),
                )
            ],
            postconditions=[],
        )

        action = ContractAction(run=lambda c: async_pure(42), contract=contract)
        verification = action.verify_preconditions(ctx)

        assert verification.all_satisfied is False
        assert verification.conditions[0].satisfied is False
        assert verification.conditions[0].error is not None


class TestPostconditionVerification:
    """Tests for postcondition verification."""

    @pytest.fixture
    def ctx(self):
        return ActionContext(session_id="test-session", agent_id="test-agent")

    @pytest.mark.asyncio
    async def test_all_postconditions_satisfied(self, ctx):
        """Should pass when all postconditions are satisfied."""
        contract = ActionContract(
            preconditions=[],
            postconditions=[
                Condition(
                    name="success",
                    expression="result.success",
                    check=lambda r: r.success,
                ),
                Condition(
                    name="positive",
                    expression="result.value > 0",
                    check=lambda r: (r.value or 0) > 0,
                ),
            ],
        )

        action = ContractAction(run=lambda c: async_pure(42), contract=contract)
        result = await action.execute(ctx)
        verification = action.verify_postconditions(result)

        assert verification.phase == "post"
        assert verification.all_satisfied is True

    @pytest.mark.asyncio
    async def test_postcondition_not_satisfied(self, ctx):
        """Should fail when a postcondition is not satisfied."""
        contract = ActionContract(
            preconditions=[],
            postconditions=[
                Condition(
                    name="is_even",
                    expression="result.value % 2 == 0",
                    check=lambda r: (r.value or 0) % 2 == 0,
                )
            ],
        )

        action = ContractAction(run=lambda c: async_pure(41), contract=contract)
        result = await action.execute(ctx)
        verification = action.verify_postconditions(result)

        assert verification.all_satisfied is False
        assert verification.conditions[0].satisfied is False


class TestVCardPairGeneration:
    """Tests for VCard pair generation."""

    @pytest.fixture
    def ctx(self):
        return ActionContext(
            session_id="test-session",
            agent_id="test-agent",
            params={"authorized": True},
        )

    @pytest.mark.asyncio
    async def test_vcard_pair_on_success(self, ctx):
        """Should generate VCard pair on successful execution."""
        contract = ActionContract(
            preconditions=[
                Condition(
                    name="has_session",
                    expression="ctx.session_id != ''",
                    check=lambda c: c.session_id != "",
                )
            ],
            postconditions=[
                Condition(
                    name="success",
                    expression="result.success",
                    check=lambda r: r.success,
                )
            ],
        )

        action = ContractAction(run=lambda c: async_pure(42), contract=contract)
        exec_result = await action.execute_with_contract(ctx)

        assert exec_result.result.success is True
        assert exec_result.result.value == 42

        # VCard pair should be generated
        assert "vcard:pre:" in exec_result.vcard_pair.pre_condition_vcard
        assert "vcard:post:" in exec_result.vcard_pair.post_condition_vcard
        assert exec_result.vcard_pair.action_hash
        assert exec_result.vcard_pair.linked_at > 0

        # Verification results
        assert exec_result.pre_verification.all_satisfied is True
        assert exec_result.post_verification.all_satisfied is True

    @pytest.mark.asyncio
    async def test_vcard_pair_on_precondition_failure(self, ctx):
        """Should stop and return incomplete VCard pair on precondition failure."""
        contract = ActionContract(
            preconditions=[
                Condition(name="impossible", expression="false", check=lambda c: False)
            ],
            postconditions=[],
        )

        action = ContractAction(run=lambda c: async_pure(42), contract=contract)
        exec_result = await action.execute_with_contract(ctx)

        assert exec_result.result.success is False
        assert "impossible" in exec_result.result.error

        # PreCondition VCard exists, PostCondition is empty
        assert "vcard:pre:" in exec_result.vcard_pair.pre_condition_vcard
        assert exec_result.vcard_pair.post_condition_vcard == ""

        assert exec_result.pre_verification.all_satisfied is False

    @pytest.mark.asyncio
    async def test_vcard_pair_on_action_failure(self, ctx):
        """Should generate VCard pair even when action fails."""
        contract = ActionContract(
            preconditions=[
                Condition(name="always_true", expression="true", check=lambda c: True)
            ],
            postconditions=[
                Condition(
                    name="success",
                    expression="result.success",
                    check=lambda r: r.success,
                )
            ],
        )

        action = ContractAction(
            run=lambda c: async_fail("Action failed"), contract=contract
        )
        exec_result = await action.execute_with_contract(ctx)

        assert exec_result.result.success is False

        # Both VCards generated
        assert "vcard:pre:" in exec_result.vcard_pair.pre_condition_vcard
        assert "vcard:post:" in exec_result.vcard_pair.post_condition_vcard

        # Pre passes, post verification is empty (action failed)
        assert exec_result.pre_verification.all_satisfied is True
        assert exec_result.post_verification.all_satisfied is False


class TestContractComposition:
    """Tests for contract-aware action composition."""

    @pytest.fixture
    def ctx(self):
        return ActionContext(session_id="test-session", agent_id="test-agent")

    @pytest.mark.asyncio
    async def test_bind_with_contract_chains_actions(self, ctx):
        """bind_with_contract should chain contract-aware actions."""
        contract1 = ActionContract(
            preconditions=[
                Condition(
                    name="has_session",
                    expression="ctx.session_id != ''",
                    check=lambda c: c.session_id != "",
                )
            ],
            postconditions=[],
        )
        contract2 = ActionContract(preconditions=[], postconditions=[])

        action1 = ContractAction(run=lambda c: async_pure(42), contract=contract1)

        def action2_fn(n):
            return ContractAction(
                run=lambda c: async_pure(f"Result: {n}"), contract=contract2
            )

        composed = action1.bind_with_contract(action2_fn)
        result = await composed.execute(ctx)

        assert result.success is True
        assert result.value == "Result: 42"

    @pytest.mark.asyncio
    async def test_bind_with_contract_propagates_failure(self, ctx):
        """bind_with_contract should propagate failure."""
        contract = ActionContract(preconditions=[], postconditions=[])

        action1 = ContractAction(
            run=lambda c: async_fail("first failed"), contract=contract
        )

        def action2_fn(n):
            return ContractAction.pure_with_contract(f"Result: {n}")

        composed = action1.bind_with_contract(action2_fn)
        result = await composed.execute(ctx)

        assert result.success is False
        assert result.error == "first failed"


class TestActionHash:
    """Tests for action hash generation."""

    def test_consistent_hash_for_same_contract(self):
        """Should generate consistent hash for same contract."""
        contract = ActionContract(
            preconditions=[
                Condition(name="cond_a", expression="a", check=lambda c: True),
                Condition(name="cond_b", expression="b", check=lambda c: True),
            ],
            postconditions=[
                Condition(name="post_a", expression="pa", check=lambda r: True)
            ],
        )

        action1 = ContractAction(run=lambda c: async_pure(1), contract=contract)
        action2 = ContractAction(run=lambda c: async_pure(2), contract=contract)

        assert action1.get_action_hash() == action2.get_action_hash()

    def test_different_hash_for_different_contracts(self):
        """Should generate different hash for different contracts."""
        contract1 = ActionContract(
            preconditions=[
                Condition(name="cond_a", expression="a", check=lambda c: True)
            ],
            postconditions=[],
        )
        contract2 = ActionContract(
            preconditions=[
                Condition(name="cond_b", expression="b", check=lambda c: True)
            ],
            postconditions=[],
        )

        action1 = ContractAction(run=lambda c: async_pure(1), contract=contract1)
        action2 = ContractAction(run=lambda c: async_pure(1), contract=contract2)

        assert action1.get_action_hash() != action2.get_action_hash()


class TestRealWorldScenarios:
    """Real-world scenario tests for ContractAction."""

    @pytest.fixture
    def authorized_ctx(self):
        return ActionContext(
            session_id="test-session",
            agent_id="test-agent",
            params={"authorized": True, "user_id": "user123"},
        )

    @pytest.fixture
    def unauthorized_ctx(self):
        return ActionContext(
            session_id="test-session",
            agent_id="test-agent",
            params={"authorized": False, "user_id": "user123"},
        )

    @pytest.mark.asyncio
    async def test_api_call_with_valid_auth(self, authorized_ctx):
        """Should validate API call with valid authorization."""
        api_contract = ActionContract(
            preconditions=[
                Condition(
                    name="has_auth",
                    expression="ctx.params.authorized",
                    check=lambda c: c.params.get("authorized", False),
                ),
                Condition(
                    name="valid_session",
                    expression="ctx.session_id != ''",
                    check=lambda c: c.session_id != "",
                ),
            ],
            postconditions=[
                Condition(
                    name="has_data",
                    expression="result.value.data exists",
                    check=lambda r: r.value and "data" in r.value,
                ),
                Condition(
                    name="no_error",
                    expression="result.success",
                    check=lambda r: r.success,
                ),
            ],
        )

        async def mock_api_call(ctx):
            await asyncio.sleep(0.01)  # Simulate network delay
            return {"data": "response from API", "status": 200}

        action = ContractAction.from_async_with_contract(mock_api_call, api_contract)
        exec_result = await action.execute_with_contract(authorized_ctx)

        assert exec_result.result.success is True
        assert exec_result.result.value["data"] == "response from API"
        assert exec_result.pre_verification.all_satisfied is True
        assert exec_result.post_verification.all_satisfied is True

        # VCard pair provides audit evidence
        assert exec_result.vcard_pair.pre_condition_vcard
        assert exec_result.vcard_pair.post_condition_vcard

    @pytest.mark.asyncio
    async def test_api_call_rejected_unauthorized(self, unauthorized_ctx):
        """Should reject unauthorized API calls with VCard evidence."""
        api_contract = ActionContract(
            preconditions=[
                Condition(
                    name="has_auth",
                    expression="ctx.params.authorized",
                    check=lambda c: c.params.get("authorized", False),
                )
            ],
            postconditions=[],
        )

        async def mock_api_call(ctx):
            return {"data": "should not reach here"}

        action = ContractAction.from_async_with_contract(mock_api_call, api_contract)
        exec_result = await action.execute_with_contract(unauthorized_ctx)

        assert exec_result.result.success is False
        assert "has_auth" in exec_result.result.error

        # PreCondition VCard records the rejection
        assert exec_result.vcard_pair.pre_condition_vcard
        assert exec_result.pre_verification.conditions[0].satisfied is False
