"""Test contract-driven output governance conformance and integration wiring.

Spec refs:
- docs/proposals/execution-architecture-refactor.md#tests-required/consumer-conformance-tests
- docs/proposals/execution-architecture-refactor.md#tests-required/output-policy-tests
- docs/proposals/execution-architecture-refactor.md#execution-contract-specific-design-rules/output-governance
- docs/proposals/execution-architecture-refactor.md#execution-contract-specific-design-rules/model-visible-runtime-context
- docs/proposals/execution-architecture-refactor.md#implementation-handoff

Test-only constraint:
- No product implementation beyond minimal helpers/fixtures
"""

from __future__ import annotations

from datetime import datetime, timezone
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch

import pytest
from click.testing import CliRunner
from returns.result import Failure, Success

from anima.app.result_service import DefaultResultService
from anima.core.contracts import (
    CONTRACT_SCHEMA_VERSION,
    ContractIdentity,
    ContractResolverProvenance,
    ContractSourceHashes,
    PersonaProvenance,
    PersonaSpawnability,
    RuntimeModelContext,
    RuntimeOutputPolicy,
    RuntimeToolAccess,
)
from anima.core.execution import (
    ExecutionIntent,
    ExecutionIntentDefaults,
    ExecutionIntentOverrides,
    ResolvedExecutionContract,
    ResolvedLifecyclePolicy,
    ResolvedRuntime,
    RuntimeOutputPolicyView,
    OutputPolicyProviderProfileView,
    OutputPolicyPerCallCapView,
    OutputPolicyPerTurnBudgetView,
    OutputPolicyTruncationStrategyView,
    resolve_execution_contract,
)
from anima.core.execution.contracts import (
    ExecutionLaunchView,
    ExecutionToolAccessView,
    ExecutionSpawnabilityView,
    ExecutionControlView,
)
from anima.core.schemas import (
    AgentResult,
    AgentState,
    AgentStateData,
    CompletionPolicy,
    JobSpec,
    LifecycleType,
    NotFoundError,
    PersonaSpec,
    SideEffectPolicy,
    TokenUsage,
)
from anima.shell.cli import main
from anima.shell.cli_commands.inspect_collect import _shape_terminal_content
from anima.shell.agent import AgentRunner
from anima.shell.mcp_serve import ServeMCPServer
from anima.shell.mcp_serve import _shape_terminal_output
from anima.shell.mcp_toolset_policy import (
    MAX_TOOL_RESULT_TEXT_CHARS,
    _ToolOutputBudgetState,
    begin_tool_output_budget_scope,
    end_tool_output_budget_scope,
)
from anima.shell.state import create_agent_state, write_result, write_state


# =============================================================================
# SECTION 1: Output Governance Consumer Conformance Fixtures
# =============================================================================


def make_output_governance_contract(
    instance_id: str = "inst_output_test",
    provider_profile: str = "openai-compatible",
    per_call_tool_output_chars: int | None = 12000,
    per_turn_tool_output_chars: int | None = 24000,
    truncation_strategy: str = "paginate_then_truncate",
    effective_model: str = "gpt-4o",
    work_dir: str | None = None,
    project_root: str | None = None,
    filesystem_scope: str | None = None,
    effective_tools: tuple[str, ...] = ("read_file", "write_file"),
) -> ResolvedExecutionContract:
    """Create a contract fixture for output governance tests and integrations."""
    return ResolvedExecutionContract(
        identity=ContractIdentity(
            instance_id=instance_id,
            contract_version=CONTRACT_SCHEMA_VERSION,
            resolver=ContractResolverProvenance(
                resolver_version="resolver/0.1.0",
                source_hashes=ContractSourceHashes(
                    cli_digest="sha256:output_test",
                ),
            ),
        ),
        runtime=ResolvedRuntime(
            effective_model=effective_model,
            work_dir=work_dir,
            project_root=project_root,
            filesystem_scope=filesystem_scope,
        ),
        tool_access=RuntimeToolAccess(
            declared_persona_tools=effective_tools,
            requested_cli_tools=effective_tools,
            requested_mcp_tools=(),
            effective_tools=effective_tools,
            effective_presets=("default",),
            effective_side_effect_policy=SideEffectPolicy.allow,
        ),
        persona_provenance=PersonaProvenance(
            source="file",
            reusable_persona_ref="output_test@v1",
            materialization_status="materialized",
            inline_behavior="reject_spawn",
            child_inheritance_policy="inherit_reusable_persona_ref",
            spawnability=PersonaSpawnability(can_spawn=True),
        ),
        lifecycle_policy=ResolvedLifecyclePolicy(
            lifecycle=LifecycleType.ephemeral,
            completion_policy=CompletionPolicy.explicit,
        ),
        output_policy=RuntimeOutputPolicy(
            provider_profile=provider_profile,
            per_call_tool_output_chars=per_call_tool_output_chars,
            per_turn_tool_output_chars=per_turn_tool_output_chars,
            truncation_strategy=truncation_strategy,
        ),
        model_context=RuntimeModelContext(
            values={
                "instance_id": instance_id,
                "effective_model": effective_model,
                "work_dir": work_dir,
                "project_root": project_root,
                "filesystem_scope": filesystem_scope,
                "effective_tool_count": len(effective_tools),
            }
        ),
    )


