package openai

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	edgeservice "iop/apps/edge/internal/service"
	"iop/packages/go/config"
	iop "iop/proto/gen/iop"
)

func TestChatCompletionsRetriesUnknownTextToolCallBeforeResponse(t *testing.T) {
	// Attempt 1: unknown tool call (triggers retry)
	run1Events := make(chan *iop.RunEvent, 2)
	run1Events <- &iop.RunEvent{Type: "delta", Delta: "<tool_call><function=delete_everything><parameter=confirm>true</parameter></function></tool_call>"}
	run1Events <- &iop.RunEvent{Type: "complete"}

	// Attempt 2: valid tool call (succeeds)
	run2Events := make(chan *iop.RunEvent, 2)
	run2Events <- &iop.RunEvent{Type: "delta", Delta: "<tool_call><function=run_commands><parameter=commands>[\"ls\"]\n</parameter></function></tool_call>"}
	run2Events <- &iop.RunEvent{Type: "complete"}

	fake := &fakeRunService{eventRuns: []chan *iop.RunEvent{run1Events, run2Events}}
	srv := NewServer(config.EdgeOpenAIConf{Adapter: "cli", Target: "codex"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"qwen3.6:35b",
		"messages":[{"role":"user","content":"status"}],
		"tools":[{"type":"function","function":{"name":"run_commands","parameters":{"type":"object","properties":{"commands":{"type":"array","items":{"type":"string"}}}}}}]
	}`))
	w := httptest.NewRecorder()

	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	var resp chatCompletionResponse
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if len(resp.Choices) == 0 {
		t.Fatal("empty choices")
	}
	choice := resp.Choices[0]
	if choice.FinishReason != "tool_calls" || len(choice.Message.ToolCalls) == 0 {
		t.Fatalf("expected synthesized tool calls: %+v", choice)
	}
}

func TestChatCompletionsFailsUnknownTextToolCallAfterRetryLimit(t *testing.T) {
	run1Events := make(chan *iop.RunEvent, 2)
	run1Events <- &iop.RunEvent{Type: "delta", Delta: "<tool_call><function=unknown><parameter=arg>val</parameter></function></tool_call>"}
	run1Events <- &iop.RunEvent{Type: "complete"}

	run2Events := make(chan *iop.RunEvent, 2)
	run2Events <- &iop.RunEvent{Type: "delta", Delta: "<tool_call><function=unknown><parameter=arg>val</parameter></function></tool_call>"}
	run2Events <- &iop.RunEvent{Type: "complete"}

	fake := &fakeRunService{eventRuns: []chan *iop.RunEvent{run1Events, run2Events}}
	srv := NewServer(config.EdgeOpenAIConf{Adapter: "cli", Target: "codex"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"qwen3.6:35b",
		"messages":[{"role":"user","content":"status"}],
		"tools":[{"type":"function","function":{"name":"run_commands"}}]
	}`))
	w := httptest.NewRecorder()

	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusBadGateway {
		t.Fatalf("status: got %d, want 502; body=%s", w.Code, w.Body.String())
	}
	var resp errorResponse
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if resp.Error.Type != "tool_validation_error" {
		t.Fatalf("unexpected error type: got %q, want tool_validation_error", resp.Error.Type)
	}
}

