package openai

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"go.uber.org/zap"
	"go.uber.org/zap/zaptest/observer"

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

func TestAnthropicChatBridgeMixedContentToolsAndResponse(t *testing.T) {
	var fixture struct {
		Request          json.RawMessage `json:"request"`
		ProviderResponse json.RawMessage `json:"provider_response"`
	}
	if err := json.Unmarshal(mustReadAnthropicFixture(t, "chat_bridge_cases.json"), &fixture); err != nil {
		t.Fatal(err)
	}
	candidate := anthropicTestCandidate(t, "openai")
	candidate.ActualModel = "served-chat"
	fake := &providerFakeRunService{
		poolDispatchPath:      string(edgeservice.ProviderPoolPathTunnel),
		poolSelectedCandidate: candidate,
		tunnelFrames:          anthropicTunnelFrames(http.StatusOK, "application/json", fixture.ProviderResponse[:41], fixture.ProviderResponse[41:]),
	}
	srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
	srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "claude-route", Providers: map[string]string{"chat": "served-chat"}}})
	w := serveAnthropicRequest(srv, "/v1/messages", string(fixture.Request))

	if w.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
	}
	var response anthropicMessageResponse
	if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
		t.Fatal(err)
	}
	if response.ID != "chatcmpl_fixture" || response.Model != "claude-route" || response.StopReason == nil || *response.StopReason != "tool_use" {
		t.Fatalf("response envelope mismatch: %+v", response)
	}
	if response.Usage.InputTokens != 31 || response.Usage.OutputTokens != 7 || len(response.Content) != 2 {
		t.Fatalf("response content or usage mismatch: %+v", response)
	}
	if response.Content[0]["type"] != "text" || response.Content[0]["text"] != "Done." || response.Content[1]["type"] != "tool_use" || response.Content[1]["id"] != "call_2" {
		t.Fatalf("response block mapping mismatch: %+v", response.Content)
	}

	requests := fake.tunnelReqsSnapshot()
	bodies := fake.tunnelBodiesSnapshot()
	if len(requests) != 1 || len(bodies) != 1 || requests[0].Operation != string(config.OperationChatCompletions) {
		t.Fatalf("Chat tunnel evidence mismatch: requests=%+v bodies=%d", requests, len(bodies))
	}
	var chat map[string]any
	if err := json.Unmarshal(bodies[0], &chat); err != nil {
		t.Fatal(err)
	}
	if chat["model"] != "served-chat" || chat["max_completion_tokens"] != float64(256) || chat["max_tokens"] != nil || chat["stream"] != false {
		t.Fatalf("Chat request envelope mismatch: %+v", chat)
	}
	messages := anthropicAnySlice(t, chat["messages"])
	if len(messages) != 4 {
		t.Fatalf("Chat messages=%d, want 4: %+v", len(messages), messages)
	}
	system := anthropicAnyMap(t, messages[0])
	user := anthropicAnyMap(t, messages[1])
	assistant := anthropicAnyMap(t, messages[2])
	toolResult := anthropicAnyMap(t, messages[3])
	if system["role"] != "system" || system["content"] != "Use tools carefully." {
		t.Fatalf("system mapping mismatch: %+v", system)
	}
	userContent := anthropicAnySlice(t, user["content"])
	image := anthropicAnyMap(t, userContent[1])
	imageURL := anthropicAnyMap(t, image["image_url"])
	if user["role"] != "user" || len(userContent) != 2 || image["type"] != "image_url" || imageURL["url"] != "data:image/png;base64,aW1hZ2U=" {
		t.Fatalf("mixed user content mapping mismatch: %+v", user)
	}
	toolCalls := anthropicAnySlice(t, assistant["tool_calls"])
	call := anthropicAnyMap(t, toolCalls[0])
	function := anthropicAnyMap(t, call["function"])
	if assistant["role"] != "assistant" || call["id"] != "toolu_1" || function["name"] != "inspect" || function["arguments"] != `{"detail":"high"}` {
		t.Fatalf("assistant tool mapping mismatch: %+v", assistant)
	}
	if toolResult["role"] != "tool" || toolResult["tool_call_id"] != "toolu_1" || toolResult["content"] != "clear" {
		t.Fatalf("tool result mapping mismatch: %+v", toolResult)
	}
	tools := anthropicAnySlice(t, chat["tools"])
	tool := anthropicAnyMap(t, tools[0])
	toolFunction := anthropicAnyMap(t, tool["function"])
	choice := anthropicAnyMap(t, chat["tool_choice"])
	choiceFunction := anthropicAnyMap(t, choice["function"])
	if toolFunction["name"] != "inspect" || choiceFunction["name"] != "inspect" || chat["parallel_tool_calls"] != true {
		t.Fatalf("tool declaration or choice mismatch: tool=%+v choice=%+v", tool, choice)
	}
}

func TestAnthropicChatBridgeKeepsCompatibleMaxTokensField(t *testing.T) {
	profile, err := config.ResolveProtocolProfile("gemini", "", config.BuiltInProtocolProfileCatalog())
	if err != nil {
		t.Fatal(err)
	}
	body, _, err := prepareAnthropicChatBridge(
		[]byte(`{"model":"claude-route","max_tokens":64,"messages":[{"role":"user","content":"hello"}]}`),
		"served-chat", profile,
	)
	if err != nil {
		t.Fatal(err)
	}
	var chat map[string]any
	if err := json.Unmarshal(body, &chat); err != nil {
		t.Fatal(err)
	}
	if chat["max_tokens"] != float64(64) || chat["max_completion_tokens"] != nil {
		t.Fatalf("compatible Chat max token field mismatch: %+v", chat)
	}
}

func TestAnthropicChatBridgeNormalizesMidConversationSystemMessage(t *testing.T) {
	profile, err := config.ResolveProtocolProfile("openai", "", config.BuiltInProtocolProfileCatalog())
	if err != nil {
		t.Fatal(err)
	}
	body, _, err := prepareAnthropicChatBridge(
		[]byte(`{"model":"claude-route","max_tokens":64,"messages":[{"role":"user","content":"implement index.html"},{"role":"system","content":"Return the marker when complete."}]}`),
		"served-chat", profile,
	)
	if err != nil {
		t.Fatal(err)
	}
	var chat map[string]any
	if err := json.Unmarshal(body, &chat); err != nil {
		t.Fatal(err)
	}
	messages := anthropicAnySlice(t, chat["messages"])
	if len(messages) != 2 {
		t.Fatalf("messages=%+v", messages)
	}
	system := anthropicAnyMap(t, messages[1])
	if system["role"] != "system" || system["content"] != "Return the marker when complete." {
		t.Fatalf("mid-conversation system mapping=%+v", system)
	}
}