def _make_contract(
    *,
    instance_id: str,
    per_call_chars: int | None,
    per_turn_chars: int | None,
    truncation_strategy: str,
    work_dir: str | None = None,
    project_root: str | None = None,
) -> ResolvedExecutionContract:
    return make_output_governance_contract(
        instance_id=instance_id,
        provider_profile="openai-compatible",
        per_call_tool_output_chars=per_call_chars,
        per_turn_tool_output_chars=per_turn_chars,
        truncation_strategy=truncation_strategy,
        work_dir=work_dir,
        project_root=project_root,
        effective_tools=(),
    )


# =============================================================================
# SECTION 2: Consumer Conformance Tests
# =============================================================================


class TestOutputPolicyViewConformance:
    """Tests proving output_policy consumers read consistent views.

    Spec source: consumer-conformance-tests
    All consumers (tool adapters, telemetry, control) derive decisions from
    the same contract fixture's output_policy section.
    """

    def test_tool_adapter_reads_provider_profile(self) -> None:
        """Tool adapters read provider_profile from output_policy for profile selection."""
        contract = make_output_governance_contract(
            provider_profile="anthropic-claude",
        )

        # Consumer: tool_output assembly reads provider_profile
        view: OutputPolicyProviderProfileView = contract.output_policy
        assert view.provider_profile == "anthropic-claude"

    def test_tool_adapter_reads_per_call_cap(self) -> None:
        """Tool adapters read per_call_tool_output_chars for single-result capping."""
        contract = make_output_governance_contract(
            per_call_tool_output_chars=8000,
        )

        # Consumer: per-call budget enforcer reads per_call_tool_output_chars
        view: OutputPolicyPerCallCapView = contract.output_policy
        assert view.per_call_tool_output_chars == 8000

    def test_tool_adapter_reads_per_turn_budget(self) -> None:
        """Tool adapters read per_turn_tool_output_chars for cumulative turn budget."""
        contract = make_output_governance_contract(
            per_turn_tool_output_chars=16000,
        )

        # Consumer: per-turn budget aggregator reads per_turn_tool_output_chars
        view: OutputPolicyPerTurnBudgetView = contract.output_policy
        assert view.per_turn_tool_output_chars == 16000

    def test_tool_adapter_reads_truncation_strategy(self) -> None:
        """Tool adapters read truncation_strategy for failsafe behavior."""
        contract = make_output_governance_contract(
            truncation_strategy="paginate_then_truncate",
        )

        # Consumer: output shaper reads truncation_strategy
        view: OutputPolicyTruncationStrategyView = contract.output_policy
        assert view.truncation_strategy == "paginate_then_truncate"

    def test_all_output_policy_views_from_same_contract(self) -> None:
        """All output-policy views derive from the same contract fixture."""
        contract = make_output_governance_contract(
            provider_profile="openai-gpt4",
            per_call_tool_output_chars=10000,
            per_turn_tool_output_chars=20000,
            truncation_strategy="truncate_raw",
        )

        # All consumers see consistent values from the same contract
        full_view: RuntimeOutputPolicyView = contract.output_policy
        assert full_view.provider_profile == "openai-gpt4"
        assert full_view.per_call_tool_output_chars == 10000
        assert full_view.per_turn_tool_output_chars == 20000
        assert full_view.truncation_strategy == "truncate_raw"

    def test_combined_output_governance_view_reads_all_sections(self) -> None:
        """ExecutionOutputGovernanceView reads both output_policy and model_context."""
        contract = make_output_governance_contract(
            per_call_tool_output_chars=5000,
            work_dir="/workspace/test",
            effective_model="gpt-4o-mini",
        )

        # output_policy section
        assert contract.output_policy.per_call_tool_output_chars == 5000

        # model_context section
        assert contract.model_context.values.get("work_dir") == "/workspace/test"
        assert contract.model_context.values.get("effective_model") == "gpt-4o-mini"