func TestChatCompletionsProviderStreamSynthesizesTextToolCalls(t *testing.T) {
	fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "thought text "}
	fake.events <- &iop.RunEvent{Type: "delta", Delta: "<tool_call><function=run_commands><parameter=commands>[\"echo 1\"]</parameter></function></tool_call>"}
	fake.events <- &iop.RunEvent{Type: "complete"}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"metadata":{},
		"model":"qwen3.6:35b",
		"messages":[{"role":"user","content":"status"}],
		"tools":[{"type":"function","function":{"name":"run_commands","parameters":{"type":"object"}}}],
		"stream": true
	}`))
	w := httptest.NewRecorder()

	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	body := w.Body.String()
	if !strings.Contains(body, `"tool_calls"`) {
		t.Fatalf("expected stream to contain tool_calls: %s", body)
	}
	if strings.Contains(body, `"<tool_call>"`) {
		t.Fatalf("stream should not contain raw tool_call block: %s", body)
	}
}

func TestChatCompletionsProviderStreamBlocksUnknownTextToolCall(t *testing.T) {
	invalidRun := func() chan *iop.RunEvent {
		return bufferedRunEvents(
			&iop.RunEvent{Type: "delta", Delta: "thought text "},
			&iop.RunEvent{Type: "delta", Delta: "<tool_call><function=unknown_tool><parameter=arg>val</parameter></function></tool_call>"},
			&iop.RunEvent{Type: "complete"},
		)
	}
	fake := &fakeRunService{
		eventRuns: []chan *iop.RunEvent{
			invalidRun(),
			invalidRun(),
		},
		runIDs: []string{"run-bad-1", "run-bad-2"},
	}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"metadata":{},
		"model":"qwen3.6:35b",
		"messages":[{"role":"user","content":"status"}],
		"tools":[{"type":"function","function":{"name":"run_commands"}}],
		"stream": true
	}`))
	w := httptest.NewRecorder()

	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	body := w.Body.String()
	if !strings.Contains(body, "tool_validation_error") {
		t.Fatalf("expected stream to end with tool_validation_error: %s", body)
	}
	if strings.Contains(body, `\u003ctool_call\u003e`) {
		t.Fatalf("unknown tool call leaked as content: %s", body)
	}
	if reqs := fake.reqsSnapshot(); len(reqs) != 2 {
		t.Fatalf("submit attempts: got %d want 2", len(reqs))
	}
}

func TestChatCompletionsStrictBufferedStreamFailsUnknownTextToolCallAfterRetryLimit(t *testing.T) {
	run1Events := make(chan *iop.RunEvent, 2)
	run1Events <- &iop.RunEvent{Type: "delta", Delta: "<tool_call><function=unknown><parameter=arg>val</parameter></function></tool_call>"}
	run1Events <- &iop.RunEvent{Type: "complete"}

	run2Events := make(chan *iop.RunEvent, 2)
	run2Events <- &iop.RunEvent{Type: "delta", Delta: "<tool_call><function=unknown><parameter=arg>val</parameter></function></tool_call>"}
	run2Events <- &iop.RunEvent{Type: "complete"}

	fake := &fakeRunService{eventRuns: []chan *iop.RunEvent{run1Events, run2Events}}
	srv := NewServer(config.EdgeOpenAIConf{Adapter: "cli", Target: "codex", StrictOutput: true, StrictStreamBuffer: true}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"qwen3.6:35b",
		"messages":[{"role":"user","content":"status"}],
		"tools":[{"type":"function","function":{"name":"run_commands"}}],
		"stream": true
	}`))
	w := httptest.NewRecorder()

	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	body := w.Body.String()
	if !strings.Contains(body, "tool_validation_error") {
		t.Fatalf("expected stream to end with tool_validation_error: %s", body)
	}
}

func TestChatCompletionsFailsMalformedTextToolCallAfterRetryLimit(t *testing.T) {
	invalidComplete := func() chan *iop.RunEvent {
		ch := make(chan *iop.RunEvent, 2)
		ch <- &iop.RunEvent{Type: "delta", Delta: "여기 툴 콜입니다. <tool_call><function=run_commands><parameter=commands>"}
		ch <- &iop.RunEvent{Type: "complete"}
		return ch
	}
	fake := &fakeRunService{
		eventRuns: []chan *iop.RunEvent{
			invalidComplete(),
			invalidComplete(),
		},
		runIDs: []string{"run-bad-1", "run-bad-2"},
	}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"metadata":{},
		"model":"qwen3.6:35b",
		"messages":[{"role":"user","content":"status"}],
		"tools":[{"type":"function","function":{"name":"run_commands","parameters":{"type":"object"}}}],
		"tool_choice":"auto"
	}`))
	w := httptest.NewRecorder()

	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusBadGateway {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if !strings.Contains(w.Body.String(), `"type":"tool_validation_error"`) || !strings.Contains(w.Body.String(), "malformed tool call: unclosed tool_call block") {
		t.Fatalf("expected tool validation error body, got %s", w.Body.String())
	}
	if reqs := fake.reqsSnapshot(); len(reqs) != 2 {
		t.Fatalf("submit attempts: got %d want 2", len(reqs))
	}
}

