"""Shared types for live agent battery scripts."""
from __future__ import annotations

from dataclasses import dataclass, field

PILLARS = (
    "core",
    "research",
    "workspace",
    "durable",
    "personality",
    "user",
    "knowledge",
    "vault",
    "assistant",
    "recovery",
    "user_trust",
    "chat_core",
    "sidebar",
    "preferences",
)


@dataclass
class Case:
    id: str
    prompt: str
    pillar: str = "core"
    timeout_sec: float = 120.0
    min_tool_calls: int = 1
    required_tools: tuple[str, ...] = ()
    any_of_tools: tuple[str, ...] = ()
    forbid_assistant_patterns: tuple[str, ...] = ()
    forbid_patterns_assistant_only: bool = False
    require_assistant_substrings: tuple[str, ...] = ()
    # At least one of these must appear in the final assistant reply (OR semantics).
    any_of_assistant_substrings: tuple[str, ...] = ()
    forbid_tool_calls: tuple[str, ...] = ()
    require_trace_events: tuple[str, ...] = ()
    require_tool_error_substrings: tuple[str, ...] = ()
    follow_up_prompts: tuple[str, ...] = ()
    follow_up_min_tools: tuple[int, ...] = ()
    follow_up_required_tools: tuple[tuple[str, ...], ...] = ()
    follow_up_any_of_tools: tuple[tuple[str, ...], ...] = ()
    follow_up_allow_zero_tools: tuple[bool, ...] = ()
    allow_zero_tool_final_reply: bool = False
    allow_confirmation_timeout: bool = False
    # ── Recovery / negative-path scoring (opt-in; default no-op) ──────────────
    # Total tool-call ceiling across the whole case. 0 = no ceiling. Catches the
    # over-action / retry-loop failure class (e.g. re-filing the same batch).
    max_tool_calls: int = 0
    # Per-tool call ceilings, e.g. (("kanban_create_batch", 1),). A tool exceeding
    # its ceiling is a loop signal even when other tools are bounded.
    max_calls_per_tool: tuple[tuple[str, int], ...] = ()
    # Per-tool ceilings counting ONLY successful (non-error) results. Distinguishes
    # legitimate validation-driven self-correction (rejected attempts, §5) from a
    # genuine over-action loop (duplicate *successful* side effects). e.g.
    # (("kanban_create_batch", 1),) = at most one batch may actually be created.
    max_successful_calls_per_tool: tuple[tuple[str, int], ...] = ()
    # Assert the run actually finalized with a non-empty assistant reply — i.e.
    # the agent recovered from the error and answered, rather than spinning or
    # going silent. The companion to require_tool_error_substrings.
    require_final_reply: bool = False
    # HTTP-prime a coordinator batch on this thread before the prompt (recovery
    # policy-block cases). Makes overlap denial deterministic vs same-turn races.
    prime_coordinator_batch: bool = False
    # ── User Model trust-boundary verification (opt-in; default no-op) ────────
    # Snapshot GET /user_model (model last_updated + pending proposal ids) right
    # before the case prompt so post-run HTTP deltas can be asserted. Implied by
    # any of the assert_um_* fields below.
    prime_user_model_snapshot: bool = False
    # After the run, a reviewable proposal must exist for the user via the
    # propose_user_model_update arbitration path: either GET /user_model/proposals
    # shows ≥1 *new* pending proposal vs the pre-prompt snapshot, OR the propose
    # tool reported an equivalent near-duplicate already pending (dedupe). The
    # dedupe arm keeps this assertion deterministic regardless of pre-existing
    # proposal-pool state.
    assert_um_proposal_created: bool = False
    # The model's ``last_updated`` must be unchanged vs the snapshot — proves a
    # proposal / refused direct write did NOT silently mutate the durable model.
    assert_um_model_unchanged: bool = False
    # The model's ``last_updated`` must change vs the snapshot — proves an
    # explicitly-stated fact actually round-tripped through patch_user_model.
    assert_um_model_changed: bool = False
    # HTTP-only case: skip MLX agent turn; run deterministic HTTP assertions instead.
    http_only: bool = False
    # When follow_up_prompts is set, substrings required in the assistant reply to
    # case.prompt (segment 0). Scoring strips the synthetic init+welcome prefix
    # that _prime_welcome inserts before the case prompt (production parity).
    require_initial_assistant_substrings: tuple[str, ...] = ()
    notes: str = ""


@dataclass
class CaseResult:
    case_id: str
    passed: bool
    thread_id: str
    elapsed_sec: float
    pillar: str = "core"
    tool_calls: list[str] = field(default_factory=list)
    turn_end_reasons: list[str] = field(default_factory=list)
    failures: list[str] = field(default_factory=list)
    assistant_tail: str = ""
    timed_out: bool = False
    # Retry/flake bookkeeping: how many attempts ran, and whether a later attempt
    # passed after an earlier timeout-only failure (load-induced flake, not a bug).
    attempts: int = 1
    flaky: bool = False
    first_attempt_failures: list[str] = field(default_factory=list)