func TestAnthropicResponsesBridgeNormalizesMidConversationSystemMessage(t *testing.T) {
	candidate := anthropicTestCandidate(t, "openai")
	profile := candidate.ProtocolProfile.Clone()
	plan := providerOperationPlan{Operation: config.OperationResponses}
	body, _, err := prepareAnthropicResponsesBridge(
		[]byte(`{"model":"gpt-route","max_tokens":64,"messages":[{"role":"user","content":"implement index.html"},{"role":"system","content":"Return the marker when complete."}]}`),
		"served-responses", profile, plan,
	)
	if err != nil {
		t.Fatal(err)
	}
	var responses map[string]any
	if err := json.Unmarshal(body, &responses); err != nil {
		t.Fatal(err)
	}
	input := anthropicAnySlice(t, responses["input"])
	if len(input) != 2 {
		t.Fatalf("input=%+v", input)
	}
	system := anthropicAnyMap(t, input[1])
	if system["type"] != "message" || system["role"] != "system" {
		t.Fatalf("mid-conversation system mapping=%+v", system)
	}
}

func TestAnthropicMidConversationSystemPlacementIsFailClosed(t *testing.T) {
	for _, tc := range []struct {
		name string
		body string
	}{
		{name: "first", body: `{"model":"route","max_tokens":64,"messages":[{"role":"system","content":"system"},{"role":"user","content":"hello"}]}`},
		{name: "after assistant", body: `{"model":"route","max_tokens":64,"messages":[{"role":"user","content":"hello"},{"role":"assistant","content":"draft"},{"role":"system","content":"system"}]}`},
		{name: "before user", body: `{"model":"route","max_tokens":64,"messages":[{"role":"user","content":"hello"},{"role":"system","content":"system"},{"role":"user","content":"again"}]}`},
		{name: "non text", body: `{"model":"route","max_tokens":64,"messages":[{"role":"user","content":"hello"},{"role":"system","content":[{"type":"image","source":{"type":"url","url":"https://example.com/image.png"}}]}]}`},
	} {
		t.Run(tc.name, func(t *testing.T) {
			if _, err := decodeAnthropicMessageRequest([]byte(tc.body), true); err == nil {
				t.Fatal("invalid mid-conversation system message accepted")
			}
		})
	}
}

func TestAnthropicChatBridgeThinkingCapabilityAndResponse(t *testing.T) {
	candidate := anthropicTestCandidate(t, "openai")
	candidate.ActualModel = "served-chat"
	profile := candidate.ProtocolProfile.Clone()
	profile.Extensions = map[string]any{"thinking": true}
	candidate.ProtocolProfile = &profile
	providerResponse := []byte(`{"id":"chat_think","choices":[{"message":{"role":"assistant","content":"answer","reasoning_content":"hidden"},"finish_reason":"stop"}],"usage":{"prompt_tokens":9,"completion_tokens":4}}`)
	fake := &providerFakeRunService{
		poolDispatchPath:      string(edgeservice.ProviderPoolPathTunnel),
		poolSelectedCandidate: candidate,
		tunnelFrames:          anthropicTunnelFrames(http.StatusOK, "application/json", providerResponse),
	}
	srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
	srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "claude-route", Providers: map[string]string{"chat": "served-chat"}}})
	body := `{"model":"claude-route","max_tokens":64,"thinking":{"type":"enabled","budget_tokens":24},"messages":[{"role":"assistant","content":[{"type":"thinking","thinking":"prior"},{"type":"text","text":"draft"}]},{"role":"user","content":"continue"}]}`
	w := serveAnthropicRequest(srv, "/v1/messages", body)

	if w.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
	}
	var response anthropicMessageResponse
	if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
		t.Fatal(err)
	}
	if len(response.Content) != 2 || response.Content[0]["type"] != "thinking" || response.Content[0]["thinking"] != "hidden" || response.Content[1]["text"] != "answer" {
		t.Fatalf("thinking response mapping mismatch: %+v", response.Content)
	}
	var chat map[string]any
	if err := json.Unmarshal(fake.tunnelBodiesSnapshot()[0], &chat); err != nil {
		t.Fatal(err)
	}
	if chat["think"] != true || chat["include_reasoning"] != true || chat["thinking_token_budget"] != float64(24) {
		t.Fatalf("thinking request options mismatch: %+v", chat)
	}
	messages := anthropicAnySlice(t, chat["messages"])
	assistant := anthropicAnyMap(t, messages[0])
	if assistant["reasoning_content"] != "prior" {
		t.Fatalf("thinking input mapping mismatch: %+v", assistant)
	}
}

func TestAnthropicChatBridgeDropsUnsignedThinkingReplayForGenericProfile(t *testing.T) {
	candidate := anthropicTestCandidate(t, "openai")
	candidate.ActualModel = "served-chat"
	fake := &providerFakeRunService{
		poolDispatchPath:      string(edgeservice.ProviderPoolPathTunnel),
		poolSelectedCandidate: candidate,
		tunnelFrames: anthropicTunnelFrames(http.StatusOK, "application/json",
			[]byte(`{"id":"chat_replay","choices":[{"message":{"role":"assistant","content":"done"},"finish_reason":"stop"}],"usage":{"prompt_tokens":8,"completion_tokens":1}}`)),
	}
	srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
	srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "claude-route", Providers: map[string]string{"chat": "served-chat"}}})
	body := `{"model":"claude-route","max_tokens":64,"thinking":{"type":"adaptive"},"messages":[{"role":"assistant","content":[{"type":"thinking","thinking":"private prior reasoning","signature":""},{"type":"text","text":"I will inspect the file."}]},{"role":"user","content":"continue"}]}`
	w := serveAnthropicRequest(srv, "/v1/messages", body)

	if w.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
	}
	var chat map[string]any
	if err := json.Unmarshal(fake.tunnelBodiesSnapshot()[0], &chat); err != nil {
		t.Fatal(err)
	}
	messages := anthropicAnySlice(t, chat["messages"])
	assistant := anthropicAnyMap(t, messages[0])
	if _, ok := assistant["reasoning_content"]; ok {
		t.Fatalf("generic Chat replay leaked unsupported reasoning: %+v", assistant)
	}
	content := anthropicAnySlice(t, assistant["content"])
	if got := anthropicAnyMap(t, content[0])["text"]; got != "I will inspect the file." {
		t.Fatalf("visible assistant content changed: %+v", assistant)
	}
}