func TestChatCompletionsProviderStreamBlocksMalformedTextToolCall(t *testing.T) {
	invalidRun := func() chan *iop.RunEvent {
		return bufferedRunEvents(
			&iop.RunEvent{Type: "delta", Delta: "이것은 정상 텍스트 "},
			&iop.RunEvent{Type: "delta", Delta: "<tool_call><function=run_commands>"},
			&iop.RunEvent{Type: "complete"},
		)
	}
	fake := &fakeRunService{
		eventRuns: []chan *iop.RunEvent{
			invalidRun(),
			invalidRun(),
		},
		runIDs: []string{"run-bad-1", "run-bad-2"},
	}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"metadata":{},
		"model":"qwen3.6:35b",
		"messages":[{"role":"user","content":"status"}],
		"tools":[{"type":"function","function":{"name":"run_commands","parameters":{"type":"object"}}}],
		"tool_choice":"auto",
		"stream": true
	}`))
	w := httptest.NewRecorder()

	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	body := w.Body.String()
	if strings.Contains(body, "<tool_call") {
		t.Fatalf("body should not contain tool_call leak, got: %s", body)
	}
	if !strings.Contains(body, "tool_validation_error") {
		t.Fatalf("expected tool_validation_error, got: %s", body)
	}
	if reqs := fake.reqsSnapshot(); len(reqs) != 2 {
		t.Fatalf("submit attempts: got %d want 2", len(reqs))
	}
}

func TestValidateToolCallResponseValidatesEveryToolCall(t *testing.T) {
	contract := toolValidationContract{
		enabled: true,
		specs: map[string]textToolSpec{
			"run_commands": {
				parameters: map[string]any{
					"type": "object",
					"properties": map[string]any{
						"commands": map[string]any{
							"type": "array",
							"items": map[string]any{
								"type": "string",
							},
						},
					},
					"required": []any{"commands"},
				},
			},
			"read_file": {
				parameters: map[string]any{
					"type": "object",
					"properties": map[string]any{
						"path": map[string]any{
							"type": "string",
						},
					},
					"required": []any{"path"},
				},
			},
		},
	}

	invalidFirst := []any{
		map[string]any{
			"id":   "call_1",
			"type": "object",
			"function": map[string]any{
				"name":      "run_commands",
				"arguments": `{"not_commands":[]}`,
			},
		},
		map[string]any{
			"id":   "call_2",
			"type": "object",
			"function": map[string]any{
				"name":      "read_file",
				"arguments": `{"path":"/a/b"}`,
			},
		},
	}

	err := validateToolCallResponse(contract, invalidFirst, "native")
	if err == nil {
		t.Fatal("expected validation error because first tool call is invalid")
	}
	if !strings.Contains(err.Error(), "tool call 0") || !strings.Contains(err.Error(), "commands is required") {
		t.Fatalf("unexpected error message: %v", err)
	}

	allValid := []any{
		map[string]any{
			"id":   "call_1",
			"type": "object",
			"function": map[string]any{
				"name":      "run_commands",
				"arguments": `{"commands":["ls"]}`,
			},
		},
		map[string]any{
			"id":   "call_2",
			"type": "object",
			"function": map[string]any{
				"name":      "read_file",
				"arguments": `{"path":"/a/b"}`,
			},
		},
	}
	if err := validateToolCallResponse(contract, allValid, "native"); err != nil {
		t.Fatalf("expected no validation error, got: %v", err)
	}
}

func TestChatCompletionsFailsWhenAnySynthesizedTextToolCallViolatesSchema(t *testing.T) {
	invalidComplete := func() chan *iop.RunEvent {
		ch := make(chan *iop.RunEvent, 2)
		ch <- &iop.RunEvent{Type: "delta", Delta: `<tool_call><function=run_commands><parameter=not_commands>["ls"]</parameter></function></tool_call><tool_call><function=read_file><parameter=path>"/a/b"</parameter></function></tool_call>`}
		ch <- &iop.RunEvent{Type: "complete"}
		return ch
	}
	fake := &fakeRunService{
		eventRuns: []chan *iop.RunEvent{
			invalidComplete(),
			invalidComplete(),
		},
		runIDs: []string{"run-bad-1", "run-bad-2"},
	}

	srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"metadata":{},
		"model":"qwen3.6:35b",
		"messages":[{"role":"user","content":"status"}],
		"tools":[
			{"type":"function","function":{"name":"run_commands","parameters":{"type":"object","properties":{"commands":{"type":"array","items":{"type":"string"}}},"required":["commands"]}}},
			{"type":"function","function":{"name":"read_file","parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}}}
		],
		"tool_choice":"auto"
	}`))
	w := httptest.NewRecorder()

	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusBadGateway {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	if !strings.Contains(w.Body.String(), `"type":"tool_validation_error"`) {
		t.Fatalf("expected tool validation error body, got %s", w.Body.String())
	}
}

