"""Test contract for resolver precedence, determinism, legacy compatibility, and persona provenance.

Spec refs:
- docs/proposals/execution-architecture-refactor.md#tests-required/resolution-tests
- docs/proposals/execution-architecture-refactor.md#tests-required/consumer-conformance-tests
- docs/proposals/execution-architecture-refactor.md#tests-required/session-runtime-tests
- docs/proposals/execution-architecture-refactor.md#execution-contract-specific-design-rules/persona-provenance-and-subagent-inheritance

Test-only constraint:
- Do not implement resolver logic beyond minimal fixtures/helpers
"""

import json
from pathlib import Path

import pytest
from returns.result import Failure, Success

from anima.core.contracts import (
    CONTRACT_SCHEMA_VERSION,
    ContractIdentity,
    PersonaProvenance,
    PersonaSpawnability,
    RuntimeModelContext,
    RuntimeOutputPolicy,
    RuntimeToolAccess,
    SessionTransportState,
    SpawnabilityReason,
)
from anima.core.execution import (
    ExecutionIntent,
    ExecutionIntentDefaults,
    ExecutionIntentOverrides,
    ExecutionSessionState,
    ResolvedExecutionContract,
    ResolvedLifecyclePolicy,
    ResolvedRuntime,
    resolve_execution_contract,
)
from anima.core.schemas.enums import CompletionPolicy, LifecycleType, SideEffectPolicy
from anima.core.schemas.models import AgentStateData, JobSpec, PersonaSpec
from anima.shell.contract_compat import load_persisted_or_legacy_contract
from anima.shell.state import (
    create_agent_state,
    get_contract_snapshot_path,
    read_contract_snapshot,
    write_contract_snapshot,
    write_state,
)
from tests.execution_contract_fixtures import (
    EXECUTION_INTENT,
    MUTABLE_AGENT_STATE,
    NON_SPAWNABLE_INLINE_CONTRACT,
    SPAWNABLE_SNAPSHOT_CONTRACT,
)

# Import resolution fixtures
from tests.fixtures.execution_contract_resolution import (
    ConsumerConformanceCase,
    EffectiveContractExpectation,
    LegacyAgentState,
    ResolutionPrecedenceInputs,
    compute_contract_snapshot_hash,
    compute_source_hash,
    contract_to_deterministic_json,
    make_consumer_conformance_fixture,
    make_contract_identity,
    make_file_backed_persona_provenance,
    make_inline_persona_provenance,
    make_legacy_agent_without_contract_snapshot,
    make_materialized_snapshot_persona_provenance,
    make_synthesized_persona_provenance,
)


# =============================================================================
# SECTION 1: Resolution Precedence Tests
# =============================================================================


class TestResolutionPrecedenceMatrix:
    """Tests for precedence: CLI > Persona > Job > Defaults > Runtime Probes.

    Spec source: resolution-tests
    """

    def test_cli_overrides_persona_model(self) -> None:
        """CLI --model flag must override persona.model."""
        # Intent with persona model
        intent = ExecutionIntent(
            persona_spec=PersonaSpec(
                id="worker",
                description="Worker persona",
                prompt="You are a worker.",
                model="gpt-4o-mini",  # Persona model
            ),
            overrides=ExecutionIntentOverrides(
                requested_model="gpt-4o",  # CLI override
            ),
        )

        # Contract should show CLI override wins
        # (This is a fixture test - resolver implementation comes later)
        assert intent.overrides.requested_model == "gpt-4o"
        assert intent.persona_spec is not None
        assert intent.persona_spec.model == "gpt-4o-mini"

    def test_cli_overrides_persona_tools(self) -> None:
        """CLI --tools must override persona.tools in effective tool set."""
        persona_spec = PersonaSpec(
            id="worker",
            description="Worker persona",
            prompt="You are a worker.",
            model="gpt-4o-mini",
            tools={"read_file": "none"},  # Persona declares read_file with posture
        )
        cli_tools = ("read_file", "write_file", "execute_shell")

        intent = ExecutionIntent(
            persona_spec=persona_spec,
            overrides=ExecutionIntentOverrides(
                requested_cli_tools=cli_tools,
            ),
        )

        # Tools from CLI should be in overrides
        assert intent.overrides.requested_cli_tools == cli_tools

    def test_cli_lifecycle_overrides_job_lifecycle(self) -> None:
        """CLI --lifecycle must override Job lifecycle."""
        job_spec = JobSpec(
            persona_ref="worker@v1",
            instance_id="inst_test",
            lifecycle=LifecycleType.ephemeral,  # Job says ephemeral
        )
        cli_lifecycle = LifecycleType.daemon  # CLI says daemon

        intent = ExecutionIntent(
            job_spec=job_spec,
            overrides=ExecutionIntentOverrides(
                requested_lifecycle=cli_lifecycle,
            ),
        )

        # CLI override should take precedence
        assert intent.overrides.requested_lifecycle == LifecycleType.daemon
        assert intent.job_spec is not None
        assert intent.job_spec.lifecycle == LifecycleType.ephemeral

    def test_cli_completion_overrides_job_completion(self) -> None:
        """CLI --completion must override Job completion policy."""
        job_spec = JobSpec(
            persona_ref="worker@v1",
            instance_id="inst_test",
            completion_policy=CompletionPolicy.auto,  # Job says auto
        )
        cli_completion = CompletionPolicy.explicit  # CLI says explicit

        intent = ExecutionIntent(
            job_spec=job_spec,
            overrides=ExecutionIntentOverrides(
                requested_completion_policy=cli_completion,
            ),
        )

        # CLI should win
        assert intent.overrides.requested_completion_policy == CompletionPolicy.explicit
        assert intent.job_spec is not None
        assert intent.job_spec.completion_policy == CompletionPolicy.auto

    def test_defaults_fill_unspecified_fields(self) -> None:
        """Config defaults must fill fields not specified in CLI/Job/Persona."""
        intent = ExecutionIntent(
            defaults=ExecutionIntentDefaults(
                default_timeout="60m",
                default_project_root="/workspace/default",
                default_side_effect_policy=SideEffectPolicy.read_only,
            ),
        )

        # Defaults should be available
        assert intent.defaults.default_timeout == "60m"
        assert intent.defaults.default_project_root == "/workspace/default"
        assert intent.defaults.default_side_effect_policy == SideEffectPolicy.read_only

    def test_job_overrides_defaults(self) -> None:
        """Job values must override config defaults."""
        job_spec = JobSpec(
            persona_ref="worker@v1",
            instance_id="inst_test",
            max_turns=10,  # Job specifies max_turns
        )
        defaults = ExecutionIntentDefaults(
            default_timeout="60m",
        )

        intent = ExecutionIntent(
            job_spec=job_spec,
            defaults=defaults,
        )

        # Job value should be present
        assert intent.job_spec is not None
        assert intent.job_spec.max_turns == 10
        # Default still available as fallback
        assert intent.defaults.default_timeout == "60m"