func TestAnthropicChatBridgeRejectsUnsupportedBeforeWire(t *testing.T) {
	for _, tc := range []struct {
		name      string
		body      string
		beta      string
		errorType string
	}{
		{name: "top k", body: `{"model":"claude-route","max_tokens":16,"top_k":4,"messages":[{"role":"user","content":"hello"}]}`},
		{name: "unknown block", body: `{"model":"claude-route","max_tokens":16,"messages":[{"role":"user","content":[{"type":"search_result","content":"unknown"}]}]}`},
		{name: "unknown field", body: `{"model":"claude-route","max_tokens":16,"vendor_extension":true,"messages":[{"role":"user","content":"hello"}]}`},
		{name: "context management scalar", body: `{"model":"claude-route","max_tokens":16,"context_management":"compact","messages":[{"role":"user","content":"hello"}]}`, beta: "context-management-2025-06-27"},
		{name: "context management array", body: `{"model":"claude-route","max_tokens":16,"context_management":[],"messages":[{"role":"user","content":"hello"}]}`, beta: "context-management-2025-06-27"},
		{name: "thinking capability", body: `{"model":"claude-route","max_tokens":16,"thinking":{"type":"enabled","budget_tokens":8},"messages":[{"role":"user","content":"hello"}]}`, errorType: "not_supported_error"},
		{name: "tool strict", body: `{"model":"claude-route","max_tokens":16,"messages":[{"role":"user","content":"hello"}],"tools":[{"name":"Read","input_schema":{"type":"object"},"strict":true}]}`, beta: "advanced-tool-use-2025-11-20"},
		{name: "tool eager input streaming", body: `{"model":"claude-route","max_tokens":16,"messages":[{"role":"user","content":"hello"}],"tools":[{"name":"Read","input_schema":{"type":"object"},"eager_input_streaming":true}]}`, beta: "advanced-tool-use-2025-11-20"},
		{name: "thinking display value", body: `{"model":"claude-route","max_tokens":16,"thinking":{"type":"adaptive","display":"raw"},"messages":[{"role":"user","content":"hello"}]}`},
		{name: "thinking display type", body: `{"model":"claude-route","max_tokens":16,"thinking":{"type":"adaptive","display":1},"messages":[{"role":"user","content":"hello"}]}`},
		{name: "unknown beta", body: `{"model":"claude-route","max_tokens":16,"messages":[{"role":"user","content":"hello"}]}`, beta: "unknown-beta-2099-01-01"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			candidate := anthropicTestCandidate(t, "openai")
			candidate.ActualModel = "served-chat"
			fake := &providerFakeRunService{
				poolDispatchPath:      string(edgeservice.ProviderPoolPathTunnel),
				poolSelectedCandidate: candidate,
				tunnelFrames:          anthropicTunnelFrames(http.StatusOK, "application/json", []byte(`{"ok":true}`)),
			}
			srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
			srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "claude-route", Providers: map[string]string{"chat": "served-chat"}}})
			req := newAnthropicRequest(http.MethodPost, "/v1/messages", tc.body)
			if tc.beta != "" {
				req.Header.Set(anthropicBetaHeader, tc.beta)
			}
			w := serveAnthropicHTTPRequest(srv, req)
			errorType := tc.errorType
			if errorType == "" {
				errorType = "invalid_request_error"
			}
			if w.Code != http.StatusBadRequest || !strings.Contains(w.Body.String(), `"type":"`+errorType+`"`) {
				t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
			}
			if got := len(fake.tunnelReqsSnapshot()); got != 0 {
				t.Fatalf("unsupported request reached provider wire: %d requests", got)
			}
		})
	}
}

func TestAnthropicContextManagementNullCompatibility(t *testing.T) {
	body := []byte(`{"model":"claude-route","max_tokens":16,"context_management":null,"messages":[{"role":"user","content":"hello"}]}`)
	req, err := decodeAnthropicMessageRequest(body, true)
	if err != nil {
		t.Fatalf("null context_management rejected: %v", err)
	}
	if !bytes.Equal(bytes.TrimSpace(req.ContextManagement), []byte("null")) {
		t.Fatalf("context_management changed: %s", req.ContextManagement)
	}
}

func TestAnthropicChatBridgeEffortMapping(t *testing.T) {
	for _, tc := range []struct {
		name       string
		body       string
		wantEffort string
		wantStatus int
		omitEffort bool
	}{
		{name: "omitted", body: `{"model":"claude-route","max_tokens":64,"messages":[{"role":"user","content":"hi"}]}`, wantStatus: http.StatusOK, omitEffort: true},
		{name: "low", body: `{"model":"claude-route","max_tokens":64,"output_config":{"effort":"low"},"messages":[{"role":"user","content":"hi"}]}`, wantEffort: "low", wantStatus: http.StatusOK},
		{name: "medium", body: `{"model":"claude-route","max_tokens":64,"output_config":{"effort":"medium"},"messages":[{"role":"user","content":"hi"}]}`, wantEffort: "medium", wantStatus: http.StatusOK},
		{name: "high", body: `{"model":"claude-route","max_tokens":64,"output_config":{"effort":"high"},"messages":[{"role":"user","content":"hi"}]}`, wantEffort: "high", wantStatus: http.StatusOK},
		{name: "xhigh", body: `{"model":"claude-route","max_tokens":64,"output_config":{"effort":"xhigh"},"messages":[{"role":"user","content":"hi"}]}`, wantEffort: "xhigh", wantStatus: http.StatusOK},
		{name: "max", body: `{"model":"claude-route","max_tokens":64,"output_config":{"effort":"max"},"messages":[{"role":"user","content":"hi"}]}`, wantEffort: "max", wantStatus: http.StatusOK},
		{name: "unknown value", body: `{"model":"claude-route","max_tokens":64,"output_config":{"effort":"ultra"},"messages":[{"role":"user","content":"hi"}]}`, wantStatus: http.StatusBadRequest},
		{name: "empty", body: `{"model":"claude-route","max_tokens":64,"output_config":{"effort":""},"messages":[{"role":"user","content":"hi"}]}`, wantStatus: http.StatusBadRequest},
		{name: "null", body: `{"model":"claude-route","max_tokens":64,"output_config":{"effort":null},"messages":[{"role":"user","content":"hi"}]}`, wantStatus: http.StatusBadRequest},
		{name: "non-string", body: `{"model":"claude-route","max_tokens":64,"output_config":{"effort":1},"messages":[{"role":"user","content":"hi"}]}`, wantStatus: http.StatusBadRequest},
		{name: "case-folded effort key", body: `{"model":"claude-route","max_tokens":64,"output_config":{"Effort":"ultra"},"messages":[{"role":"user","content":"hi"}]}`, wantStatus: http.StatusBadRequest},
		{name: "duplicate effort", body: `{"model":"claude-route","max_tokens":64,"output_config":{"effort":"ultra","effort":"max"},"messages":[{"role":"user","content":"hi"}]}`, wantStatus: http.StatusBadRequest},
		{name: "duplicate output config", body: `{"model":"claude-route","max_tokens":64,"output_config":{"effort":"ultra"},"output_config":{"effort":"max"},"messages":[{"role":"user","content":"hi"}]}`, wantStatus: http.StatusBadRequest},
	} {
		t.Run(tc.name, func(t *testing.T) {
			candidate := anthropicTestCandidate(t, "openai")
			candidate.ActualModel = "served-chat"
			fake := &providerFakeRunService{
				poolDispatchPath:      string(edgeservice.ProviderPoolPathTunnel),
				poolSelectedCandidate: candidate,
				tunnelFrames:          anthropicTunnelFrames(http.StatusOK, "application/json", []byte(`{"id":"chat_effort","choices":[{"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1}}`)),
			}
			srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
			srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "claude-route", Providers: map[string]string{"chat": "served-chat"}}})
			w := serveAnthropicRequest(srv, "/v1/messages", tc.body)

			if w.Code != tc.wantStatus {
				t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
			}
			if tc.wantStatus == http.StatusBadRequest {
				if !strings.Contains(w.Body.String(), `"type":"invalid_request_error"`) {
					t.Fatalf("expected invalid_request_error: %s", w.Body.String())
				}
				if got := len(fake.tunnelReqsSnapshot()); got != 0 {
					t.Fatalf("rejected request reached provider wire: %d requests", got)
				}
				return
			}
			var chat map[string]any
			if err := json.Unmarshal(fake.tunnelBodiesSnapshot()[0], &chat); err != nil {
				t.Fatal(err)
			}
			if tc.omitEffort {
				if _, ok := chat["reasoning_effort"]; ok {
					t.Fatalf("omitted effort set reasoning_effort: %+v", chat)
				}
				return
			}
			if got := chat["reasoning_effort"]; got != tc.wantEffort {
				t.Fatalf("reasoning_effort=%v, want %q", got, tc.wantEffort)
			}
		})
	}
}