// TestChatCompletionsProviderPoolNormalizedToolValidationRetryPreservesPoolRequest
// verifies SDD S01: when a provider-pool normalized run fails tool validation,
// the retry calls SubmitProviderPool (not SubmitRun) to preserve ModelGroupKey,
// provider-pool metadata, and input. The retry metadata includes
// iop_tool_validation_attempt=2.
func TestChatCompletionsProviderPoolNormalizedToolValidationRetryPreservesPoolRequest(t *testing.T) {
	badToolEvent := &iop.RunEvent{
		Type: "complete",
		Metadata: map[string]string{
			"finish_reason":                "tool_calls",
			runtimeMetadataOpenAIToolCalls: `[{"id":"call_bad","type":"function","function":{"name":"run_commands","arguments":"{\"commands\":\"git status\"}"}}]`,
		},
	}
	goodToolEvent := &iop.RunEvent{
		Type: "complete",
		Metadata: map[string]string{
			"finish_reason":                "tool_calls",
			runtimeMetadataOpenAIToolCalls: `[{"id":"call_good","type":"function","function":{"name":"run_commands","arguments":"{\"commands\":[\"git status\"]}"}}]`,
		},
	}

	badRun := &fakeRunResult{
		dispatch: edgeservice.RunDispatch{
			RunID:         "run-pool-bad",
			NodeID:        "node-pool",
			ModelGroupKey: "pool-group",
			Adapter:       "ollama",
			Target:        "served",
		},
		events: bufferedRunEvents(badToolEvent),
	}
	goodRun := &fakeRunResult{
		dispatch: edgeservice.RunDispatch{
			RunID:         "run-pool-good",
			NodeID:        "node-pool",
			ModelGroupKey: "pool-group",
			Adapter:       "ollama",
			Target:        "served",
		},
		events: bufferedRunEvents(goodToolEvent),
	}

	fake := &providerFakeRunService{
		poolSubmitResults: []edgeservice.ProviderPoolDispatchResult{
			{Path: edgeservice.ProviderPoolPathNormalized, Run: badRun, DispatchInfo: edgeservice.RunDispatch{RunID: "run-pool-bad", NodeID: "node-pool", ModelGroupKey: "pool-group", Adapter: "ollama", Target: "served"}},
			{Path: edgeservice.ProviderPoolPathNormalized, Run: goodRun, DispatchInfo: edgeservice.RunDispatch{RunID: "run-pool-good", NodeID: "node-pool", ModelGroupKey: "pool-group", Adapter: "ollama", Target: "served"}},
		},
	}
	catalog := []config.ModelCatalogEntry{
		{ID: "pool-ollama-model", Providers: map[string]string{"prov-ollama": "qwen3.6:35b"}},
	}
	srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
	srv.SetModelCatalog(catalog)

	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"pool-ollama-model",
		"messages":[{"role":"user","content":"status"}],
		"tools":[{"type":"function","function":{"name":"run_commands","parameters":{"type":"object","properties":{"commands":{"type":"array","items":{"type":"string"}}},"required":["commands"],"additionalProperties":false}}}],
		"tool_choice":"auto"
	}`))
	w := httptest.NewRecorder()
	srv.handleChatCompletions(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}

	// Two SubmitProviderPool calls: initial + 1 retry.
	reqs := fake.reqsSnapshot()
	if len(reqs) != 2 {
		t.Fatalf("expected 2 SubmitRun (initial + retry), got %d", len(reqs))
	}
	// Both must be provider-pool dispatches.
	if !reqs[0].ProviderPool {
		t.Error("first req must have ProviderPool=true")
	}
	if !reqs[1].ProviderPool {
		t.Error("retry req must have ProviderPool=true")
	}
	// The model field must be preserved (submitted via SubmitProviderPool, not SubmitRun).
	if reqs[0].ModelGroupKey != "pool-ollama-model" {
		t.Errorf("first req ModelGroupKey: got %q", reqs[0].ModelGroupKey)
	}
	if reqs[1].ModelGroupKey != "pool-ollama-model" {
		t.Errorf("retry req ModelGroupKey: got %q", reqs[1].ModelGroupKey)
	}
	// Retry metadata must indicate attempt 2.
	if got := reqs[1].Metadata[runtimeMetadataToolValidationAttempt]; got != "2" {
		t.Errorf("retry attempt metadata: got %q", got)
	}
	if got := reqs[1].Metadata[runtimeMetadataToolValidationRetryOf]; got != "run-pool-bad" {
		t.Errorf("retry retry-of metadata: got %q", got)
	}
}

// TestChatCompletionsProviderPoolToolValidationRetryRejectsTunnelResult verifies
// that when a provider-pool retry returns a tunnel-path result (which would
// cause a nil Run panic downstream), the handler closes the tunnel handle and
// returns a tool_validation_retry_error instead of panicking.
func TestChatCompletionsProviderPoolToolValidationRetryRejectsTunnelResult(t *testing.T) {
	// First run: produces a tool validation failure.
	badToolEvent := &iop.RunEvent{
		Type: "complete",
		Metadata: map[string]string{
			"finish_reason":                "tool_calls",
			runtimeMetadataOpenAIToolCalls: `[{"id":"call_bad","type":"function","function":{"name":"run_commands","arguments":"{\"commands\":\"git status\"}"}}]`,
		},
	}
	badRun := &fakeRunResult{
		dispatch: edgeservice.RunDispatch{
			RunID:         "run-pool-bad",
			NodeID:        "node-pool",
			ModelGroupKey: "pool-group",
			Adapter:       "ollama",
			Target:        "served",
		},
		events: bufferedRunEvents(badToolEvent),
	}

	// The retry result is tunnel-path: this would set Run=nil and cause a panic
	// without the guard. We create a fake tunnel handle so the guard can close it.
	tunnelFrames := make(chan *iop.ProviderTunnelFrame, 4)
	tunnelFrames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START, StatusCode: 200}
	tunnelFrames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte(`{"ok":true}`)}
	tunnelFrames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true}
	close(tunnelFrames)

	fakeTunnel := &fakeTunnelHandle{
		dispatch: edgeservice.RunDispatch{
			RunID:         "run-pool-tunnel-retry",
			NodeID:        "node-pool",
			ModelGroupKey: "pool-group",
			Adapter:       "vllm",
			Target:        "served",
		},
		headers:     map[string]string{},
		frames:      tunnelFrames,
		waitTimeout: 5 * time.Second,
	}

	fake := &providerFakeRunService{
		poolSubmitResults: []edgeservice.ProviderPoolDispatchResult{
			{Path: edgeservice.ProviderPoolPathNormalized, Run: badRun, DispatchInfo: edgeservice.RunDispatch{RunID: "run-pool-bad", NodeID: "node-pool", ModelGroupKey: "pool-group", Adapter: "ollama", Target: "served"}},
			{Path: edgeservice.ProviderPoolPathTunnel, Tunnel: fakeTunnel, DispatchInfo: edgeservice.RunDispatch{RunID: "run-pool-tunnel-retry", NodeID: "node-pool", ModelGroupKey: "pool-group", Adapter: "vllm", Target: "served"}},
		},
	}
	catalog := []config.ModelCatalogEntry{
		{ID: "pool-ollama-model", Providers: map[string]string{"prov-ollama": "qwen3.6:35b"}},
	}
	srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
	srv.SetModelCatalog(catalog)

	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
		"model":"pool-ollama-model",
		"messages":[{"role":"user","content":"status"}],
		"tools":[{"type":"function","function":{"name":"run_commands","parameters":{"type":"object","properties":{"commands":{"type":"array","items":{"type":"string"}}},"required":["commands"],"additionalProperties":false}}}],
		"tool_choice":"auto"
	}`))
	w := httptest.NewRecorder()
	srv.handleChatCompletions(w, req)

	// The handler must reject the tunnel-path retry result with 502.
	if w.Code != http.StatusBadGateway {
		t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
	}
	var resp map[string]any
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	errMap, ok := resp["error"].(map[string]any)
	if !ok {
		t.Fatalf("expected error map in response: %s", w.Body.String())
	}
	if errMap["type"] != "tool_validation_retry_error" {
		t.Errorf("error type: got %q", errMap["type"])
	}
	if errMap["message"] != "provider-pool retry selected tunnel path" {
		t.Errorf("error message: got %q", errMap["message"])
	}
}