# =============================================================================
# SECTION 2: Deterministic Snapshot Tests
# =============================================================================


class TestDeterministicContractSnapshots:
    """Tests for determinism: same inputs must produce same contract.

    Spec source: resolution-tests
    """

    def test_identical_inputs_produce_identical_hash(self) -> None:
        """Same resolved contract must have identical snapshot hash."""
        contract = SPAWNABLE_SNAPSHOT_CONTRACT

        hash1 = compute_contract_snapshot_hash(contract)
        hash2 = compute_contract_snapshot_hash(contract)

        assert hash1 == hash2
        assert len(hash1) == 64  # SHA256 produces 64 hex chars

    def test_different_inputs_produce_different_hash(self) -> None:
        """Different resolved contracts must have different snapshot hashes."""
        contract1 = SPAWNABLE_SNAPSHOT_CONTRACT
        contract2 = NON_SPAWNABLE_INLINE_CONTRACT

        hash1 = compute_contract_snapshot_hash(contract1)
        hash2 = compute_contract_snapshot_hash(contract2)

        assert hash1 != hash2

    def test_contract_snapshot_is_json_serializable(self) -> None:
        """Resolved contract must be JSON serializable for persistence."""
        contract = SPAWNABLE_SNAPSHOT_CONTRACT

        json_str = contract_to_deterministic_json(contract)

        # Must be valid JSON
        data = json.loads(json_str)

        # Must contain expected sections
        assert "identity" in data
        assert "runtime" in data
        assert "tool_access" in data
        assert "persona_provenance" in data
        assert "lifecycle_policy" in data
        assert "output_policy" in data
        assert "model_context" in data

    def test_contract_version_is_stable(self) -> None:
        """Contract version must be stable across instances."""
        contract = SPAWNABLE_SNAPSHOT_CONTRACT

        assert contract.identity.contract_version == CONTRACT_SCHEMA_VERSION

    def test_source_hash_computation_is_deterministic(self) -> None:
        """Source hash must be deterministic for identical inputs."""
        hash1 = compute_source_hash("test-input")
        hash2 = compute_source_hash("test-input")

        assert hash1 == hash2
        assert hash1.startswith("sha256:")

    def test_different_sources_produce_different_hash(self) -> None:
        """Different source inputs must produce different hashes."""
        hash1 = compute_source_hash("input-a")
        hash2 = compute_source_hash("input-b")

        assert hash1 != hash2


# =============================================================================
# SECTION 3: Legacy Compatibility Tests
# =============================================================================


