"""Resolution precedence, determinism, legacy compatibility, and persona-provenance fixtures.

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
"""

from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass, field
from typing import Any

from anima.core.contracts import (
    CONTRACT_SCHEMA_VERSION,
    ChildInheritancePolicy,
    ContractIdentity,
    ContractResolverProvenance,
    ContractSourceHashes,
    InlinePersonaBehavior,
    MaterializedPersonaStatus,
    PersonaProvenance,
    PersonaSource,
    PersonaSpawnability,
    RuntimeModelContext,
    RuntimeOutputPolicy,
    RuntimeToolAccess,
    SessionTransportState,
    SpawnabilityReason,
)
from anima.core.execution import (
    ExecutionIntent,
    ExecutionIntentDefaults,
    ExecutionIntentOverrides,
    ExecutionSessionState,
    ResolvedExecutionContract,
    ResolvedLifecyclePolicy,
    ResolvedRuntime,
)
from anima.core.schemas.enums import CompletionPolicy, LifecycleType, SideEffectPolicy
from anima.core.schemas.models import JobSpec, PersonaSpec


# =============================================================================
# Resolution Precedence Matrix Construction Helpers
# =============================================================================


@dataclass(frozen=True)
class ResolutionPrecedenceInputs:
    """All inputs that participate in precedence resolution.

    Used to construct test cases that prove correct precedence order:
    CLI > Persona > Job > Defaults > Runtime Probes
    """

    persona_spec: PersonaSpec | None = None
    job_spec: JobSpec | None = None
    cli_overrides: ExecutionIntentOverrides = field(
        default_factory=ExecutionIntentOverrides
    )
    config_defaults: ExecutionIntentDefaults = field(
        default_factory=ExecutionIntentDefaults
    )
    runtime_probe_values: dict[str, Any] = field(default_factory=dict)


@dataclass(frozen=True)
class EffectiveContractExpectation:
    """Expected effective values after resolution for a test case."""

    effective_model: str | None = None
    effective_tools: tuple[str, ...] = ()
    effective_lifecycle: LifecycleType | None = None
    effective_completion_policy: CompletionPolicy | None = None
    effective_work_dir: str | None = None
    effective_project_root: str | None = None
    spawnability_can_spawn: bool = True
    spawnability_reason_code: str | None = None


def compute_source_hash(source: str) -> str:
    """Compute a deterministic SHA256 hash for contract source provenance.

    >>> compute_source_hash("test-content")
    '0db9d4c9bf78f1e9...'

    The hash is always prefixed with 'sha256:' for clarity.
    """
    digest = hashlib.sha256(source.encode("utf-8")).hexdigest()[:16]
    return f"sha256:{digest}"


def make_contract_identity(
    instance_id: str,
    resolver_version: str = "resolver/0.1.0",
    persona_digest: str | None = None,
    job_digest: str | None = None,
    cli_digest: str | None = None,
) -> ContractIdentity:
    """Construct a valid ContractIdentity for test contracts.

    Args:
        instance_id: Agent instance identifier.
        resolver_version: Resolver version string.
        persona_digest: Optional persona source hash.
        job_digest: Optional job source hash.
        cli_digest: Optional CLI args hash.

    Returns:
        ContractIdentity with valid provenance.
    """
    return ContractIdentity(
        instance_id=instance_id,
        contract_version=CONTRACT_SCHEMA_VERSION,
        resolver=ContractResolverProvenance(
            resolver_version=resolver_version,
            source_hashes=ContractSourceHashes(
                persona_digest=persona_digest,
                job_digest=job_digest,
                cli_digest=cli_digest,
            ),
        ),
    )


# =============================================================================
# Persona Provenance Fixtures
# =============================================================================


def make_file_backed_persona_provenance(
    persona_ref: str,
    *,
    materialized: bool = True,
) -> PersonaProvenance:
    """Create persona provenance for file-backed personas.

    File-backed personas have a reusable persona_ref and are naturally spawnable.

    Args:
        persona_ref: The persona reference (e.g., "worker@sha256:abc123").
        materialized: Whether a snapshot has been materialized.

    Returns:
        PersonaProvenance for file-backed personas.
    """
    return PersonaProvenance(
        source="file",
        reusable_persona_ref=persona_ref,
        materialization_status="materialized" if materialized else "not_required",
        inline_behavior="reject_spawn",
        child_inheritance_policy="inherit_reusable_persona_ref",
        spawnability=PersonaSpawnability(can_spawn=True),
    )