class TestModelContextViewConformance:
    """Tests proving model_context consumers read consistent runtime facts.

    Spec source: consumer-conformance-tests, model-visible-runtime-context
    """

    def test_model_context_includes_instance_id(self) -> None:
        """Model context always includes instance_id."""
        contract = make_output_governance_contract(instance_id="inst_12345")

        assert contract.model_context.values.get("instance_id") == "inst_12345"

    def test_model_context_includes_effective_model(self) -> None:
        """Model context includes effective_model for provider selection."""
        contract = make_output_governance_contract(effective_model="claude-3-opus")

        assert contract.model_context.values.get("effective_model") == "claude-3-opus"

    def test_model_context_includes_effective_tool_count(self) -> None:
        """Model context includes effective_tool_count for tool scope visibility."""
        contract = make_output_governance_contract(
            effective_tools=("read_file", "write_file", "execute_shell"),
        )

        assert contract.model_context.values.get("effective_tool_count") == 3

    def test_model_context_values_are_primitive_or_none(self) -> None:
        """Model context values must be primitive types suitable for prompt injection."""
        contract = make_output_governance_contract(
            work_dir="/workspace",
            effective_tools=(
                "read_file",
                "write_file",
                "execute_shell",
                "mcp_tool",
                "other",
            ),
        )

        for key, value in contract.model_context.values.items():
            assert value is None or isinstance(value, (str, int, float, bool)), (
                f"model_context[{key}] must be primitive, got {type(value)}"
            )


# =============================================================================
# SECTION 3: Output Policy Regressions
# =============================================================================


class TestCurrentTurnOversizedToolOutput:
    """Regressions for oversized current-turn tool output caps.

    Spec source: output-policy-tests
    - current-turn oversized tool output cannot exceed provider-safe caps
    - per-call and per-turn budget enforcement work together
    """

    def test_tool_output_cannot_exceed_per_call_cap(self) -> None:
        """Per-tool-result cap is enforced for oversized output."""
        contract = make_output_governance_contract(
            per_call_tool_output_chars=100,
        )

        # Consumer reads cap from contract
        per_call_cap = contract.output_policy.per_call_tool_output_chars
        assert per_call_cap == 100

        # Simulate oversized output
        oversized_output = "x" * 500
        truncated = (
            oversized_output[:per_call_cap] if per_call_cap else oversized_output
        )

        assert len(truncated) <= (per_call_cap or len(oversized_output))

    def test_turn_budget_cumulative_enforcement(self) -> None:
        """Current-turn cumulative budget caps total tool output."""
        contract = make_output_governance_contract(
            per_call_tool_output_chars=200,
            per_turn_tool_output_chars=500,
        )

        # Budget scope enforces cumulative cap
        token = begin_tool_output_budget_scope(
            per_call_cap_bytes=contract.output_policy.per_call_tool_output_chars or 200,
            per_turn_cap_bytes=contract.output_policy.per_turn_tool_output_chars or 500,
        )
        try:
            state = token.var.get()
            assert isinstance(state, _ToolOutputBudgetState)

            # Verify initial state has correct caps
            assert state.per_call_cap_bytes == 200
            assert state.per_turn_cap_bytes == 500

            # Verify per_call cannot exceed per_turn
            # If per_call > per_turn, effective per_call is capped at per_turn
            assert state.per_call_cap_bytes <= state.per_turn_cap_bytes
        finally:
            end_tool_output_budget_scope(token)

    def test_per_call_and_per_turn_work_together(self) -> None:
        """Per-call and per-turn budgets are both enforced in contract."""
        contract = make_output_governance_contract(
            per_call_tool_output_chars=1000,
            per_turn_tool_output_chars=5000,
        )

        # Both caps are present in contract
        assert contract.output_policy.per_call_tool_output_chars == 1000
        assert contract.output_policy.per_turn_tool_output_chars == 5000

        # Both must be non-None for enforcement
        assert contract.output_policy.per_call_tool_output_chars is not None
        assert contract.output_policy.per_turn_tool_output_chars is not None

    def test_none_caps_allow_unbounded_output(self) -> None:
        """Per-call/per_turn as None means no cap (provider-specific handling)."""
        contract = make_output_governance_contract(
            per_call_tool_output_chars=None,
            per_turn_tool_output_chars=None,
        )

        # None caps are valid (no enforcement, provider handles limits)
        assert contract.output_policy.per_call_tool_output_chars is None
        assert contract.output_policy.per_turn_tool_output_chars is None