class TestLegacyCompatibilityFixtures:
    """Tests for backward compatibility with legacy agents.

    Spec source: resolution-tests
    """

    def test_legacy_agent_without_contract_snapshot(self) -> None:
        """Legacy agent without contract snapshot must be handled gracefully."""
        legacy_agent = make_legacy_agent_without_contract_snapshot(
            instance_id="inst_legacy_001",
            persona_name="legacy-worker",
            persona_ref=None,  # No persona ref in legacy
        )

        assert legacy_agent.instance_id == "inst_legacy_001"
        assert legacy_agent.persona_name == "legacy-worker"
        assert legacy_agent.persona_ref is None
        assert legacy_agent.state == "running"

    def test_legacy_agent_with_persona_ref_but_no_contract(self) -> None:
        """Legacy agent with persona_ref but no contract snapshot still needs resolution."""
        legacy_agent = make_legacy_agent_without_contract_snapshot(
            instance_id="inst_legacy_002",
            persona_name="legacy-worker-v2",
            persona_ref="worker@sha256:legacy",  # Has ref but no contract
        )

        assert legacy_agent.persona_ref == "worker@sha256:legacy"
        # Consumer must not infer spawnability from persona_ref presence alone
        # (This is a test fixture - consumer logic is separate)

    def test_legacy_lifecycle_uses_job_default(self) -> None:
        """Legacy agent lifecycle defaults to Job spec if no CLI override."""
        legacy_agent = make_legacy_agent_without_contract_snapshot(
            instance_id="inst_legacy_003",
            persona_name="legacy-daemon",
            persona_ref="daemon@v1",
            lifecycle=LifecycleType.daemon,
        )

        assert legacy_agent.lifecycle == LifecycleType.daemon


class TestMissingSnapshotBackwardCompatibility:
    """Tests for agents that lack persisted contract snapshots.

    Spec source: resolution-tests, consumer-conformance-tests
    """

    def test_consumer_handles_missing_contract_gracefully(self) -> None:
        """Consumers must handle legacy agents without persisted contracts."""
        # Legacy agent state doesn't have contract_version field
        legacy_state = {
            "instance_id": "inst_legacy_no_contract",
            "persona_name": "orphan-agent",
            "state": "running",
            # No contract_version, no resolved_contract field
        }

        # Consumer must detect missing contract and fall back
        # This is a test fixture - actual consumer logic is separate
        assert "contract_version" not in legacy_state


# =============================================================================
# SECTION 4: Persona Provenance Tests
# =============================================================================


class TestPersonaProvenanceVariants:
    """Tests for persona provenance variants: file, inline, synthesized, snapshot.

    Spec source: execution-contract-specific-design-rules/persona-provenance-and-subagent-inheritance
    """

    def test_file_backed_persona_is_spawnable(self) -> None:
        """File-backed personas must be naturally spawnable via persona_ref."""
        provenance = make_file_backed_persona_provenance(
            persona_ref="worker@sha256:abc123",
        )

        assert provenance.source == "file"
        assert provenance.reusable_persona_ref == "worker@sha256:abc123"
        assert provenance.spawnability.can_spawn is True
        assert provenance.child_inheritance_policy == "inherit_reusable_persona_ref"

    def test_inline_persona_with_auto_materialize_is_spawnable(self) -> None:
        """Inline persona with auto_materialize policy must be spawnable."""
        provenance = make_inline_persona_provenance(
            materialization_policy="auto_materialize_snapshot",
        )

        assert provenance.source == "inline"
        assert provenance.materialization_status == "materialized"
        assert provenance.spawnability.can_spawn is True
        assert provenance.child_inheritance_policy == "inherit_materialized_snapshot"

    def test_inline_persona_with_reject_spawn_is_not_spawnable(self) -> None:
        """Inline persona with reject_spawn policy must not be spawnable."""
        provenance = make_inline_persona_provenance(
            materialization_policy="reject_spawn",
        )

        assert provenance.source == "inline"
        assert provenance.spawnability.can_spawn is False
        assert provenance.spawnability.reason is not None
        assert (
            provenance.spawnability.reason.code
            == "inline_persona_requires_materialization"
        )
        assert provenance.spawnability.reason.retryable is True
        assert provenance.child_inheritance_policy == "reject_child_inheritance"

    def test_synthesized_persona_with_auto_materialize_is_spawnable(self) -> None:
        """Synthesized persona with auto_materialize policy must be spawnable."""
        provenance = make_synthesized_persona_provenance(
            materialization_policy="auto_materialize_snapshot",
        )

        assert provenance.source == "synthesized"
        assert provenance.spawnability.can_spawn is True
        assert provenance.child_inheritance_policy == "inherit_materialized_snapshot"

    def test_synthesized_persona_with_reject_spawn_is_not_spawnable(self) -> None:
        """Synthesized persona with reject_spawn must have structured reason."""
        provenance = make_synthesized_persona_provenance(
            materialization_policy="reject_spawn",
        )

        assert provenance.spawnability.can_spawn is False
        assert provenance.spawnability.reason is not None
        assert (
            provenance.spawnability.reason.code
            == "synthesized_persona_requires_materialization"
        )
        assert "user_action" in provenance.spawnability.reason.model_fields_set

    def test_materialized_snapshot_persona_is_spawnable(self) -> None:
        """Materialized snapshot persona must be spawnable with ref."""
        provenance = make_materialized_snapshot_persona_provenance(
            snapshot_ref="snapshot@sha256:def456",
        )

        assert provenance.source == "snapshot"
        assert provenance.reusable_persona_ref == "snapshot@sha256:def456"
        assert provenance.materialization_status == "materialized"
        assert provenance.spawnability.can_spawn is True

    def test_spawnability_reason_is_structured(self) -> None:
        """Non-spawnable persona must have structured reason for user message."""
        provenance = make_inline_persona_provenance(
            materialization_policy="reject_spawn"
        )

        reason = provenance.spawnability.reason
        assert reason is not None

        # Structured reason must have all fields for user-facing message
        assert reason.code == "inline_persona_requires_materialization"
        assert len(reason.message) > 0
        assert reason.user_action is not None
        # User action should mention persona or snapshot or child
        user_action_lower = reason.user_action.lower()
        assert any(
            keyword in user_action_lower for keyword in ["persona", "snapshot", "child"]
        ), f"user_action should mention resolution: {reason.user_action}"