def make_inline_persona_provenance(
    *,
    materialization_policy: InlinePersonaBehavior,
) -> PersonaProvenance:
    """Create persona provenance for inline personas.

    Inline personas require explicit materialization policy for spawnability.

    Args:
        materialization_policy: Either "auto_materialize_snapshot" or "reject_spawn".

    Returns:
        PersonaProvenance for inline personas.
    """
    can_spawn = materialization_policy == "auto_materialize_snapshot"

    if can_spawn:
        spawnability = PersonaSpawnability(can_spawn=True)
        child_policy: ChildInheritancePolicy = "inherit_materialized_snapshot"
    else:
        spawnability = PersonaSpawnability(
            can_spawn=False,
            reason=SpawnabilityReason(
                code="inline_persona_requires_materialization",
                message="This inline persona cannot spawn child agents until it is materialized.",
                user_action="Retry with a reusable persona snapshot or explicit child persona.",
                retryable=True,
                details={"source": "inline"},
            ),
        )
        child_policy = "reject_child_inheritance"

    return PersonaProvenance(
        source="inline",
        reusable_persona_ref=None,
        materialization_status="materialized" if can_spawn else "not_materialized",
        inline_behavior=materialization_policy,
        child_inheritance_policy=child_policy,
        spawnability=spawnability,
    )


def make_synthesized_persona_provenance(
    *,
    materialization_policy: InlinePersonaBehavior,
) -> PersonaProvenance:
    """Create persona provenance for synthesized personas.

    Synthesized personas (e.g., from inference) require explicit materialization
    policy for spawnability, similar to inline personas.

    Args:
        materialization_policy: Either "auto_materialize_snapshot" or "reject_spawn".

    Returns:
        PersonaProvenance for synthesized personas.
    """
    can_spawn = materialization_policy == "auto_materialize_snapshot"

    if can_spawn:
        spawnability = PersonaSpawnability(can_spawn=True)
        child_policy: ChildInheritancePolicy = "inherit_materialized_snapshot"
    else:
        spawnability = PersonaSpawnability(
            can_spawn=False,
            reason=SpawnabilityReason(
                code="synthesized_persona_requires_materialization",
                message="This synthesized persona cannot spawn child agents until it is materialized.",
                user_action="Retry with a reusable persona snapshot or explicit child persona.",
                retryable=True,
                details={"source": "synthesized"},
            ),
        )
        child_policy = "reject_child_inheritance"

    return PersonaProvenance(
        source="synthesized",
        reusable_persona_ref=None,
        materialization_status="materialized" if can_spawn else "not_materialized",
        inline_behavior=materialization_policy,
        child_inheritance_policy=child_policy,
        spawnability=spawnability,
    )


def make_materialized_snapshot_persona_provenance(
    snapshot_ref: str,
) -> PersonaProvenance:
    """Create persona provenance for materialized snapshot personas.

    Materialized snapshots have both a reusable ref and explicit materialization status.

    Args:
        snapshot_ref: The materialized snapshot reference.

    Returns:
        PersonaProvenance for materialized snapshot personas.
    """
    return PersonaProvenance(
        source="snapshot",
        reusable_persona_ref=snapshot_ref,
        materialization_status="materialized",
        inline_behavior="auto_materialize_snapshot",
        child_inheritance_policy="inherit_materialized_snapshot",
        spawnability=PersonaSpawnability(can_spawn=True),
    )


# =============================================================================
# Legacy Compatibility Fixtures
# =============================================================================


@dataclass(frozen=True)
class LegacyAgentState:
    """Simulates legacy agent state without persisted contract snapshot.

    Used to test backward compatibility when legacy agents don't have
    ResolvedExecutionContract persisted alongside their state.
    """

    instance_id: str
    persona_name: str
    persona_ref: str | None  # Legacy field, may be None
    state: str
    lifecycle: LifecycleType


def make_legacy_agent_without_contract_snapshot(
    instance_id: str,
    persona_name: str,
    persona_ref: str | None = None,
    lifecycle: LifecycleType = LifecycleType.ephemeral,
) -> LegacyAgentState:
    """Create a legacy agent state that lacks a persisted contract snapshot.

    Tests backward compatibility: consumers must handle missing contract gracefully.

    Args:
        instance_id: Agent instance ID.
        persona_name: Human-readable persona name.
        persona_ref: Optional legacy persona reference (may be absent).
        lifecycle: Agent lifecycle type.

    Returns:
        LegacyAgentState simulating pre-contract-snapshot agents.
    """
    return LegacyAgentState(
        instance_id=instance_id,
        persona_name=persona_name,
        persona_ref=persona_ref,
        state="running",
        lifecycle=lifecycle,
    )


# =============================================================================
# Consumer Conformance Test Fixtures
# =============================================================================


@dataclass(frozen=True)
class ConsumerConformanceCase:
    """A reusable test case proving multiple consumers read the same contract.

    Used by warning generators, UI panels, runtime policy, and control consumers
    to verify they all derive their decisions from the same ResolvedExecutionContract.
    """

    contract: ResolvedExecutionContract
    expected_effective_tools: tuple[str, ...]
    expected_spawnability: bool
    expected_lifecycle: LifecycleType
    expected_completion: CompletionPolicy
    expected_model: str

    @property
    def instance_id(self) -> str:
        """Get instance ID from contract identity."""
        return self.contract.identity.instance_id