class TestPaginationPreference:
    """Regressions for pagination preference over truncation.

    Spec source: output-policy-tests
    - paginated/summary responses are favored over raw truncation when supported
    """

    def test_truncation_strategy_indicates_pagination_preference(self) -> None:
        """truncation_strategy='paginate_then_truncate' signals pagination support."""
        contract = make_output_governance_contract(
            truncation_strategy="paginate_then_truncate",
        )

        assert contract.output_policy.truncation_strategy == "paginate_then_truncate"

        # Consumer can check for pagination support
        prefers_pagination = "paginate" in contract.output_policy.truncation_strategy
        assert prefers_pagination is True

    def test_truncation_strategy_raw_means_no_pagination(self) -> None:
        """truncation_strategy='truncate' or 'truncate_raw' means no pagination."""
        contract = make_output_governance_contract(
            truncation_strategy="truncate_raw",
        )

        prefers_pagination = "paginate" in contract.output_policy.truncation_strategy
        assert prefers_pagination is False

    def test_provider_profile_influences_pagination_capability(self) -> None:
        """Provider profile can indicate native pagination support."""
        contract = make_output_governance_contract(
            provider_profile="anthropic-claude",
            truncation_strategy="paginate_then_truncate",
        )

        # Contract carries provider profile for pagination capability lookup
        assert contract.output_policy.provider_profile == "anthropic-claude"
        assert contract.output_policy.truncation_strategy == "paginate_then_truncate"


class TestProviderSafeFallback:
    """Regressions for provider-safe fallback behavior.

    Output governance falls back gracefully when provider caps are missing or
    exceeded.
    """

    def test_missing_provider_profile_uses_hard_defaults(self) -> None:
        """When provider_profile unknown, hard defaults are used."""
        contract = make_output_governance_contract(
            provider_profile="unknown-provider",
            per_call_tool_output_chars=10000,
            per_turn_tool_output_chars=20000,
        )

        # Contract still has caps from resolution
        assert contract.output_policy.per_call_tool_output_chars == 10000
        assert contract.output_policy.per_turn_tool_output_chars == 20000

    def test_provider_cap_dominates_configured_cap(self) -> None:
        """Provider hard cap must be enforced even if config specifies larger."""
        # This is enforced by resolve_tool_output_budget_contract in model_config
        # Here we test that the contract carries the resolved values
        contract = make_output_governance_contract(
            per_call_tool_output_chars=50000,  # Config wants 50KB
            # But provider hard cap (resolved elsewhere) is 8KB
        )

        # Contract holds the resolved value (after provider cap enforcement)
        assert contract.output_policy.per_call_tool_output_chars == 50000


# =============================================================================
# SECTION 4: Filesystem/Project-Root Hints Conformance
# =============================================================================


class TestFilesystemProjectRootHints:
    """Fixtures for filesystem/project-root hints only when effective access exists.

    Spec source: model-visible-runtime-context
    - project root/work dir
    - accessible filesystem scope
    - effective tools summary
    - how to progressively inspect large outputs

    Key invariant: runtime context hints appear only when effective filesystem
    or shell access exists.
    """

    def test_work_dir_appears_when_set(self) -> None:
        """work_dir appears in model_context when explicitly set."""
        contract = make_output_governance_contract(
            work_dir="/workspace/task",
        )

        assert contract.model_context.values.get("work_dir") == "/workspace/task"
        assert contract.runtime.work_dir == "/workspace/task"

    def test_project_root_appears_when_set(self) -> None:
        """project_root appears in model_context when explicitly set."""
        contract = make_output_governance_contract(
            project_root="/project/root",
        )

        assert contract.model_context.values.get("project_root") == "/project/root"
        assert contract.runtime.project_root == "/project/root"

    def test_filesystem_scope_appears_when_set(self) -> None:
        """filesystem_scope appears in model_context when set."""
        contract = make_output_governance_contract(
            filesystem_scope="project-root",
        )

        assert contract.model_context.values.get("filesystem_scope") == "project-root"

    def test_work_dir_absent_when_not_set(self) -> None:
        """work_dir is None when not set (no filesystem access)."""
        contract = make_output_governance_contract(
            work_dir=None,
        )

        assert contract.model_context.values.get("work_dir") is None
        assert contract.runtime.work_dir is None

    def test_no_runtime_paths_when_no_filesystem_access(self) -> None:
        """When no filesystem tools, runtime context has no path hints."""
        # Contract with no filesystem tools
        contract = make_output_governance_contract(
            effective_tools=("browser.snapshot",),  # No filesystem tools
            work_dir=None,
            project_root=None,
        )

        # model_context still has these fields, but they're None
        assert contract.model_context.values.get("work_dir") is None
        assert contract.model_context.values.get("project_root") is None

    def test_effective_tool_count_always_present(self) -> None:
        """effective_tool_count is always in model_context regardless of tools."""
        contract = make_output_governance_contract(
            effective_tools=(),
        )

        # Tool count is always present, even if zero
        assert "effective_tool_count" in contract.model_context.values
        assert contract.model_context.values.get("effective_tool_count") == 0