func TestAnthropicChatBridgeEffortExactTokenPreservation(t *testing.T) {
	for _, effort := range []string{"low", "medium", "high", "xhigh", "max"} {
		t.Run(effort, func(t *testing.T) {
			candidate := anthropicTestCandidate(t, "openai")
			candidate.ActualModel = "served-chat"
			fake := &providerFakeRunService{
				poolDispatchPath:      string(edgeservice.ProviderPoolPathTunnel),
				poolSelectedCandidate: candidate,
				tunnelFrames:          anthropicTunnelFrames(http.StatusOK, "application/json", []byte(`{"id":"chat","choices":[{"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1}}`)),
			}
			srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
			srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "claude-route", Providers: map[string]string{"chat": "served-chat"}}})
			body := fmt.Sprintf(`{"model":"claude-route","max_tokens":64,"output_config":{"effort":%q},"messages":[{"role":"user","content":"hi"}]}`, effort)
			w := serveAnthropicRequest(srv, "/v1/messages", body)

			if w.Code != http.StatusOK {
				t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
			}
			var chat map[string]any
			if err := json.Unmarshal(fake.tunnelBodiesSnapshot()[0], &chat); err != nil {
				t.Fatal(err)
			}
			if got, ok := chat["reasoning_effort"].(string); !ok || got != effort {
				t.Fatalf("reasoning_effort=%v, want %q", chat["reasoning_effort"], effort)
			}
		})
	}
}

func TestAnthropicToolsAndEffortSelectResponsesWithLowerFallback(t *testing.T) {
	candidate := anthropicTestCandidate(t, "openai")
	candidate.ActualModel = "served-responses"
	responsesMapping := candidate.ProtocolProfile.Normalization.Effort[string(config.OperationResponses)]
	delete(responsesMapping.Levels, "max")
	candidate.ProtocolProfile.Normalization.Effort[string(config.OperationResponses)] = responsesMapping

	providerResponse := []byte(`{"id":"resp_effort","model":"served-responses","status":"completed","output":[{"type":"function_call","id":"fc_1","call_id":"call_1","name":"write_file","arguments":"{\"path\":\"index.html\"}"}],"usage":{"input_tokens":11,"output_tokens":3,"input_tokens_details":{"cached_tokens":2}}}`)
	fake := &providerFakeRunService{
		poolDispatchPath:      string(edgeservice.ProviderPoolPathTunnel),
		poolSelectedCandidate: candidate,
		tunnelFrames:          anthropicTunnelFrames(http.StatusOK, "application/json", providerResponse),
	}
	srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
	srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "gpt-route", Providers: map[string]string{"openai": "served-responses"}}})
	body := `{"model":"gpt-route","max_tokens":256,"output_config":{"effort":"max"},"tools":[{"name":"write_file","description":"write a file","input_schema":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}}],"messages":[{"role":"user","content":"create index.html"}]}`
	w := serveAnthropicRequest(srv, "/v1/messages", body)

	if w.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
	}
	requests := fake.tunnelReqsSnapshot()
	if len(requests) != 1 {
		t.Fatalf("tunnel requests=%d, want 1", len(requests))
	}
	if requests[0].Operation != string(config.OperationResponses) || requests[0].Path != "/v1/responses" {
		t.Fatalf("operation/path=%q/%q, want responses//v1/responses", requests[0].Operation, requests[0].Path)
	}
	var upstream map[string]any
	if err := json.Unmarshal(fake.tunnelBodiesSnapshot()[0], &upstream); err != nil {
		t.Fatal(err)
	}
	reasoning, _ := upstream["reasoning"].(map[string]any)
	if reasoning["effort"] != "xhigh" {
		t.Fatalf("reasoning effort=%v, want xhigh fallback", reasoning["effort"])
	}
	if _, hasChatMessages := upstream["messages"]; hasChatMessages {
		t.Fatalf("Responses bridge emitted Chat messages: %+v", upstream)
	}
	if _, hasResponsesInput := upstream["input"]; !hasResponsesInput {
		t.Fatalf("Responses bridge omitted input: %+v", upstream)
	}
	var response anthropicMessageResponse
	if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
		t.Fatal(err)
	}
	if len(response.Content) != 1 || response.Content[0]["type"] != "tool_use" || response.Content[0]["name"] != "write_file" {
		t.Fatalf("Anthropic tool response=%+v", response.Content)
	}
	if response.StopReason == nil || *response.StopReason != "tool_use" {
		t.Fatalf("stop_reason=%v, want tool_use", response.StopReason)
	}
}