class TestSpawnabilityPolicyOutcomes:
    """Tests proving inline/synthesized spawnability outcomes.

    Spec source: execution-contract-specific-design-rules/persona-provenance-and-subagent-inheritance
    """

    def test_materialized_child_can_spawn(self) -> None:
        """Materialized persona must enable successful child spawn."""
        provenance = make_materialized_snapshot_persona_provenance(
            snapshot_ref="child@v1",
        )

        # Materialized status enables spawnability
        assert provenance.materialization_status == "materialized"
        assert provenance.spawnability.can_spawn is True

    def test_non_spawnable_returns_structured_failure(self) -> None:
        """Non-spawnable persona must return structured failure reason."""
        provenance = make_inline_persona_provenance(
            materialization_policy="reject_spawn",
        )

        # Must have structured reason, not just boolean
        assert provenance.spawnability.can_spawn is False
        assert provenance.spawnability.reason is not None
        assert provenance.spawnability.reason.code != "spawnable"


# =============================================================================
# SECTION 5: Consumer Conformance Tests
# =============================================================================


class TestConsumerConformanceFixture:
    """Tests proving multiple consumers read same contract fixture.

    Spec source: consumer-conformance-tests
    """

    def test_consumer_conformance_fixture_creation(self) -> None:
        """Consumer conformance fixture must be reusable across consumers."""
        case = make_consumer_conformance_fixture(
            instance_id="inst_consumer_test",
            effective_tools=("read_file", "write_file"),
            spawnability=True,
        )

        assert case.contract.identity.instance_id == "inst_consumer_test"
        assert case.expected_effective_tools == ("read_file", "write_file")
        assert case.expected_spawnability is True

    def test_warnings_consumer_reads_effective_tools(self) -> None:
        """Warning generator must read effective tools from contract."""
        case = make_consumer_conformance_fixture(
            effective_tools=("read_file",),
        )

        # Warning consumer reads tool_access.effective_tools
        effective_tools = case.contract.tool_access.effective_tools
        assert effective_tools == case.expected_effective_tools

    def test_ui_consumer_reads_lifecycle(self) -> None:
        """UI panel must read lifecycle from contract."""
        case = make_consumer_conformance_fixture(
            lifecycle=LifecycleType.daemon,
            completion=CompletionPolicy.explicit,
        )

        # UI consumer reads lifecycle_policy
        assert case.contract.lifecycle_policy.lifecycle == case.expected_lifecycle
        assert (
            case.contract.lifecycle_policy.completion_policy == case.expected_completion
        )

    def test_runtime_policy_consumer_reads_spawnability(self) -> None:
        """Runtime policy must read spawnability from contract provenance."""
        case = make_consumer_conformance_fixture(
            spawnability=False,
        )

        # Runtime policy reads persona_provenance.spawnability
        spawnability = case.contract.persona_provenance.spawnability
        assert spawnability.can_spawn == case.expected_spawnability

    def test_control_consumer_reads_model_context(self) -> None:
        """Control consumer must read model context from contract."""
        case = make_consumer_conformance_fixture()

        # Control consumer reads model_context
        model_context = case.contract.model_context
        assert "instance_id" in model_context.values


class TestConsumerConformanceConsistency:
    """Tests proving all consumers derive same decisions from same contract.

    Spec source: consumer-conformance-tests
    """

    def test_all_consumers_see_same_effective_model(self) -> None:
        """All consumers must see the same effective model after resolution."""
        case = make_consumer_conformance_fixture(
            effective_model="gpt-4o-turbo",
        )

        # All consumers read runtime.effective_model
        assert case.contract.runtime.effective_model == "gpt-4o-turbo"
        assert case.expected_model == "gpt-4o-turbo"

    def test_all_consumers_see_same_lifecycle_policy(self) -> None:
        """All consumers must see same lifecycle + completion after resolution."""
        case = make_consumer_conformance_fixture(
            lifecycle=LifecycleType.daemon,
            completion=CompletionPolicy.explicit,
        )

        # Lifecycle is resolved once, consumed by all
        assert case.contract.lifecycle_policy.lifecycle == LifecycleType.daemon
        assert (
            case.contract.lifecycle_policy.completion_policy
            == CompletionPolicy.explicit
        )
        assert case.expected_lifecycle == LifecycleType.daemon
        assert case.expected_completion == CompletionPolicy.explicit