class TestRuntimeContextConditionalOnEffectiveAccess:
    """Tests proving runtime context is conditional on effective access.

    This tests the key invariant: filesystem/project-root hints appear only
    when there's actual access (tools allowing filesystem operations).
    """

    def test_filesystem_hints_present_with_filesystem_tools(self) -> None:
        """When filesystem tools are present, paths can be shown."""
        contract = make_output_governance_contract(
            effective_tools=("read_file", "write_file"),
            work_dir="/workspace",
            project_root="/project",
        )

        # Filesystem tools + paths means agent has filesystem access
        assert contract.model_context.values.get("work_dir") == "/workspace"
        assert contract.model_context.values.get("project_root") == "/project"

    def test_filesystem_hints_absent_without_filesystem_tools(self) -> None:
        """When no filesystem tools, paths should be None to avoid confusing agent."""
        contract = make_output_governance_contract(
            effective_tools=("browser.snapshot",),
            work_dir=None,
            project_root=None,
        )

        # No filesystem tools -> paths are None
        assert contract.model_context.values.get("work_dir") is None
        assert contract.model_context.values.get("project_root") is None


# =============================================================================
# SECTION 5: Cross-Consumer View Consistency
# =============================================================================


class TestConsumerViewConsistency:
    """Tests proving all consumers see consistent views from same contract.

    Spec source: consumer-conformance-tests
    """

    def test_launch_view_sees_same_runtime_as_control_view(self) -> None:
        """Launch and Control consumers see same runtime values."""
        contract = make_output_governance_contract(
            effective_model="gpt-4o",
            per_call_tool_output_chars=8000,
        )

        # Launch view
        launch_view: ExecutionLaunchView = contract
        assert launch_view.runtime.effective_model == "gpt-4o"
        assert launch_view.output_policy.per_call_tool_output_chars == 8000

        # Control view sees same runtime
        control_view: ExecutionControlView = contract
        assert control_view.identity.instance_id == contract.identity.instance_id
        # Control view doesn't have output_policy directly, but sees same identity

    def test_tool_access_view_sees_effective_tools(self) -> None:
        """Tool access view shows effective tools for gating decisions."""
        contract = make_output_governance_contract(
            effective_tools=("read_file", "write_file", "execute_shell"),
        )

        tool_view: ExecutionToolAccessView = contract
        assert tool_view.tool_access.effective_tools == (
            "read_file",
            "write_file",
            "execute_shell",
        )

    def test_spawnability_view_sees_provenance(self) -> None:
        """Spawnability view shows persona provenance for child-agent decisions."""
        contract = make_output_governance_contract()

        spawn_view: ExecutionSpawnabilityView = contract
        assert spawn_view.persona_provenance.spawnability.can_spawn is True
        assert spawn_view.persona_provenance.source == "file"

    def test_all_views_read_same_instance_id(self) -> None:
        """All consumer views reference the same instance_id."""
        contract = make_output_governance_contract(instance_id="inst_shared_id")

        # All views share identity
        assert contract.identity.instance_id == "inst_shared_id"
        assert contract.model_context.values.get("instance_id") == "inst_shared_id"


# =============================================================================
# SECTION 6: Output Policy Integration with Budget Scope
# =============================================================================