// --- runtime-enabled tool validation: Core is the single recovery owner ------

// streamGateToolValidationServer builds a runtime-enabled server whose Core
// recovery budget is the only retry budget in the request.
func streamGateToolValidationServer(t *testing.T, enabled bool, maxRequestFaultRecovery int, runs ...chan *iop.RunEvent) (*Server, *fakeRunService) {
	t.Helper()
	fault := maxRequestFaultRecovery
	fake := &fakeRunService{eventRuns: runs}
	srv := NewServer(config.EdgeOpenAIConf{
		Adapter: "cli",
		Target:  "codex",
		StreamEvidenceGate: config.StreamEvidenceGateConf{
			Enabled:                 enabled,
			MaxRequestFaultRecovery: &fault,
		},
	}, fake, nil)
	return srv, fake
}

const streamGateToolValidationBody = `{
	"model":"qwen3.6:35b",
	"messages":[{"role":"user","content":"status"}],
	"tools":[{"type":"function","function":{"name":"run_commands","parameters":{"type":"object","properties":{"commands":{"type":"array","items":{"type":"string"}}}}}}]
}`

func invalidToolCallRun() chan *iop.RunEvent {
	return bufferedRunEvents(
		&iop.RunEvent{Type: "delta", Delta: "<tool_call><function=delete_everything><parameter=confirm>true</parameter></function></tool_call>"},
		&iop.RunEvent{Type: "complete"},
	)
}