func TestAnthropicStreamingToolsAndEffortUseResponsesBridge(t *testing.T) {
	candidate := anthropicTestCandidate(t, "openai")
	candidate.ActualModel = "served-responses"
	providerResponse := []byte("event: response.completed\n" +
		`data: {"type":"response.completed","response":{"id":"resp_stream","model":"served-responses","status":"completed","output":[{"type":"function_call","id":"fc_1","call_id":"call_1","name":"write_file","arguments":"{\"path\":\"index.html\"}"}],"usage":{"input_tokens":11,"output_tokens":3}}}` + "\n\n" +
		"data: [DONE]\n\n")
	fake := &providerFakeRunService{
		poolDispatchPath:      string(edgeservice.ProviderPoolPathTunnel),
		poolSelectedCandidate: candidate,
		tunnelFrames:          anthropicTunnelFrames(http.StatusOK, "text/event-stream", providerResponse),
	}
	srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
	srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "gpt-route", Providers: map[string]string{"openai": "served-responses"}}})
	body := `{"model":"gpt-route","max_tokens":256,"stream":true,"output_config":{"effort":"high"},"tools":[{"name":"write_file","input_schema":{"type":"object"}}],"messages":[{"role":"user","content":"create index.html"}]}`
	w := serveAnthropicRequest(srv, "/v1/messages", body)

	if w.Code != http.StatusOK || w.Header().Get("Content-Type") != "text/event-stream" {
		t.Fatalf("status=%d content-type=%q body=%s", w.Code, w.Header().Get("Content-Type"), w.Body.String())
	}
	requests := fake.tunnelReqsSnapshot()
	if len(requests) != 1 || requests[0].Operation != string(config.OperationResponses) || !requests[0].Stream {
		t.Fatalf("Responses stream dispatch mismatch: %+v", requests)
	}
	output := w.Body.String()
	for _, want := range []string{"event: message_start", `"type":"tool_use"`, `"name":"write_file"`, "event: message_stop"} {
		if !strings.Contains(output, want) {
			t.Fatalf("Anthropic stream missing %q: %s", want, output)
		}
	}
	if strings.Contains(output, "response.completed") || strings.Count(output, "event: message_stop") != 1 {
		t.Fatalf("provider event leaked or terminal count mismatched: %s", output)
	}
}

func TestAnthropicChatBridgeEffortRejectsInvalidValue(t *testing.T) {
	for _, effort := range []string{"HIGH", "XHigh", "maxx", "xhighx", "h"} {
		t.Run(effort, func(t *testing.T) {
			candidate := anthropicTestCandidate(t, "openai")
			candidate.ActualModel = "served-chat"
			fake := &providerFakeRunService{
				poolDispatchPath:      string(edgeservice.ProviderPoolPathTunnel),
				poolSelectedCandidate: candidate,
				tunnelFrames:          anthropicTunnelFrames(http.StatusOK, "application/json", []byte(`{"ok":true}`)),
			}
			srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
			srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "claude-route", Providers: map[string]string{"chat": "served-chat"}}})
			body := fmt.Sprintf(`{"model":"claude-route","max_tokens":64,"output_config":{"effort":%q},"messages":[{"role":"user","content":"hi"}]}`, effort)
			w := serveAnthropicRequest(srv, "/v1/messages", body)

			if w.Code != http.StatusBadRequest {
				t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
			}
			if !strings.Contains(w.Body.String(), `"type":"invalid_request_error"`) {
				t.Fatalf("expected invalid_request_error: %s", w.Body.String())
			}
			if !strings.Contains(w.Body.String(), "xhigh") || !strings.Contains(w.Body.String(), "max") {
				t.Fatalf("error should list xhigh and max as allowed: %s", w.Body.String())
			}
			if got := len(fake.tunnelReqsSnapshot()); got != 0 {
				t.Fatalf("rejected request reached provider wire: %d requests", got)
			}
		})
	}
}

func TestAnthropicChatBridgeClaudeCodeRequest(t *testing.T) {
	candidate := anthropicTestCandidate(t, "gemini")
	candidate.ActualModel = "gemini-3.6-flash"
	providerResponse := []byte(`{"id":"chat_claude_code","choices":[{"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}],"usage":{"prompt_tokens":11,"completion_tokens":2}}`)
	fake := &providerFakeRunService{
		poolDispatchPath:      string(edgeservice.ProviderPoolPathTunnel),
		poolSelectedCandidate: candidate,
		tunnelFrames:          anthropicTunnelFrames(http.StatusOK, "application/json", providerResponse),
	}
	srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
	srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "gemini-route", Providers: map[string]string{"gemini": "gemini-3.6-flash"}}})
	body := `{
		"model":"gemini-route",
		"max_tokens":1024,
		"system":[
			{"type":"text","text":"base"},
			{"type":"text","text":"cached","cache_control":{"type":"ephemeral"}}
		],
		"messages":[{"role":"user","content":[
			{"type":"text","text":"hello"},
			{"type":"text","text":"cached prompt","cache_control":{"type":"ephemeral"}}
		]}],
		"thinking":{"type":"adaptive"},
		"output_config":{"effort":"high","format":{"type":"json_schema","schema":{"type":"object","properties":{"title":{"type":"string"}},"required":["title"],"additionalProperties":false}}},
		"metadata":{"user_id":"claude-code"},
		"context_management":{"edits":[{"type":"clear_tool_uses_20250919","trigger":{"type":"input_tokens","value":50000}}]},
		"tools":[{"name":"Read","description":"Read a file","input_schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"file_path":{"type":"string"}},"required":["file_path"],"additionalProperties":false},"defer_loading":true}]
	}`
	req := newAnthropicRequest(http.MethodPost, "/v1/messages", body)
	req.Header.Set(anthropicBetaHeader, strings.Join([]string{
		"advanced-tool-use-2025-11-20",
		"advisor-tool-2026-03-01",
		"claude-code-20250219",
		"context-management-2025-06-27",
		"interleaved-thinking-2025-05-14",
		"mid-conversation-system-2026-04-07",
		"prompt-caching-scope-2026-01-05",
		"redact-thinking-2026-02-12",
		"effort-2025-11-24",
		"structured-outputs-2025-12-15",
	}, ","))
	w := serveAnthropicHTTPRequest(srv, req)
	if w.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
	}
	requests := fake.tunnelReqsSnapshot()
	if len(requests) != 1 || requests[0].Headers[anthropicBetaHeader] != "" {
		t.Fatalf("Chat bridge forwarded Anthropic compatibility beta: %+v", requests)
	}

	var chat map[string]any
	if err := json.Unmarshal(fake.tunnelBodiesSnapshot()[0], &chat); err != nil {
		t.Fatal(err)
	}
	if chat["model"] != "gemini-3.6-flash" || chat["reasoning_effort"] != "high" {
		t.Fatalf("Claude Code model or effort mapping mismatch: %+v", chat)
	}
	for _, key := range []string{"think", "include_reasoning", "thinking_token_budget", "output_config", "context_management"} {
		if _, ok := chat[key]; ok {
			t.Fatalf("adaptive request leaked unsupported field %q: %+v", key, chat)
		}
	}
	if _, ok := chat["metadata"]; ok {
		t.Fatalf("Anthropic metadata must not be forwarded to Chat providers: %+v", chat)
	}
	responseFormat := anthropicAnyMap(t, chat["response_format"])
	jsonSchema := anthropicAnyMap(t, responseFormat["json_schema"])
	schema := anthropicAnyMap(t, jsonSchema["schema"])
	if responseFormat["type"] != "json_schema" || jsonSchema["name"] != "response" || jsonSchema["strict"] != true || schema["type"] != "object" {
		t.Fatalf("structured output mapping mismatch: %+v", responseFormat)
	}
	messages := anthropicAnySlice(t, chat["messages"])
	if len(messages) != 2 || anthropicAnyMap(t, messages[0])["content"] != "base\ncached" {
		t.Fatalf("cache-controlled system mapping mismatch: %+v", messages)
	}
	tools := anthropicAnySlice(t, chat["tools"])
	tool := anthropicAnyMap(t, tools[0])
	if _, ok := tool["defer_loading"]; ok {
		t.Fatalf("Claude Code defer_loading leaked into normalized Chat tool: %+v", tool)
	}
	function := anthropicAnyMap(t, tool["function"])
	if _, ok := function["defer_loading"]; ok {
		t.Fatalf("Claude Code defer_loading leaked into normalized Chat function: %+v", function)
	}
	if function["name"] != "Read" {
		t.Fatalf("Claude Code tool mapping mismatch: %+v", tools)
	}
}

