// ---------------------------------------------------------------------------
// RecordingToolProvider — records list/execute order and configurable outcomes.
// ---------------------------------------------------------------------------

///|
/// Configurable outcome for a tool call: success outcome, or raised runtime
/// error. Constructors are prefixed with `Outcome` to avoid ambiguity with
/// `Result::Ok`/`Result::Err` at unqualified call sites.
pub(all) enum ScriptedToolOutcome {
  OutcomeOk(@kernel.ToolOutcome)
  OutcomeErr(@error.RuntimeError)
} derive(Debug)

///|
/// A recorded tool execution: call id, tool name, and outcome.
pub(all) struct ToolExecRecord {
  requested_name : String
  call_id : String
  tool_name : String
  arguments : Json
  outcome : ScriptedToolOutcome
} derive(Debug)

///|
/// Unified provider operation trace, preserving list/execute interleaving.
pub(all) enum ToolProviderOp {
  ListTools
  ExecuteTool(requested_name~ : String, call~ : @kernel.ToolCall)
} derive(Debug)

///|
fn testkit_snapshot_tool_outcome(
  outcome : ScriptedToolOutcome,
) -> ScriptedToolOutcome {
  match outcome {
    OutcomeOk(value) => OutcomeOk(snapshot_tool_outcome(value))
    OutcomeErr(error) => OutcomeErr(error)
  }
}

///|
/// ToolProvider fake that declares a fixed tool list and routes execute() by
/// tool name through a configurable outcome map. Unknown tools raise
/// `RuntimeError::UnknownTool`. Every execute is recorded with its call id.
pub(all) struct RecordingToolProvider {
  tool_defs : Array[@kernel.ToolDef]
  outcomes : Map[String, ScriptedToolOutcome]
  mut list_calls : Int
  mut exec_records : Array[ToolExecRecord]
  mut ops : Array[ToolProviderOp]
}

///|
pub fn RecordingToolProvider::RecordingToolProvider(
  tool_defs : Array[@kernel.ToolDef],
  outcomes : Map[String, ScriptedToolOutcome],
) -> RecordingToolProvider {
  let copied_outcomes : Map[String, ScriptedToolOutcome] = Map::from_array([])
  for key in outcomes.keys() {
    copied_outcomes[key] = testkit_snapshot_tool_outcome(outcomes[key])
  }
  {
    tool_defs: tool_defs.map(testkit_snapshot_tool_def),
    outcomes: copied_outcomes,
    list_calls: 0,
    exec_records: [],
    ops: [],
  }
}

///|
/// Number of times list_tools was called.
pub fn RecordingToolProvider::list_call_count(
  self : RecordingToolProvider,
) -> Int {
  self.list_calls
}

///|
/// Snapshot of recorded executions (call id, tool name, outcome).
pub fn RecordingToolProvider::exec_records(
  self : RecordingToolProvider,
) -> Array[ToolExecRecord] {
  self.exec_records.map(fn(record) {
    {
      requested_name: record.requested_name,
      call_id: record.call_id,
      tool_name: record.tool_name,
      arguments: snapshot_json(record.arguments),
      outcome: testkit_snapshot_tool_outcome(record.outcome),
    }
  })
}

///|
/// Snapshot of list/execute operations in their exact observed order.
pub fn RecordingToolProvider::ops(
  self : RecordingToolProvider,
) -> Array[ToolProviderOp] {
  self.ops.map(fn(op) {
    match op {
      ListTools => ListTools
      ExecuteTool(requested_name~, call~) =>
        ExecuteTool(requested_name~, call=snapshot_tool_call(call))
    }
  })
}

///|
/// Direct (non-trait) execute entry point for testkit self-tests.
pub async fn RecordingToolProvider::execute_direct(
  self : RecordingToolProvider,
  name : String,
  call : @kernel.ToolCall,
) -> @kernel.ToolOutcome raise @error.RuntimeError {
  (self as &@port.ToolProvider).execute(name, call)
}

///|
pub impl @port.ToolProvider for RecordingToolProvider with fn list_tools(self) {
  self.list_calls = self.list_calls + 1
  self.ops.push(ListTools)
  self.tool_defs.map(testkit_snapshot_tool_def)
}

///|
pub impl @port.ToolProvider for RecordingToolProvider with fn execute(
  self,
  name : String,
  call : @kernel.ToolCall,
) -> @kernel.ToolOutcome raise @error.RuntimeError {
  let key = call.name.to_string()
  let outcome = match self.outcomes.get(key) {
    Some(o) => o
    None =>
      OutcomeErr(
        @error.RuntimeError::UnknownTool(
          "recording_tool: no outcome configured for '\{key}'",
        ),
      )
  }
  self.ops.push(ExecuteTool(requested_name=name, call=snapshot_tool_call(call)))
  self.exec_records.push({
    requested_name: name,
    call_id: call.call_id.to_string(),
    tool_name: call.name.to_string(),
    arguments: snapshot_json(call.arguments),
    outcome: testkit_snapshot_tool_outcome(outcome),
  })
  match outcome {
    OutcomeOk(value) => snapshot_tool_outcome(value)
    OutcomeErr(e) => raise e
  }
}