# =============================================================================
# SECTION 6: Contract Immutability Tests
# =============================================================================


class TestContractImmutability:
    """Tests proving resolved contract is immutable.

    Spec source: execution-contract-boundary
    """

    def test_runtime_is_frozen(self) -> None:
        """ResolvedRuntime must be frozen after creation."""
        runtime = ResolvedRuntime(effective_model="gpt-4o")

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

    def test_tool_access_is_frozen(self) -> None:
        """RuntimeToolAccess must be frozen."""
        tool_access = RuntimeToolAccess(
            effective_side_effect_policy=SideEffectPolicy.read_only,
        )

        with pytest.raises(Exception):
            tool_access.effective_tools = ("new_tool",)  # type: ignore[misc]

    def test_lifecycle_policy_is_frozen(self) -> None:
        """ResolvedLifecyclePolicy must be frozen."""
        policy = ResolvedLifecyclePolicy(
            lifecycle=LifecycleType.ephemeral,
            completion_policy=CompletionPolicy.auto,
        )

        with pytest.raises(Exception):
            policy.lifecycle = LifecycleType.daemon  # type: ignore[misc]

    def test_persona_provenance_is_frozen(self) -> None:
        """PersonaProvenance must be frozen."""
        provenance = make_file_backed_persona_provenance("worker@v1")

        with pytest.raises(Exception):
            provenance.source = "other"  # type: ignore[misc]


# =============================================================================
# SECTION 7: Session State Separation Tests
# =============================================================================


class TestSessionStateSeparation:
    """Tests proving session state is ephemeral and separate from contract.

    Spec source: execution-contract-boundary, session-runtime-tests
    """

    def test_session_state_is_mutable(self) -> None:
        """ExecutionSessionState must be mutable (ephemeral)."""
        session = ExecutionSessionState(
            instance_id="inst_session_test",
            transport=SessionTransportState(
                attach_mode="streaming",
                stream_active=True,
            ),
        )

        # Session state should be mutable (not frozen)
        session.transport.stream_active = False
        assert session.transport.stream_active is False

    def test_session_state_does_not_redefine_contract(self) -> None:
        """Session state must not override contract values."""
        session = ExecutionSessionState(
            instance_id="inst_session_test",
            contract_version=CONTRACT_SCHEMA_VERSION,
        )

        # Session only holds ephemeral state
        # It cannot redefine execution truth (that lives in contract)
        assert session.contract_version == CONTRACT_SCHEMA_VERSION
        assert hasattr(session, "transport")
        assert not hasattr(session, "runtime")  # No runtime in session
        assert not hasattr(session, "tool_access")  # No tool_access in session

    def test_terminal_state_survives_transport_disconnect(self) -> None:
        """Terminal state must be preserved even when transport disconnects."""
        # This tests the invariant: terminal state > transport failure
        # When agent is completed/failed/killed, that state persists
        # even if socket disappears

        session = ExecutionSessionState(
            instance_id="inst_terminal_test",
            transport=SessionTransportState(
                transport_connected=False,  # Socket gone
                terminal_snapshot_ref="terminal-snapshot-1",  # But state survives
            ),
        )

        # Transport disconnected but terminal snapshot survives
        assert session.transport.transport_connected is False
        assert session.transport.terminal_snapshot_ref == "terminal-snapshot-1"


# =============================================================================
# SECTION 8: Execution Intent Tests
# =============================================================================


class TestExecutionIntentStructure:
    """Tests proving intent keeps requested inputs separate from effective truth.

    Spec source: execution-contract-boundary
    """

    def test_intent_has_overrides_separate_from_effective(self) -> None:
        """ExecutionIntent must keep requested inputs separate from resolved truth."""
        # This is already tested in execution_contract_fixtures
        assert EXECUTION_INTENT.overrides.requested_cli_tools == (
            "read_file",
            "write_file",
        )
        assert (
            EXECUTION_INTENT.overrides.requested_completion_policy
            == CompletionPolicy.explicit
        )

        # Overrides are separate from defaults
        assert EXECUTION_INTENT.defaults.default_timeout == "30m"

    def test_intent_can_be_incomplete(self) -> None:
        """ExecutionIntent may be incomplete pending resolution."""
        incomplete_intent = ExecutionIntent(
            persona_spec=None,
            job_spec=None,
            overrides=ExecutionIntentOverrides(),
            defaults=ExecutionIntentDefaults(),
        )

        # Incomplete intent is valid
        assert incomplete_intent.persona_spec is None
        assert incomplete_intent.job_spec is None