func TestAnthropicChatBridgeThinkingDisplayCompatibility(t *testing.T) {
	for _, display := range []string{"omitted", "summarized"} {
		t.Run(display, func(t *testing.T) {
			candidate := anthropicTestCandidate(t, "openai")
			candidate.ActualModel = "served-chat"
			fake := &providerFakeRunService{
				poolDispatchPath:      string(edgeservice.ProviderPoolPathTunnel),
				poolSelectedCandidate: candidate,
				tunnelFrames: anthropicTunnelFrames(http.StatusOK, "application/json",
					[]byte(`{"id":"chat_display","choices":[{"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":1}}`)),
			}
			srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
			srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "claude-route", Providers: map[string]string{"chat": "served-chat"}}})
			body := fmt.Sprintf(`{"model":"claude-route","max_tokens":16,"thinking":{"type":"adaptive","display":%q},"messages":[{"role":"user","content":"hello"}]}`, display)
			w := serveAnthropicRequest(srv, "/v1/messages", body)

			if w.Code != http.StatusOK {
				t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
			}
			var chat map[string]any
			if err := json.Unmarshal(fake.tunnelBodiesSnapshot()[0], &chat); err != nil {
				t.Fatal(err)
			}
			for _, key := range []string{"thinking", "display"} {
				if _, ok := chat[key]; ok {
					t.Fatalf("Claude thinking display compatibility leaked field %q: %+v", key, chat)
				}
			}
		})
	}
}

func TestAnthropicChatBridgeGeminiThoughtSignatureRoundTrip(t *testing.T) {
	providerResponse := []byte(`{
		"id":"chat_signature",
		"choices":[{"message":{"role":"assistant","tool_calls":[{
			"id":"call_1",
			"type":"function",
			"function":{"name":"Bash","arguments":"{\"command\":\"printf 5 > answer.txt\"}"},
			"extra_content":{"google":{"thought_signature":"signature-1"}}
		}]},"finish_reason":"tool_calls"}],
		"usage":{"prompt_tokens":9,"completion_tokens":4}
	}`)
	response, err := convertChatResponseToAnthropic(providerResponse, "gemini-route")
	if err != nil {
		t.Fatal(err)
	}
	if len(response.Content) != 1 {
		t.Fatalf("content blocks=%d, want 1", len(response.Content))
	}
	encodedID, ok := response.Content[0]["id"].(string)
	if !ok || encodedID == "call_1" {
		t.Fatalf("thought signature was not encoded in tool_use id: %+v", response.Content[0])
	}
	toolID, signature, encoded := decodeAnthropicBridgeToolID(encodedID)
	if !encoded || toolID != "call_1" || signature != "signature-1" {
		t.Fatalf("encoded tool id mismatch: id=%q signature=%q encoded=%v", toolID, signature, encoded)
	}

	requestBody := fmt.Sprintf(`{
		"model":"gemini-route",
		"max_tokens":1024,
		"messages":[
			{"role":"assistant","content":[{"type":"tool_use","id":%q,"name":"Bash","input":{"command":"printf 5 > answer.txt"}}]},
			{"role":"user","content":[{"type":"tool_result","tool_use_id":%q,"content":"done","is_error":false,"cache_control":{"type":"ephemeral"}}]}
		]
	}`, encodedID, encodedID)
	profile, err := config.ResolveProtocolProfile("gemini", "", config.BuiltInProtocolProfileCatalog())
	if err != nil {
		t.Fatal(err)
	}
	bridged, _, err := prepareAnthropicChatBridge([]byte(requestBody), "gemini-3.6-flash", profile)
	if err != nil {
		t.Fatal(err)
	}
	var chat map[string]any
	if err := json.Unmarshal(bridged, &chat); err != nil {
		t.Fatal(err)
	}
	messages := anthropicAnySlice(t, chat["messages"])
	assistant := anthropicAnyMap(t, messages[0])
	toolCall := anthropicAnyMap(t, anthropicAnySlice(t, assistant["tool_calls"])[0])
	extra := anthropicAnyMap(t, anthropicAnyMap(t, toolCall["extra_content"])["google"])
	toolResult := anthropicAnyMap(t, messages[1])
	if toolCall["id"] != "call_1" || extra["thought_signature"] != "signature-1" || toolResult["tool_call_id"] != "call_1" {
		t.Fatalf("Gemini thought signature round trip mismatch: assistant=%+v tool_result=%+v", assistant, toolResult)
	}
}