class TestOutputPolicyBudgetScopeIntegration:
    """Tests proving output_policy integrates with budget scope management.

    The contract's output_policy values drive begin_tool_output_budget_scope.
    """

    def test_budget_scope_uses_contract_caps(self) -> None:
        """Budget scope is initialized with contract's per_call/per_turn caps."""
        contract = make_output_governance_contract(
            per_call_tool_output_chars=12000,
            per_turn_tool_output_chars=24000,
        )

        # Budget scope uses contract values
        per_call = contract.output_policy.per_call_tool_output_chars
        per_turn = contract.output_policy.per_turn_tool_output_chars

        assert per_call == 12000
        assert per_turn == 24000

        # Integration: these values initialize budget scope
        token = begin_tool_output_budget_scope(
            per_call_cap_bytes=per_call or MAX_TOOL_RESULT_TEXT_CHARS,
            per_turn_cap_bytes=per_turn or (MAX_TOOL_RESULT_TEXT_CHARS * 5),
        )
        try:
            state = token.var.get()
            assert isinstance(state, _ToolOutputBudgetState)
            assert state.per_call_cap_bytes == per_call
            assert state.per_turn_cap_bytes == per_turn
        finally:
            end_tool_output_budget_scope(token)

    def test_budget_across_calls_in_same_turn(self) -> None:
        """Multiple tool calls in same turn share per_turn budget."""
        contract = make_output_governance_contract(
            per_call_tool_output_chars=500,
            per_turn_tool_output_chars=1000,
        )

        per_call = contract.output_policy.per_call_tool_output_chars
        per_turn = contract.output_policy.per_turn_tool_output_chars

        token = begin_tool_output_budget_scope(
            per_call_cap_bytes=per_call or 500,
            per_turn_cap_bytes=per_turn or 1000,
        )
        try:
            state = token.var.get()
            assert isinstance(state, _ToolOutputBudgetState)

            # First call uses budget
            first_output_size = 400
            # In real code, this would be applied via _apply_current_turn_budget

            # Second call must respect remaining budget
            remaining = state.per_turn_cap_bytes - first_output_size
            second_call_cap = min(state.per_call_cap_bytes, remaining)

            assert second_call_cap <= state.per_call_cap_bytes
            assert first_output_size + second_call_cap <= state.per_turn_cap_bytes
        finally:
            end_tool_output_budget_scope(token)


# =============================================================================
# SECTION 7: Edge Cases and Error Handling
# =============================================================================


class TestOutputPolicyEdgeCases:
    """Edge cases for output policy handling."""

    def test_truncation_strategy_required(self) -> None:
        """truncation_strategy is required (non-optional)."""
        # RuntimeOutputPolicy has truncation_strategy as required
        contract = make_output_governance_contract()

        assert contract.output_policy.truncation_strategy is not None
        assert len(contract.output_policy.truncation_strategy) > 0

    def test_provider_profile_required(self) -> None:
        """provider_profile is required."""
        contract = make_output_governance_contract()

        assert contract.output_policy.provider_profile is not None
        assert len(contract.output_policy.provider_profile) > 0

    def test_can_create_contract_with_zero_tools(self) -> None:
        """Contract with zero tools is valid."""
        contract = make_output_governance_contract(effective_tools=())

        assert contract.tool_access.effective_tools == ()
        assert contract.model_context.values.get("effective_tool_count") == 0

    def test_model_context_values_include_spawnable(self) -> None:
        """model_context values are primitive types suitable for prompt injection."""
        contract = make_output_governance_contract()

        # All values must be primitive types (str, int, float, bool, None)
        for key, value in contract.model_context.values.items():
            assert value is None or isinstance(value, (str, int, float, bool)), (
                f"model_context[{key}] must be primitive, got {type(value)}"
            )


class TestOutputPolicyImmutability:
    """Tests proving output_policy is immutable after contract resolution."""

    def test_output_policy_is_frozen(self) -> None:
        """output_policy cannot be mutated after contract creation."""
        contract = make_output_governance_contract(
            per_call_tool_output_chars=10000,
        )

        with pytest.raises(Exception):
            contract.output_policy.per_call_tool_output_chars = 20000  # type: ignore[misc]

    def test_contract_is_frozen(self) -> None:
        """ResolvedExecutionContract is frozen."""
        contract = make_output_governance_contract()

        with pytest.raises(Exception):
            contract.runtime.effective_model = "other-model"  # type: ignore[misc]

    def test_model_context_is_frozen(self) -> None:
        """model_context cannot be replaced after contract creation."""
        contract = make_output_governance_contract()

        # model_context itself is frozen, but we can verify the values dict is not assignable
        with pytest.raises(Exception):
            contract.model_context = RuntimeModelContext(values={"new": "value"})  # type: ignore[misc]


def _create_completed_agent(
    *,
    instance_id: str,
    resolved_contract: ResolvedExecutionContract,
) -> AgentStateData:
    create_result = create_agent_state(
        instance_id=instance_id,
        persona_name="test-persona",
        job=JobSpec(persona_ref="test@test", instance_id=instance_id),
        resolved_contract=resolved_contract,
    )
    assert isinstance(create_result, Success), create_result.failure()
    state = create_result.unwrap()
    completed_state = AgentStateData(
        **{
            **state.model_dump(),
            "state": AgentState.completed,
            "updated_at": datetime.now(timezone.utc).isoformat(),
        }
    )
    write_state_result = write_state(state.instance_id, completed_state, "finish", {})
    assert isinstance(write_state_result, Success), write_state_result.failure()
    return completed_state