# =============================================================================
# SECTION 9: Fixture Matrix Summary
# =============================================================================


class TestFixtureMatrixComplete:
    """Summary test proving all fixture variants exist.

    This test documents the fixture matrix and ensures all required variants
    are available for later implementation phases.
    """

    def test_all_persona_sources_have_fixtures(self) -> None:
        """All persona sources (file, inline, synthesized, snapshot) have fixtures."""
        # File-backed
        file_prov = make_file_backed_persona_provenance(persona_ref="worker@v1")
        assert file_prov.source == "file"

        # Inline (both materialization policies)
        inline_spawnable = make_inline_persona_provenance(
            materialization_policy="auto_materialize_snapshot"
        )
        assert inline_spawnable.source == "inline"
        assert inline_spawnable.spawnability.can_spawn is True

        inline_blocked = make_inline_persona_provenance(
            materialization_policy="reject_spawn"
        )
        assert inline_blocked.spawnability.can_spawn is False

        # Synthesized (both materialization policies)
        synth_spawnable = make_synthesized_persona_provenance(
            materialization_policy="auto_materialize_snapshot"
        )
        assert synth_spawnable.source == "synthesized"

        synth_blocked = make_synthesized_persona_provenance(
            materialization_policy="reject_spawn"
        )
        assert synth_blocked.spawnability.can_spawn is False

        # Materialized snapshot
        snapshot_prov = make_materialized_snapshot_persona_provenance(
            snapshot_ref="snap@v1"
        )
        assert snapshot_prov.source == "snapshot"

    def test_all_spawnability_outcomes_have_fixtures(self) -> None:
        """Both spawnable and non-spawnable outcomes have fixtures."""
        # Spawnable
        spawnable_prov = make_file_backed_persona_provenance(persona_ref="worker@v1")
        assert spawnable_prov.spawnability.can_spawn is True

        # Non-spawnable with structure reason
        non_spawnable_prov = make_inline_persona_provenance(
            materialization_policy="reject_spawn"
        )
        assert non_spawnable_prov.spawnability.can_spawn is False
        assert non_spawnable_prov.spawnability.reason is not None

    def test_all_compatibility_scenarios_have_fixtures(self) -> None:
        """Legacy and current compatibility scenarios have fixtures."""
        # Legacy without contract snapshot
        legacy = make_legacy_agent_without_contract_snapshot("legacy_1", "old-worker")
        assert legacy.persona_ref is None  # May be missing

        # Legacy with persona_ref but no contract
        legacy_with_ref = make_legacy_agent_without_contract_snapshot(
            "legacy_2",
            "old-worker-v2",
            persona_ref="old@v1",
        )
        assert legacy_with_ref.persona_ref == "old@v1"

        # Current with full contract
        current = SPAWNABLE_SNAPSHOT_CONTRACT
        assert current.identity.contract_version == CONTRACT_SCHEMA_VERSION

    def test_all_conformance_consumers_have_fixtures(self) -> None:
        """Consumer conformance fixture supports all consumer types."""
        case = make_consumer_conformance_fixture(
            effective_tools=("read_file", "write_file"),
            spawnability=True,
            lifecycle=LifecycleType.daemon,
            completion=CompletionPolicy.explicit,
        )

        # Warnings consumer
        assert case.expected_effective_tools == ("read_file", "write_file")

        # UI consumer
        assert case.expected_lifecycle == LifecycleType.daemon
        assert case.expected_completion == CompletionPolicy.explicit

        # Runtime policy consumer
        assert case.expected_spawnability is True

        # Control consumer
        assert case.contract.identity.instance_id is not None


@pytest.fixture
def tmp_state_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
    """Create isolated ANIMA_STATE for contract snapshot tests."""
    state_dir = tmp_path / "anima_state"
    state_dir.mkdir()
    monkeypatch.setenv("ANIMA_STATE", str(state_dir))
    return state_dir