func TestAnthropicChatBridgeProviderError(t *testing.T) {
	for _, tc := range []struct {
		name string
		body string
	}{
		{name: "object", body: `{"error":{"type":"rate_limit_error","message":"slow down","code":429}}`},
		{name: "Google array", body: `[{"error":{"type":"rate_limit_error","message":"slow down","code":429}}]`},
	} {
		t.Run(tc.name, func(t *testing.T) {
			candidate := anthropicTestCandidate(t, "openai")
			candidate.ActualModel = "served-chat"
			providerError := []byte(tc.body)
			fake := &providerFakeRunService{
				poolDispatchPath:      string(edgeservice.ProviderPoolPathTunnel),
				poolSelectedCandidate: candidate,
				tunnelFrames:          anthropicTunnelFrames(http.StatusTooManyRequests, "application/json", providerError[:13], providerError[13:]),
			}
			srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
			srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "claude-route", Providers: map[string]string{"chat": "served-chat"}}})
			w := serveAnthropicRequest(srv, "/v1/messages", `{"model":"claude-route","max_tokens":16,"messages":[{"role":"user","content":"hello"}]}`)
			if w.Code != http.StatusTooManyRequests || !strings.Contains(w.Body.String(), `"type":"rate_limit_error"`) || !strings.Contains(w.Body.String(), `"message":"slow down"`) {
				t.Fatalf("provider error mapping mismatch: status=%d body=%s", w.Code, w.Body.String())
			}
		})
	}
}

func TestAnthropicChatBridgeProviderRejectionObservationIsClassificationOnly(t *testing.T) {
	core, observed := observer.New(zap.InfoLevel)
	secret := "secret-provider-message-must-not-be-logged"
	candidate := anthropicTestCandidate(t, "openai")
	candidate.ActualModel = "served-chat"
	fake := &providerFakeRunService{
		poolDispatchPath:      string(edgeservice.ProviderPoolPathTunnel),
		poolSelectedCandidate: candidate,
		tunnelFrames: anthropicTunnelFrames(
			http.StatusBadRequest,
			"application/json",
			[]byte(`{"error":{"type":"invalid_request_error","message":"`+secret+`"}}`),
		),
	}
	srv := NewServer(config.EdgeOpenAIConf{}, fake, zap.New(core))
	srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "claude-route", Providers: map[string]string{"chat": "served-chat"}}})
	w := serveAnthropicRequest(srv, "/v1/messages", `{"model":"claude-route","max_tokens":16,"messages":[{"role":"user","content":"hello"}]}`)
	if w.Code != http.StatusBadRequest {
		t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
	}
	entries := observed.FilterMessage(anthropicChatBridgeRejectionLogMessage).All()
	if len(entries) != 1 {
		t.Fatalf("rejection observations=%d logs=%+v", len(entries), observed.All())
	}
	context := entries[0].ContextMap()
	if len(context) != 4 || context["surface"] != "messages" || context["bridge"] != "chat" || context["rejection_class"] != "provider_http" || context["http_status"] != int64(http.StatusBadRequest) {
		t.Fatalf("unsafe or incomplete observation: %+v", context)
	}
	if strings.Contains(fmt.Sprint(context), secret) {
		t.Fatalf("secret leaked to observation: %+v", context)
	}
}

func TestAnthropicChatBridgeStreamFragmentationOrderAndTerminal(t *testing.T) {
	fixture := mustReadAnthropicFixture(t, "chat_bridge_stream.sse")
	parts := bytes.SplitN(fixture, []byte("---ANTHROPIC-OUTPUT---\n"), 2)
	if len(parts) != 2 {
		t.Fatal("chat bridge stream fixture is missing the Anthropic output section")
	}
	expectedParts := bytes.SplitN(parts[1], []byte("---END-ANTHROPIC-OUTPUT---"), 2)
	if len(expectedParts) != 2 {
		t.Fatal("chat bridge stream fixture is missing the Anthropic output terminator")
	}
	streamBody, expectedOutput := parts[0], expectedParts[0]
	frames := make(chan *iop.ProviderTunnelFrame, 9)
	frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START, StatusCode: http.StatusOK, Headers: map[string]string{"Content-Type": "text/event-stream"}}
	for index, fragment := range splitAnthropicFixture(streamBody, 5, 67, 139, 251, 409, len(streamBody)-3) {
		frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Sequence: int64(index + 1), Body: fragment}
	}
	frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true}
	close(frames)

	candidate := anthropicTestCandidate(t, "openai")
	candidate.ActualModel = "served-chat"
	fake := &providerFakeRunService{
		poolDispatchPath:      string(edgeservice.ProviderPoolPathTunnel),
		poolSelectedCandidate: candidate,
		tunnelFrames:          frames,
	}
	srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
	srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "claude-route", Providers: map[string]string{"chat": "served-chat"}}})
	w := serveAnthropicRequest(srv, "/v1/messages", `{"model":"claude-route","max_tokens":64,"stream":true,"messages":[{"role":"user","content":"hello"}]}`)

	if w.Code != http.StatusOK || w.Header().Get("Content-Type") != "text/event-stream" {
		t.Fatalf("stream response mismatch: status=%d headers=%v body=%s", w.Code, w.Header(), w.Body.String())
	}
	if !bytes.Equal(w.Body.Bytes(), expectedOutput) {
		t.Fatalf("stream golden mismatch:\n got=%q\nwant=%q", w.Body.Bytes(), expectedOutput)
	}
	events := anthropicSSEEventNames(w.Body.Bytes())
	wantEvents := []string{
		"message_start",
		"content_block_start", "content_block_delta", "content_block_stop",
		"content_block_start", "content_block_delta", "content_block_stop",
		"content_block_start", "content_block_delta", "content_block_stop",
		"message_delta", "message_stop",
	}
	if strings.Join(events, ",") != strings.Join(wantEvents, ",") {
		t.Fatalf("stream event order mismatch:\n got=%v\nwant=%v\nbody=%s", events, wantEvents, w.Body.String())
	}
	output := w.Body.String()
	for _, fragment := range []string{`"thinking":"checking "`, `"type":"thinking_delta"`, `"text":"hello "`, `"type":"text_delta"`, `"id":"call_1"`, `"name":"lookup"`, `"partial_json":"{\"q\":\"iop\"}"`, `"stop_reason":"tool_use"`, `"input_tokens":19`, `"output_tokens":5`} {
		if !strings.Contains(output, fragment) {
			t.Fatalf("stream output missing %q: %s", fragment, output)
		}
	}
	if strings.Count(output, "event: message_stop") != 1 {
		t.Fatalf("message_stop count mismatch: %s", output)
	}
	var chat map[string]any
	if err := json.Unmarshal(fake.tunnelBodiesSnapshot()[0], &chat); err != nil {
		t.Fatal(err)
	}
	streamOptions := anthropicAnyMap(t, chat["stream_options"])
	if chat["stream"] != true || streamOptions["include_usage"] != true {
		t.Fatalf("stream request options mismatch: %+v", chat)
	}
}