def test_result_service_projects_char_cap_and_strategy() -> None:
    contract = _make_contract(
        instance_id="inst_projection",
        per_call_chars=250,
        per_turn_chars=180,
        truncation_strategy="paginate_then_truncate",
    )

    projection = DefaultResultService().project_terminal_result_policy(
        contract,
        adapter_surface="cli_terminal_presenter",
    )

    assert projection.terminal_result_char_cap == 180
    assert projection.prefer_pagination is True
    assert projection.output_policy.provider_profile == "openai-compatible"


def test_contract_work_dir_drives_cli_relative_result_ref_read(
    tmp_state_dir: Path,
    tmp_path: Path,
) -> None:
    runner = CliRunner()
    work_dir = tmp_path / "contract-work"
    work_dir.mkdir(parents=True)
    result_file = work_dir / "relative-result.txt"
    result_file.write_text("contract anchored content", encoding="utf-8")

    contract = _make_contract(
        instance_id="inst_cli_contract",
        per_call_chars=200,
        per_turn_chars=300,
        truncation_strategy="paginate_then_truncate",
        work_dir=str(work_dir),
    )
    completed_agent = _create_completed_agent(
        instance_id="inst_cli_contract",
        resolved_contract=contract,
    )

    write_result_outcome = write_result(
        completed_agent.instance_id,
        AgentResult(
            final_state=AgentState.completed,
            token_usage=TokenUsage(prompt_tokens=1, completion_tokens=2),
            result_ref="relative-result.txt",
        ),
    )
    assert isinstance(write_result_outcome, Success), write_result_outcome.failure()

    collect_result = runner.invoke(
        main,
        ["collect", completed_agent.instance_id],
        env={"ANIMA_STATE": str(tmp_state_dir)},
    )

    assert collect_result.exit_code == 0
    assert "Result Content:" in collect_result.output
    assert "contract anchored content" in collect_result.output


@pytest.mark.asyncio
async def test_mcp_collect_uses_contract_policy_for_terminal_output_cap(
    tmp_path: Path,
) -> None:
    contract = _make_contract(
        instance_id="inst_mcp_collect",
        per_call_chars=48,
        per_turn_chars=90,
        truncation_strategy="truncate",
    )
    terminal_state = AgentStateData(
        instance_id="inst_mcp_collect",
        trace_id="trace_test",
        persona_name="test_persona",
        spec_id="spec_001",
        spec_digest="abc123",
        model="test-model",
        state=AgentState.completed,
        lifecycle=LifecycleType.ephemeral,
        parent_id=None,
        children=[],
        depth=0,
        max_depth=3,
        pid=12345,
        mcp_socket=None,
        created_at="2024-01-01T00:00:00Z",
        updated_at="2024-01-01T00:00:00Z",
        token_usage=TokenUsage(),
        tools_called=[],
        current_turn=0,
        max_turns=100,
        budget_total=100000,
        grace_budget=1000,
        error=None,
        result_ref=None,
    )

    agent_dir = tmp_path / terminal_state.instance_id
    agent_dir.mkdir(parents=True)
    (agent_dir / "result.json").write_text(
        '{"output": "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"}',
        encoding="utf-8",
    )

    server = ServeMCPServer(host="127.0.0.1", port=8420)
    with (
        patch("anima.shell.mcp_serve.find_agent_by_prefix") as mock_find,
        patch("anima.shell.mcp_serve.get_agent_dir") as mock_get_dir,
        patch("anima.shell.mcp_serve.load_persisted_or_legacy_contract") as mock_load,
    ):
        mock_find.return_value = Success(terminal_state)
        mock_get_dir.return_value = agent_dir
        mock_load.return_value = contract

        result = await server._handle_collect(terminal_state.instance_id)

    assert result["status"] == "completed"
    assert isinstance(result["output"], str)
    assert len(result["output"]) <= 48
    assert result["output"].endswith("...[truncated for terminal output]")


def test_resolve_execution_contract_persists_output_policy_defaults() -> None:
    intent = ExecutionIntent(
        persona_spec=PersonaSpec(
            id="worker",
            description="Worker",
            prompt="Work",
            model="gpt-4o",
        ),
        job_spec=JobSpec(
            persona_ref="worker@v1",
            instance_id="inst_output_defaults",
        ),
        overrides=ExecutionIntentOverrides(),
        defaults=ExecutionIntentDefaults(
            default_output_provider_profile="provider-x",
            default_per_call_tool_output_chars=321,
            default_per_turn_tool_output_chars=654,
            default_truncation_strategy="truncate",
        ),
    )

    contract = resolve_execution_contract(intent)

    assert contract.output_policy.provider_profile == "provider-x"
    assert contract.output_policy.per_call_tool_output_chars == 321
    assert contract.output_policy.per_turn_tool_output_chars == 654
    assert contract.output_policy.truncation_strategy == "truncate"