class TestResolverAndSnapshotRuntimeTruth:
    """Focused regressions for resolver truth and snapshot persistence."""

    def test_relative_runtime_paths_preserve_intent_values(self) -> None:
        """Resolver preserves runtime path intent values without shell-side rewriting."""
        intent = ExecutionIntent(
            persona_spec=PersonaSpec(
                id="worker",
                description="Worker persona",
                prompt="Do work",
                model="gpt-4o-mini",
            ),
            job_spec=JobSpec(persona_ref="worker@v1", instance_id="inst_relpath"),
            overrides=ExecutionIntentOverrides(
                requested_work_dir=".",
                requested_project_root="./project",
            ),
        )

        contract = resolve_execution_contract(intent)

        assert contract.runtime.work_dir == "."
        assert contract.runtime.project_root == "./project"

    def test_snapshot_location_is_absolute_under_agent_state_dir(
        self, tmp_state_dir: Path
    ) -> None:
        """Persisted contract snapshot path is absolute and agent-scoped."""
        _ = tmp_state_dir
        snapshot_path = get_contract_snapshot_path("inst_location")
        assert snapshot_path.is_absolute()
        assert snapshot_path.name == "contract.snapshot.json"
        assert "agents" in snapshot_path.parts

    def test_explicit_null_override_clears_default_runtime_field(self) -> None:
        """Explicit null override clears default project_root value."""
        intent = ExecutionIntent(
            persona_spec=PersonaSpec(
                id="worker",
                description="Worker persona",
                prompt="Do work",
                model="gpt-4o-mini",
            ),
            job_spec=JobSpec(persona_ref="worker@v1", instance_id="inst_clear"),
            overrides=ExecutionIntentOverrides(requested_project_root=None),
            defaults=ExecutionIntentDefaults(default_project_root="/workspace/default"),
        )

        contract = resolve_execution_contract(intent)

        assert contract.runtime.project_root is None

    def test_missing_snapshot_falls_back_to_legacy_contract(
        self, tmp_state_dir: Path
    ) -> None:
        """Legacy state without snapshot still loads a synthesized contract."""
        _ = tmp_state_dir
        state = AgentStateData(
            instance_id="inst_legacy_fallback",
            trace_id="tr_legacy_fallback",
            persona_name="legacy-worker",
            persona_ref="legacy@v1",
            model="gpt-4o-mini",
            lifecycle=LifecycleType.daemon,
        )
        write_state(state.instance_id, state)

        loaded = load_persisted_or_legacy_contract(state)

        assert loaded.identity.instance_id == state.instance_id
        assert loaded.runtime.effective_model == "gpt-4o-mini"
        assert loaded.lifecycle_policy.lifecycle == LifecycleType.daemon

    def test_snapshot_truth_overrides_stale_legacy_state_fields(
        self, tmp_state_dir: Path
    ) -> None:
        """Persisted snapshot wins over stale mutable legacy state values."""
        _ = tmp_state_dir
        job = JobSpec(persona_ref="worker@v1", instance_id="inst_snapshot_precedence")
        contract = resolve_execution_contract(
            ExecutionIntent(
                persona_spec=PersonaSpec(
                    id="worker",
                    description="Worker persona",
                    prompt="Do work",
                    model="gpt-4o",
                ),
                job_spec=job,
                overrides=ExecutionIntentOverrides(requested_model="gpt-5.2"),
            )
        )
        created = create_agent_state(
            instance_id=job.instance_id,
            persona_name="worker",
            job=job,
            model="gpt-4o",
            resolved_contract=contract,
        )
        assert isinstance(created, Success)

        stale_state = created.unwrap().model_copy(update={"model": "stale-model"})
        write_state(job.instance_id, stale_state)

        loaded = load_persisted_or_legacy_contract(stale_state)

        assert loaded.runtime.effective_model == "gpt-5.2"

    def test_snapshot_write_reports_cas_conflict_for_immutable_truth(
        self, tmp_state_dir: Path
    ) -> None:
        """Snapshot persistence rejects stale expected hash rewrites."""
        _ = tmp_state_dir
        job = JobSpec(persona_ref="worker@v1", instance_id="inst_cas_conflict")
        created = create_agent_state(
            instance_id=job.instance_id,
            persona_name="worker",
            job=job,
            model="gpt-4o-mini",
        )
        assert isinstance(created, Success)

        base_contract = resolve_execution_contract(
            ExecutionIntent(
                persona_spec=PersonaSpec(
                    id="worker",
                    description="Worker persona",
                    prompt="Do work",
                    model="gpt-4o-mini",
                ),
                job_spec=job,
            )
        )
        first_write = write_contract_snapshot(job.instance_id, base_contract)
        assert isinstance(first_write, Success)
        first_hash = first_write.unwrap()

        competing_contract = resolve_execution_contract(
            ExecutionIntent(
                persona_spec=PersonaSpec(
                    id="worker",
                    description="Worker persona",
                    prompt="Do work",
                    model="gpt-4o",
                ),
                job_spec=job,
                overrides=ExecutionIntentOverrides(requested_model="gpt-4o"),
            )
        )

        conflict = write_contract_snapshot(
            job.instance_id,
            competing_contract,
            expected_hash="sha256:stale-hash",
        )
        assert isinstance(conflict, Failure)

        read_back = read_contract_snapshot(job.instance_id)
        assert isinstance(read_back, Success)
        assert read_back.unwrap().runtime.effective_model == "gpt-4o-mini"
        assert len(first_hash) == 64

    def test_inline_source_remains_non_spawnable_even_with_persona_ref(self) -> None:
        """Inline parent contract never infers spawnability from raw persona_ref."""
        contract = resolve_execution_contract(
            ExecutionIntent(
                persona_spec=PersonaSpec(
                    id="inline-parent",
                    description="Inline parent",
                    prompt="Inline prompt",
                    model="gpt-4o-mini",
                ),
                job_spec=JobSpec(
                    persona_ref="inline-parent@sha256:raw-ref",
                    instance_id="inst_inline_parent",
                ),
            ),
            persona_source="inline",
            inline_behavior="reject_spawn",
        )

        assert contract.persona_provenance.source == "inline"
        assert contract.persona_provenance.reusable_persona_ref is None
        assert contract.persona_provenance.spawnability.can_spawn is False
        assert contract.persona_provenance.spawnability.reason is not None
        assert (
            contract.persona_provenance.spawnability.reason.code
            == "inline_persona_requires_materialization"
        )


