Feature: Tool Call Router
  As a developer integrating multiple LLM providers
  I want a tool call router that normalizes provider-specific formats
  So that tools execute uniformly regardless of the calling provider

  # ---- Provider Format Detection ----

  Scenario: Detect OpenAI format from arguments string
    Given a tool call payload with name "test/echo" and arguments '{"key": "value"}'
    When I detect the provider format
    Then the detected format should be "openai"

  Scenario: Detect Anthropic format from input dict
    Given a tool call payload with name "test/echo" and input {"key": "value"}
    When I detect the provider format
    Then the detected format should be "anthropic"

  Scenario: Detect LangChain format from type tool_call
    Given a tool call payload with name "test/echo" and type "tool_call" and args {"key": "value"}
    When I detect the provider format
    Then the detected format should be "langchain"

  Scenario: Detect LangChain format from args key
    Given a tool call payload with name "test/echo" and args key only
    When I detect the provider format
    Then the detected format should be "langchain"

  Scenario: Detect unknown format from empty payload
    Given an empty tool call payload
    When I detect the provider format
    Then the detected format should be "unknown"

  Scenario: Detect OpenAI format with dict arguments
    Given a tool call payload with name "test/echo" and dict arguments {"a": 1}
    When I detect the provider format
    Then the detected format should be "openai"

  # ---- Payload Normalization ----

  Scenario: Normalize OpenAI payload
    Given a tool call payload with name "test/echo" and arguments '{"key": "value"}'
    When I normalize the tool call
    Then the normalized request tool_name should be "test/echo"
    And the normalized request arguments should have key "key"
    And the normalized request provider_format should be "openai"

  Scenario: Normalize Anthropic payload
    Given a tool call payload with name "test/echo" and input {"msg": "hello"}
    When I normalize the tool call
    Then the normalized request tool_name should be "test/echo"
    And the normalized request arguments should have key "msg"
    And the normalized request provider_format should be "anthropic"

  Scenario: Normalize LangChain payload
    Given a tool call payload with name "test/echo" and type "tool_call" and args {"data": 42}
    When I normalize the tool call
    Then the normalized request tool_name should be "test/echo"
    And the normalized request arguments should have key "data"
    And the normalized request provider_format should be "langchain"

  Scenario: Normalize payload with invalid JSON arguments raises error
    Given a tool call with invalid JSON arguments
    When I try to normalize the tool call
    Then a router ValueError should be raised containing "parse error"

  Scenario: Normalize payload without name raises error
    Given a tool call payload without a name
    When I try to normalize the tool call
    Then a router ValueError should be raised containing "name"

  Scenario: Normalize non-dict payload raises error
    When I try to normalize a non-dict payload
    Then a router ValueError should be raised containing "dict"

  Scenario: Normalize unknown format with dict arguments
    Given a tool call payload with name "test/echo" and parameters key {"x": 1}
    When I normalize the tool call
    Then the normalized request tool_name should be "test/echo"
    And the normalized request arguments should have key "x"

  # ---- Stable ID Generation ----

  Scenario: Generate deterministic tool call ID
    When I generate a tool call ID for plan "plan-001" sequence 0
    Then the tool call ID should start with "tc_"
    And the tool call ID should have length 27

  Scenario: Same inputs produce same ID
    When I generate a tool call ID for plan "plan-001" sequence 5
    And I generate another tool call ID for plan "plan-001" sequence 5
    Then both tool call IDs should be identical

  Scenario: Different inputs produce different IDs
    When I generate a tool call ID for plan "plan-001" sequence 0
    And I generate another tool call ID for plan "plan-001" sequence 1
    Then the tool call IDs should differ

  Scenario: Empty plan_id raises ValueError
    When I try to generate a tool call ID with empty plan_id
    Then a router ValueError should be raised containing "plan_id"

  Scenario: Negative sequence raises ValueError
    When I try to generate a tool call ID with negative sequence
    Then a router ValueError should be raised containing "sequence"

  # ---- Router Execution ----

  Scenario: Route OpenAI tool call to echo tool
    Given a tool registry with an echo tool "test/echo"
    And a tool call router for plan "plan-001"
    And an OpenAI-format tool call for "test/echo" with arguments '{"msg": "hi"}'
    When I route the tool call
    Then the normalized result should be successful
    And the normalized result tool_name should be "test/echo"
    And the normalized result provider_format should be "openai"
    And the normalized result tool_call_id should start with "tc_"

  Scenario: Route Anthropic tool call to echo tool
    Given a tool registry with an echo tool "test/echo"
    And a tool call router for plan "plan-002"
    And an Anthropic-format tool call for "test/echo" with input {"msg": "hi"}
    When I route the tool call
    Then the normalized result should be successful
    And the normalized result provider_format should be "anthropic"

  Scenario: Route LangChain tool call to echo tool
    Given a tool registry with an echo tool "test/echo"
    And a tool call router for plan "plan-003"
    And a LangChain-format tool call for "test/echo" with args {"msg": "hi"}
    When I route the tool call
    Then the normalized result should be successful
    And the normalized result provider_format should be "langchain"

  Scenario: Route tool call for missing tool
    Given a tool registry with an echo tool "test/echo"
    And a tool call router for plan "plan-004"
    And an OpenAI-format tool call for "test/missing" with arguments '{}'
    When I route the tool call
    Then the normalized result should not be successful
    And the normalized result error_category should be "not_found"

  Scenario: Route tool call with invalid payload
    Given a tool registry with an echo tool "test/echo"
    And a tool call router for plan "plan-005"
    And a tool call payload without a name
    When I route the tool call
    Then the normalized result should not be successful
    And the normalized result error_category should be "parse"

  Scenario: Route with provider metadata
    Given a tool registry with an echo tool "test/echo"
    And a tool call router for plan "plan-006"
    And an OpenAI-format tool call for "test/echo" with arguments '{"msg": "hi"}'
    And provider metadata with model "gpt-4" and provider_id "openai"
    When I route the tool call with provider metadata
    Then the normalized result provider_metadata should contain key "model"

  # ---- Batch Routing ----

  Scenario: Route batch of tool calls
    Given a tool registry with an echo tool "test/echo"
    And a tool call router for plan "plan-010"
    And a batch of 3 OpenAI-format tool calls for "test/echo"
    When I route the batch
    Then the batch should return 3 results
    And all batch results should be successful

  Scenario: Route empty batch
    Given a tool registry with an echo tool "test/echo"
    And a tool call router for plan "plan-011"
    When I route an empty batch
    Then the batch should return 0 results

  # ---- Streaming Execution ----

  Scenario: Route streaming tool call emits updates
    Given a tool registry with an echo tool "test/echo"
    And a tool call router for plan "plan-020"
    And an OpenAI-format tool call for "test/echo" with arguments '{"msg": "stream"}'
    When I route the tool call with streaming
    Then the stream should emit a pending update
    And the stream should emit a running update
    And the stream should emit a complete update
    And the stream should emit a final result

  Scenario: Route streaming tool call for failing tool
    Given a tool registry with a failing tool "test/fail"
    And a tool call router for plan "plan-021"
    And an OpenAI-format tool call for "test/fail" with arguments '{}'
    When I route the tool call with streaming
    Then the stream should emit a complete update
    And the stream should emit a final result that is not successful

  Scenario: Route streaming with invalid payload
    Given a tool registry with an echo tool "test/echo"
    And a tool call router for plan "plan-022"
    And a tool call payload without a name
    When I route the tool call with streaming
    Then the stream should emit a final result that is not successful

  # ---- Validation Tool Surfacing ----

  Scenario: Route call to validation tool surfaces pass result
    Given a tool registry with a passing validation tool "test/validator"
    And a tool call router for plan "plan-030"
    And an OpenAI-format tool call for "test/validator" with arguments '{}'
    When I route the tool call
    Then the normalized result is_validation should be True
    And the normalized result validation_passed should be True

  Scenario: Route call to validation tool surfaces fail result
    Given a tool registry with a failing validation tool "test/validator-fail"
    And a tool call router for plan "plan-031"
    And an OpenAI-format tool call for "test/validator-fail" with arguments '{}'
    When I route the tool call
    Then the normalized result is_validation should be True
    And the normalized result validation_passed should be False

  # ---- Error Classification ----

  Scenario: Classify timeout error
    When I classify the error "Operation timed out after 30s"
    Then the error category should be "timeout"

  Scenario: Classify permission error
    When I classify the error "Permission denied: cannot write"
    Then the error category should be "permission"

  Scenario: Classify not found error
    When I classify the error "Tool 'x/y' not found"
    Then the error category should be "not_found"

  Scenario: Classify resource error
    When I classify the error "Insufficient memory for operation"
    Then the error category should be "resource"

  Scenario: Classify schema error
    When I classify the error "Schema validation failed"
    Then the error category should be "schema"

  Scenario: Classify parse error
    When I classify the error "Failed to parse response"
    Then the error category should be "parse"

  Scenario: Classify generic execution error
    When I classify the error "Something unexpected happened"
    Then the error category should be "execution"

  Scenario: Classify empty error
    When I classify an empty error message
    Then the error category should be "unknown"

  # ---- Schema Normalization ----

  Scenario: Normalize schema for OpenAI provider
    Given a tool spec named "test/echo" with description "Echo tool"
    When I normalize the schema for "openai" provider
    Then the normalized schema should have key "parameters"
    And the normalized schema should have name "test/echo"

  Scenario: Normalize schema for Anthropic provider
    Given a tool spec named "test/echo" with description "Echo tool"
    When I normalize the schema for "anthropic" provider
    Then the normalized schema should have key "input_schema"

  Scenario: Normalize schema for LangChain provider
    Given a tool spec named "test/echo" with description "Echo tool"
    When I normalize the schema for "langchain" provider
    Then the normalized schema should have key "args_schema"

  Scenario: Normalize schema truncates long description
    Given a tool spec named "test/echo" with a very long description
    When I normalize the schema for "openai" provider with max length 50
    Then the normalized schema description should be at most 50 characters

  Scenario: Normalize schema with invalid max length raises error
    Given a tool spec named "test/echo" with description "Echo tool"
    When I try to normalize the schema with max_description_length 0
    Then a router ValueError should be raised containing "max_description_length"

  # ---- Schema Export ----

  Scenario: Export schemas for OpenAI provider
    Given a tool registry with an echo tool "test/echo"
    And a tool call router for plan "plan-040"
    When I export schemas for "openai" provider
    Then the exported schemas should contain 1 schema
    And the first exported schema should have tool_type "tool"

  # ---- Sequence Counter ----

  Scenario: Router sequence increments on each route
    Given a tool registry with an echo tool "test/echo"
    And a tool call router for plan "plan-050"
    And an OpenAI-format tool call for "test/echo" with arguments '{}'
    When I route the tool call
    Then the router sequence should be 1
    When I route the tool call
    Then the router sequence should be 2

  # ---- Router Construction Validation ----

  Scenario: Router with empty plan_id raises ValueError
    Given a tool registry with an echo tool "test/echo"
    When I try to create a router with empty plan_id
    Then a router ValueError should be raised containing "plan_id"

  Scenario: Non-dict payload raises ValueError on route
    Given a tool registry with an echo tool "test/echo"
    And a tool call router for plan "plan-060"
    When I try to route a non-dict payload
    Then a router ValueError should be raised containing "dict"

  Scenario: Non-list payloads raises ValueError on route_batch
    Given a tool registry with an echo tool "test/echo"
    And a tool call router for plan "plan-061"
    When I try to route_batch a non-list payload
    Then a router ValueError should be raised containing "list"

  # ---- Detect format for non-dict ----

  Scenario: Detect format for non-dict returns unknown
    When I detect format for a non-dict value
    Then the detected format should be "unknown"

  # ---- Coverage: normalize with dict args, Anthropic/LangChain branches ----

  Scenario: Normalize OpenAI payload with dict arguments
    Given a tool call payload with name "test/echo" and dict arguments
    When I normalize the tool call for "openai" format
    Then the normalized request should have tool name "test/echo"
    And the normalized arguments should be a dict with key "msg"

  Scenario: Normalize LangChain payload with dict args
    Given a LangChain payload with dict args
    When I normalize the tool call for "langchain" format
    Then the normalized request should have tool name "test/echo"

  Scenario: Normalize LangChain payload with non-dict args raises ValueError
    Given a LangChain payload with list args
    When I try to normalize the invalid langchain args
    Then a router ValueError should be raised containing "must be a dict"

  Scenario: Normalize unknown payload with parameters key
    Given an unknown format payload with parameters key
    When I normalize the tool call for "unknown" format
    Then the normalized arguments should have key "query"

  Scenario: Normalize unknown payload with JSON string args
    Given an unknown format payload with JSON string in args key
    When I normalize the tool call for "unknown" format
    Then the normalized arguments should have key "data"

  Scenario: Normalize unknown payload with bad JSON skips to next key
    Given an unknown format payload with bad JSON and valid fallback
    When I normalize the tool call for "unknown" format
    Then the normalized arguments should have key "ok"

  # ---- Coverage: schema normalization LangChain and unknown ----

  Scenario: Schema normalization for LangChain uses args_schema
    Given a tool spec named "test/echo"
    When I normalize the schema for "langchain" provider
    Then the schema should have key "args_schema"

  Scenario: Schema normalization for unknown uses input_schema
    Given a tool spec named "test/echo"
    When I normalize the schema for "unknown" provider
    Then the schema should have key "input_schema"

  # ---- Coverage: plan_id property ----

  Scenario: Router plan_id property returns correct value
    Given a tool registry with an echo tool "test/echo"
    And a tool call router for plan "plan-prop-test"
    Then the router plan_id should be "plan-prop-test"

  # ---- Coverage: route generic exception, validation surfacing, error classification ----

  Scenario: Route catches generic RuntimeError during execution
    Given a tool registry with a tool that raises RuntimeError
    And a tool call router for plan "plan-runtime-err"
    When I route an OpenAI payload for the error tool
    Then the route result should have success false
    And the route result error should contain "boom"

  Scenario: Route returns error classification for failed ToolResult
    Given a tool registry with a tool that returns failed result
    And a tool call router for plan "plan-fail-result"
    When I route an OpenAI payload for the fail tool
    Then the route result should have success false
    And the route result should have error_category set

  Scenario: Route validates validation tool with passed result
    Given a tool registry with a validation tool
    And a tool call router for plan "plan-valid-check"
    When I route an OpenAI payload for the validation tool
    Then the route result should have is_validation true
    And the route result should have validation_passed true

  # ---- Coverage: streaming exception and validation ----

  Scenario: Streaming route catches exception and yields ERROR
    Given a tool registry with a tool that raises RuntimeError
    And a tool call router for plan "plan-stream-err"
    When I stream-route an OpenAI payload for the error tool
    Then the streaming updates should include ERROR status
    And the streaming final result should have success false

  Scenario: Streaming route non-dict payload raises ValueError
    Given a tool registry with an echo tool "test/echo"
    And a tool call router for plan "plan-stream-ndict"
    When I try to stream-route a non-dict payload
    Then a router ValueError should be raised containing "payload must be a dict"

  Scenario: Streaming route validates validation tool result
    Given a tool registry with a validation tool
    And a tool call router for plan "plan-stream-valid"
    When I stream-route an OpenAI payload for the validation tool
    Then the streaming final result should have is_validation true

  # ---- Coverage: get_tool_schemas validation annotation ----

  Scenario: Export schemas annotates validation tools
    Given a tool registry with a validation tool
    And a tool call router for plan "plan-schema-export"
    When I export tool schemas for "openai"
    Then the exported schemas should include a tool with type "validation"

  Scenario: Export schemas annotates regular tools
    Given a tool registry with an echo tool "test/echo"
    And a tool call router for plan "plan-schema-regular"
    When I export tool schemas for "openai"
    Then the exported schemas should include a tool with type "tool"

  # ---- Coverage: _get_validation_mode ----

  Scenario: Validation mode extracted from output_schema
    Given a tool registry with a validation tool with mode "strict"
    And a tool call router for plan "plan-vmode"
    When I export tool schemas for "openai"
    Then the exported validation schema should have mode "strict"

  Scenario: Validation mode returns None for non-string mode
    Given a tool registry with a validation tool with numeric mode
    And a tool call router for plan "plan-vmode-num"
    When I export tool schemas for "openai"
    Then the exported validation schema should not have a mode