def test_cli_and_mcp_collect_shape_from_same_contract_output_policy() -> None:
    contract = _make_contract(
        instance_id="inst_cross_consumer",
        per_call_chars=64,
        per_turn_chars=128,
        truncation_strategy="truncate",
    )
    source_output = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" * 2

    cli_projection = DefaultResultService().project_terminal_result_policy(
        contract,
        adapter_surface="cli_terminal_presenter",
    )
    mcp_projection = DefaultResultService().project_terminal_result_policy(
        contract,
        adapter_surface="mcp_response_presenter",
    )

    cli_output, cli_truncated = _shape_terminal_content(
        source_output,
        result_projection=cli_projection,
    )
    mcp_output, mcp_truncated = _shape_terminal_output(
        source_output,
        projection=mcp_projection,
    )

    assert cli_projection.output_policy.per_call_tool_output_chars == 64
    assert mcp_projection.output_policy.per_call_tool_output_chars == 64
    assert cli_projection.output_policy.per_turn_tool_output_chars == 128
    assert mcp_projection.output_policy.per_turn_tool_output_chars == 128
    assert cli_output == mcp_output
    assert cli_truncated is True
    assert mcp_truncated is True


@pytest.mark.asyncio
async def test_agent_runner_reads_budget_caps_from_contract_snapshot(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    persona = PersonaSpec(
        id="worker",
        description="Worker",
        prompt="Do work",
        model="gpt-4o",
    )
    runner = AgentRunner(
        instance_id="inst_snapshot_caps",
        task="task",
        persona=persona,
        job=JobSpec(persona_ref="worker@v1", instance_id="inst_snapshot_caps"),
    )
    snapshot_contract = _make_contract(
        instance_id="inst_snapshot_caps",
        per_call_chars=777,
        per_turn_chars=999,
        truncation_strategy="truncate",
    )

    async def _fake_create_agent(*args: object, **kwargs: object) -> Success[object]:
        del args, kwargs
        return Success(SimpleNamespace(model="gpt-4o"))

    def _forbidden_budget_resolve(*args: object, **kwargs: object) -> dict[str, object]:
        del args, kwargs
        raise AssertionError("runtime-local budget resolution must not run")

    monkeypatch.setattr("anima.shell.agent.create_agent", _fake_create_agent)
    monkeypatch.setattr(
        "anima.shell.agent.read_contract_snapshot",
        lambda _instance_id: Success(snapshot_contract),
    )
    monkeypatch.setattr(
        "anima.shell.agent.resolve_tool_output_budget_profile",
        _forbidden_budget_resolve,
    )

    result = await runner._create_runtime_agent(persona, global_config=None)

    assert isinstance(result, Success)
    assert runner._tool_output_per_call_cap_bytes == 777
    assert runner._tool_output_per_turn_cap_bytes == 999


@pytest.mark.asyncio
async def test_agent_runner_uses_runtime_local_fallback_when_snapshot_missing(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    persona = PersonaSpec(
        id="worker",
        description="Worker",
        prompt="Do work",
        model="gpt-4o",
    )
    runner = AgentRunner(
        instance_id="inst_legacy_fallback",
        task="task",
        persona=persona,
        job=JobSpec(persona_ref="worker@v1", instance_id="inst_legacy_fallback"),
    )

    async def _fake_create_agent(*args: object, **kwargs: object) -> Success[object]:
        del args, kwargs
        return Success(SimpleNamespace(model="gpt-4o"))

    monkeypatch.setattr("anima.shell.agent.create_agent", _fake_create_agent)
    monkeypatch.setattr(
        "anima.shell.agent.read_contract_snapshot",
        lambda instance_id: Failure(NotFoundError(f"missing: {instance_id}")),
    )
    monkeypatch.setattr(
        "anima.shell.agent.resolve_tool_output_budget_profile",
        lambda **kwargs: {
            "per_call_cap_bytes": 222,
            "per_turn_cap_bytes": 444,
            "provider_profile_name": "fallback-profile",
            "model_profile_name": None,
        },
    )

    result = await runner._create_runtime_agent(persona, global_config=None)

    assert isinstance(result, Success)
    assert runner._tool_output_per_call_cap_bytes == 222
    assert runner._tool_output_per_turn_cap_bytes == 444