func TestAnthropicChatBridgeStreamStopsAtTerminalWithinFrame(t *testing.T) {
	for _, tc := range []struct {
		name       string
		body       string
		wantEvents []string
		wantFinal  string
	}{
		{
			name: "DONE terminal",
			body: "data: {\"id\":\"chat_1\",\"choices\":[{\"delta\":{\"content\":\"first \"}}]}\n\n" +
				"data: [DONE]\n\n" +
				"data: {\"id\":\"chat_1\",\"choices\":[{\"delta\":{\"content\":\"late\"}}]}\n\n",
			wantEvents: []string{
				"message_start",
				"content_block_start", "content_block_delta", "content_block_stop",
				"message_delta", "message_stop",
			},
			wantFinal: "message_stop",
		},
		{
			name: "Upstream error terminal",
			body: "data: {\"id\":\"chat_1\",\"choices\":[{\"delta\":{\"content\":\"first \"}}]}\n\n" +
				"data: {\"error\":{\"type\":\"server_error\",\"message\":\"boom\"}}\n\n" +
				"data: {\"id\":\"chat_1\",\"choices\":[{\"delta\":{\"content\":\"late\"}}]}\n\n",
			wantEvents: []string{
				"message_start",
				"content_block_start", "content_block_delta",
				"error",
			},
			wantFinal: "error",
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			frames := make(chan *iop.ProviderTunnelFrame, 3)
			frames <- &iop.ProviderTunnelFrame{
				Kind:       iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START,
				StatusCode: http.StatusOK,
				Headers:    map[string]string{"Content-Type": "text/event-stream"},
			}
			frames <- &iop.ProviderTunnelFrame{
				Kind:     iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY,
				Sequence: 1,
				Body:     []byte(tc.body),
			}
			frames <- &iop.ProviderTunnelFrame{
				Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END,
				End:  true,
			}
			close(frames)

			candidate := anthropicTestCandidate(t, "openai")
			candidate.ActualModel = "served-chat"
			fake := &providerFakeRunService{
				poolDispatchPath:      string(edgeservice.ProviderPoolPathTunnel),
				poolSelectedCandidate: candidate,
				tunnelFrames:          frames,
			}
			srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
			srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "claude-route", Providers: map[string]string{"chat": "served-chat"}}})
			w := serveAnthropicRequest(srv, "/v1/messages", `{"model":"claude-route","max_tokens":64,"stream":true,"messages":[{"role":"user","content":"hello"}]}`)

			if w.Code != http.StatusOK || w.Header().Get("Content-Type") != "text/event-stream" {
				t.Fatalf("stream response mismatch: status=%d headers=%v body=%s", w.Code, w.Header(), w.Body.String())
			}

			output := w.Body.String()
			if strings.Contains(output, "late") {
				t.Fatalf("stream output contained late content after terminal: %s", output)
			}

			events := anthropicSSEEventNames(w.Body.Bytes())
			if strings.Join(events, ",") != strings.Join(tc.wantEvents, ",") {
				t.Fatalf("stream event order mismatch:\n got=%v\nwant=%v\nbody=%s", events, tc.wantEvents, output)
			}

			if len(events) == 0 || events[len(events)-1] != tc.wantFinal {
				t.Fatalf("final event mismatch: got=%v wantFinal=%s", events, tc.wantFinal)
			}

			if count := strings.Count(output, "event: "+tc.wantFinal); count != 1 {
				t.Fatalf("terminal event count mismatch for %s: count=%d body=%s", tc.wantFinal, count, output)
			}
		})
	}
}

func TestAnthropicChatBridgeStreamEncodesGeminiThoughtSignature(t *testing.T) {
	w := httptest.NewRecorder()
	stream := newAnthropicBridgeStream(w, "gemini-route")
	payload := `data: {"id":"chat_signature","choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_1","function":{"name":"Bash","arguments":"{\"command\":\"printf 5 > answer.txt\"}"},"extra_content":{"google":{"thought_signature":"signature-1"}}}]},"finish_reason":"tool_calls"}]}` + "\n\n" +
		"data: [DONE]\n\n"
	if err := stream.Feed([]byte(payload)); err != nil {
		t.Fatal(err)
	}

	var encodedID string
	for _, event := range bytes.Split(w.Body.Bytes(), []byte("\n\n")) {
		var data []byte
		for _, line := range bytes.Split(event, []byte("\n")) {
			if bytes.HasPrefix(line, []byte("data: ")) {
				data = bytes.TrimPrefix(line, []byte("data: "))
			}
		}
		if len(data) == 0 {
			continue
		}
		var item struct {
			Type         string `json:"type"`
			ContentBlock struct {
				Type string `json:"type"`
				ID   string `json:"id"`
			} `json:"content_block"`
		}
		if json.Unmarshal(data, &item) == nil && item.Type == "content_block_start" && item.ContentBlock.Type == "tool_use" {
			encodedID = item.ContentBlock.ID
		}
	}
	toolID, signature, encoded := decodeAnthropicBridgeToolID(encodedID)
	if !encoded || toolID != "call_1" || signature != "signature-1" {
		t.Fatalf("stream signature encoding mismatch: encoded_id=%q id=%q signature=%q encoded=%v body=%s", encodedID, toolID, signature, encoded, w.Body.String())
	}
}

func newAnthropicRequest(method, path, body string) *http.Request {
	req := httptest.NewRequest(method, path, bytes.NewBufferString(body))
	req.Header.Set(anthropicVersionHeader, anthropicSupportedVersion)
	return req
}

func serveAnthropicHTTPRequest(srv *Server, req *http.Request) *httptest.ResponseRecorder {
	w := httptest.NewRecorder()
	srv.routes().ServeHTTP(w, req)
	return w
}

func anthropicAnySlice(t *testing.T, value any) []any {
	t.Helper()
	items, ok := value.([]any)
	if !ok {
		t.Fatalf("value is %T, want []any: %+v", value, value)
	}
	return items
}

func anthropicAnyMap(t *testing.T, value any) map[string]any {
	t.Helper()
	item, ok := value.(map[string]any)
	if !ok {
		t.Fatalf("value is %T, want map[string]any: %+v", value, value)
	}
	return item
}

func anthropicSSEEventNames(body []byte) []string {
	var names []string
	for _, event := range bytes.Split(bytes.ReplaceAll(body, []byte("\r\n"), []byte("\n")), []byte("\n\n")) {
		for _, line := range bytes.Split(event, []byte("\n")) {
			if bytes.HasPrefix(line, []byte("event: ")) {
				names = append(names, string(bytes.TrimPrefix(line, []byte("event: "))))
				break
			}
		}
	}
	return names
}