func validToolCallRun() chan *iop.RunEvent {
	return bufferedRunEvents(
		&iop.RunEvent{Type: "delta", Delta: "planning. <tool_call><function=run_commands><parameter=commands>[\"ls\"]\n</parameter></function></tool_call>"},
		&iop.RunEvent{Type: "complete"},
	)
}

// TestStreamGateEnabledToolValidationHasSingleRecoveryOwner proves the Core
// request runtime is the only retry owner on the runtime-enabled path: the
// number of provider dispatches follows the Core recovery budget exactly, so a
// budget of 0 must fail after one dispatch even though the legacy loop would
// have retried once and succeeded on the same fixture.
func TestStreamGateEnabledToolValidationHasSingleRecoveryOwner(t *testing.T) {
	t.Run("core budget zero blocks every retry", func(t *testing.T) {
		srv, fake := streamGateToolValidationServer(t, true, 0, invalidToolCallRun(), validToolCallRun())
		w := httptest.NewRecorder()
		srv.handleChatCompletions(w, httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(streamGateToolValidationBody)))

		if got := len(fake.reqsSnapshot()); got != 1 {
			t.Fatalf("provider dispatches: got %d, want 1 (Core budget 0 forbids recovery; a second dispatch means a legacy retry owner survives)", got)
		}
		if w.Code != http.StatusBadGateway {
			t.Fatalf("status: got %d, want 502; body=%s", w.Code, w.Body.String())
		}
		var resp errorResponse
		if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
			t.Fatalf("decode response: %v", err)
		}
		if resp.Error.Type != "tool_validation_error" {
			t.Fatalf("error type: got %q, want tool_validation_error", resp.Error.Type)
		}
	})

	t.Run("core budget one re-admits exactly once", func(t *testing.T) {
		srv, fake := streamGateToolValidationServer(t, true, 1, invalidToolCallRun(), validToolCallRun())
		w := httptest.NewRecorder()
		srv.handleChatCompletions(w, httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(streamGateToolValidationBody)))

		reqs := fake.reqsSnapshot()
		if len(reqs) != 2 {
			t.Fatalf("provider dispatches: got %d, want 2 (initial admission + exactly one Core re-admission)", len(reqs))
		}
		if got := reqs[1].Metadata[runtimeMetadataToolValidationAttempt]; got != "2" {
			t.Fatalf("recovery attempt metadata: got %q, want \"2\" (legacy provider-visible retry annotation must survive)", got)
		}
		if got := reqs[1].Metadata[runtimeMetadataToolValidationRetryOf]; got == "" {
			t.Fatalf("recovery retry_of metadata is missing: %v", reqs[1].Metadata)
		}
		if got := reqs[1].Metadata[runtimeMetadataToolValidationFailure]; got == "" {
			t.Fatalf("recovery failure metadata is missing: %v", reqs[1].Metadata)
		}
		if w.Code != http.StatusOK {
			t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
		}
	})

	t.Run("semantic filters disabled still use the core budget", func(t *testing.T) {
		// The semantic filter switch does not select the request runtime. The
		// always-on Core liveness owner therefore applies the same zero budget.
		srv, fake := streamGateToolValidationServer(t, false, 0, invalidToolCallRun(), validToolCallRun())
		w := httptest.NewRecorder()
		srv.handleChatCompletions(w, httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(streamGateToolValidationBody)))

		if got := len(fake.reqsSnapshot()); got != 1 {
			t.Fatalf("provider dispatches: got %d, want 1 (Core budget 0 forbids recovery)", got)
		}
		if w.Code != http.StatusBadGateway {
			t.Fatalf("status: got %d, want 502; body=%s", w.Code, w.Body.String())
		}
	})
}