class TestModelVisibleRuntimeContext:
    """Tests proving model-visible runtime context from contract.

    Spec source: model-visible-runtime-context
    """

    def test_runtime_context_includes_work_dir(self) -> None:
        """work_dir must appear in model_context when set."""
        intent = ExecutionIntent(
            persona_spec=PersonaSpec(
                id="worker",
                description="Worker",
                prompt="Work hard",
                model="gpt-4o",
            ),
            job_spec=JobSpec(
                persona_ref="worker@v1",
                instance_id="inst_work_dir",
            ),
            overrides=ExecutionIntentOverrides(
                requested_work_dir="/workspace/task",
            ),
        )

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

    def test_runtime_context_includes_project_root(self) -> None:
        """project_root must appear in model_context when set."""
        intent = ExecutionIntent(
            persona_spec=PersonaSpec(
                id="worker",
                description="Worker",
                prompt="Work hard",
                model="gpt-4o",
            ),
            job_spec=JobSpec(
                persona_ref="worker@v1",
                instance_id="inst_project_root",
            ),
            overrides=ExecutionIntentOverrides(
                requested_project_root="/project",
            ),
        )

        contract = resolve_execution_contract(intent)
        assert contract.model_context.values.get("project_root") == "/project"

    def test_runtime_context_includes_filesystem_scope(self) -> None:
        """filesystem_scope must appear in model_context when set."""
        intent = ExecutionIntent(
            persona_spec=PersonaSpec(
                id="worker",
                description="Worker",
                prompt="Work hard",
                model="gpt-4o",
            ),
            job_spec=JobSpec(
                persona_ref="worker@v1",
                instance_id="inst_fs_scope",
            ),
            overrides=ExecutionIntentOverrides(
                requested_filesystem_scope="project-root",
            ),
        )

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

    def test_runtime_context_includes_effective_tool_count(self) -> None:
        """effective_tool_count must always appear in model_context."""
        intent = ExecutionIntent(
            persona_spec=PersonaSpec(
                id="worker",
                description="Worker",
                prompt="Work hard",
                model="gpt-4o",
            ),
            job_spec=JobSpec(
                persona_ref="worker@v1",
                instance_id="inst_tool_count",
            ),
            overrides=ExecutionIntentOverrides(
                requested_cli_tools=("read_file", "write_file", "execute_shell"),
            ),
        )

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

    def test_runtime_context_tool_count_empty(self) -> None:
        """effective_tool_count must be 0 when no tools."""
        intent = ExecutionIntent(
            persona_spec=PersonaSpec(
                id="worker",
                description="Worker",
                prompt="Work hard",
                model="gpt-4o",
            ),
            job_spec=JobSpec(
                persona_ref="worker@v1",
                instance_id="inst_empty_tools",
            ),
        )

        contract = resolve_execution_contract(intent)
        assert contract.model_context.values.get("effective_tool_count") == 0

    def test_runtime_context_appears_only_with_effective_access(self) -> None:
        """Runtime context must match effective filesystem/shell access only.

        This is the key invariant: runtime context hints appear only when
        effective filesystem or shell access exists.
        """
        # With work_dir set -> filesystem access -> work_dir in context
        intent_with_access = ExecutionIntent(
            persona_spec=PersonaSpec(
                id="worker",
                description="Worker",
                prompt="Work hard",
                model="gpt-4o",
            ),
            job_spec=JobSpec(
                persona_ref="worker@v1",
                instance_id="inst_with_access",
            ),
            overrides=ExecutionIntentOverrides(
                requested_work_dir="/workspace",
            ),
        )
        contract_with_access = resolve_execution_contract(intent_with_access)
        assert contract_with_access.model_context.values.get("work_dir") is not None

        # Without work_dir -> no filesystem access hint -> work_dir is None
        intent_no_access = ExecutionIntent(
            persona_spec=PersonaSpec(
                id="worker",
                description="Worker",
                prompt="Work hard",
                model="gpt-4o",
            ),
            job_spec=JobSpec(
                persona_ref="worker@v1",
                instance_id="inst_no_access",
            ),
        )
        contract_no_access = resolve_execution_contract(intent_no_access)
        assert contract_no_access.model_context.values.get("work_dir") is None