def make_consumer_conformance_fixture(
    instance_id: str = "inst_conformance_test",
    effective_model: str = "gpt-4o",
    effective_tools: tuple[str, ...] = ("read_file", "write_file"),
    spawnability: bool = True,
    lifecycle: LifecycleType = LifecycleType.ephemeral,
    completion: CompletionPolicy = CompletionPolicy.explicit,
) -> ConsumerConformanceCase:
    """Create a reusable consumer-conformance test case.

    Multiple consumers (warnings, UI, runtime policy, control) should read this
    fixture and derive consistent decisions.

    Args:
        instance_id: Agent instance ID.
        effective_model: Effective model after resolution.
        effective_tools: Effective tool set after resolution.
        spawnability: Whether child agent spawning is allowed.
        lifecycle: Effective lifecycle type.
        completion: Effective completion policy.

    Returns:
        ConsumerConformanceCase for reuse across consumer tests.
    """
    spawn_reason: SpawnabilityReason | None
    if spawnability:
        spawn_reason = None
    else:
        spawn_reason = SpawnabilityReason(
            code="contract_resolution_incomplete",
            message="Spawnability not determined: contract resolution incomplete.",
            retryable=False,
        )

    contract = ResolvedExecutionContract(
        identity=make_contract_identity(instance_id),
        runtime=ResolvedRuntime(
            effective_model=effective_model,
        ),
        tool_access=RuntimeToolAccess(
            declared_persona_tools=("read_file",),
            requested_cli_tools=effective_tools,
            requested_mcp_tools=(),
            effective_tools=effective_tools,
            effective_presets=("default",),
            effective_side_effect_policy=SideEffectPolicy.read_only,
        ),
        persona_provenance=PersonaProvenance(
            source="file",
            reusable_persona_ref=f"{instance_id}@v1",
            materialization_status="materialized",
            inline_behavior="reject_spawn",
            child_inheritance_policy="inherit_reusable_persona_ref",
            spawnability=PersonaSpawnability(
                can_spawn=spawnability,
                reason=spawn_reason,
            ),
        ),
        lifecycle_policy=ResolvedLifecyclePolicy(
            lifecycle=lifecycle,
            completion_policy=completion,
        ),
        output_policy=RuntimeOutputPolicy(
            provider_profile="test-profile",
            truncation_strategy="paginate_then_truncate",
        ),
        model_context=RuntimeModelContext(
            values={"instance_id": instance_id},
        ),
    )

    return ConsumerConformanceCase(
        contract=contract,
        expected_effective_tools=effective_tools,
        expected_spawnability=spawnability,
        expected_lifecycle=lifecycle,
        expected_completion=completion,
        expected_model=effective_model,
    )


# =============================================================================
# Deterministic Snapshot Hash Verification
# =============================================================================


def contract_to_deterministic_json(contract: ResolvedExecutionContract) -> str:
    """Serialize contract to deterministic JSON for snapshot testing.

    Sorts all keys and ensures consistent ordering for hash verification.

    Args:
        contract: The resolved execution contract.

    Returns:
        Deterministic JSON string representation.
    """
    # Pydantic model_dump with mode='json' for serialization
    data = contract.model_dump(mode="json")
    return json.dumps(data, sort_keys=True, indent=2)


def compute_contract_snapshot_hash(contract: ResolvedExecutionContract) -> str:
    """Compute deterministic SHA256 hash of contract snapshot.

    Used to verify that identical inputs produce identical contracts
    (determinism invariant).

    Args:
        contract: The resolved execution contract.

    Returns:
        SHA256 hash of the serialized contract.
    """
    json_str = contract_to_deterministic_json(contract)
    return hashlib.sha256(json_str.encode("utf-8")).hexdigest()


# =============================================================================
# Runtime Probe Mocks
# =============================================================================


@dataclass(frozen=True)
class MockRuntimeProbeResults:
    """Mock results from runtime probes that influence resolution.

    Runtime probes discover: available tools, model capabilities, filesystem state.
    These values have lower precedence than CLI/Job/Config/Persona.
    """

    discovered_model_capabilities: dict[str, Any] = field(default_factory=dict)
    discovered_available_tools: tuple[str, ...] = ()
    discovered_project_root: str | None = None
    discovered_filesystem_scope: str | None = None


__all__ = [
    # Precedence helpers
    "ResolutionPrecedenceInputs",
    "EffectiveContractExpectation",
    "compute_source_hash",
    "make_contract_identity",
    # Persona provenance fixtures
    "make_file_backed_persona_provenance",
    "make_inline_persona_provenance",
    "make_synthesized_persona_provenance",
    "make_materialized_snapshot_persona_provenance",
    # Legacy compatibility
    "LegacyAgentState",
    "make_legacy_agent_without_contract_snapshot",
    # Consumer conformance
    "ConsumerConformanceCase",
    "make_consumer_conformance_fixture",
    # Deterministic verification
    "contract_to_deterministic_json",
    "compute_contract_snapshot_hash",
    # Runtime probes
    "MockRuntimeProbeResults",
]