// TestStreamGateEnabledToolValidationPreservesStructuredOutput verifies the
// runtime-enabled buffered renderers still emit the existing OpenAI response
// shape: structured tool_calls, the cleaned content, the tool_calls finish
// reason, and — for the buffered stream — the [DONE] sentinel.
func TestStreamGateEnabledToolValidationPreservesStructuredOutput(t *testing.T) {
	t.Run("non-stream json", func(t *testing.T) {
		srv, fake := streamGateToolValidationServer(t, true, 1, validToolCallRun())
		w := httptest.NewRecorder()
		srv.handleChatCompletions(w, httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(streamGateToolValidationBody)))

		if w.Code != http.StatusOK {
			t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
		}
		if got := len(fake.reqsSnapshot()); got != 1 {
			t.Fatalf("provider dispatches: got %d, want 1 (a passing attempt must not recover)", got)
		}
		var resp chatCompletionResponse
		if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
			t.Fatalf("decode response: %v", err)
		}
		if len(resp.Choices) == 0 {
			t.Fatal("empty choices")
		}
		choice := resp.Choices[0]
		if choice.FinishReason != "tool_calls" {
			t.Fatalf("finish reason: got %q, want tool_calls", choice.FinishReason)
		}
		if len(choice.Message.ToolCalls) != 1 {
			t.Fatalf("tool calls: got %d, want 1 structured call: %+v", len(choice.Message.ToolCalls), choice.Message)
		}
		if strings.Contains(choice.Message.Content, "<tool_call>") {
			t.Fatalf("raw tool_call block leaked into content: %q", choice.Message.Content)
		}
		if resp.Object != "chat.completion" {
			t.Fatalf("object: got %q, want chat.completion", resp.Object)
		}
	})

	t.Run("buffered stream", func(t *testing.T) {
		body := strings.TrimSuffix(strings.TrimSpace(streamGateToolValidationBody), "}") + `,"stream":true}`
		srv, fake := streamGateToolValidationServer(t, true, 1, validToolCallRun())
		w := httptest.NewRecorder()
		srv.handleChatCompletions(w, httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body)))

		if w.Code != http.StatusOK {
			t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
		}
		if got := len(fake.reqsSnapshot()); got != 1 {
			t.Fatalf("provider dispatches: got %d, want 1", got)
		}
		out := w.Body.String()
		if !strings.Contains(out, `"tool_calls"`) {
			t.Fatalf("buffered stream is missing structured tool_calls: %s", out)
		}
		if strings.Contains(out, "<tool_call>") {
			t.Fatalf("buffered stream leaked the raw tool_call block: %s", out)
		}
		if !strings.Contains(out, "data: [DONE]") {
			t.Fatalf("buffered stream is missing the [DONE] sentinel: %s", out)
		}
		if got := w.Header().Get("Content-Type"); got != "text/event-stream" {
			t.Fatalf("content type: got %q, want text/event-stream", got)
		}
	})

	t.Run("buffered stream exhaustion keeps the tool_validation_error envelope", func(t *testing.T) {
		body := strings.TrimSuffix(strings.TrimSpace(streamGateToolValidationBody), "}") + `,"stream":true}`
		srv, _ := streamGateToolValidationServer(t, true, 0, invalidToolCallRun())
		w := httptest.NewRecorder()
		srv.handleChatCompletions(w, httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body)))

		if w.Code != http.StatusOK {
			t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
		}
		out := w.Body.String()
		if !strings.Contains(out, "tool_validation_error") {
			t.Fatalf("buffered stream must report tool_validation_error: %s", out)
		}
		// The legacy envelope carries the validation reason (which names the
		// rejected tool), but the raw provider tool_call block must never reach
		// the caller as content.
		if strings.Contains(out, "<tool_call>") || strings.Contains(out, `"tool_calls"`) {
			t.Fatalf("the rejected tool call leaked into the response: %s", out)
		}
	})
}
