package openai

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

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

func TestGeminiChatProviderThoughtSignatureRoundTrip(t *testing.T) {
	response := []byte(`{"id":"chat-1","model":"served","choices":[{"message":{"role":"assistant","content":null,"tool_calls":[{"id":"call-1","type":"function","function":{"name":"glob","arguments":"{\"pattern\":\"*\"}"},"extra_content":{"google":{"thought_signature":"opaque-signature"}}}]},"finish_reason":"tool_calls"}]}`)
	rewritten := rewriteProviderJSONResponse(response, "public-gemini", config.ProtocolToolCallWireGeminiChat)

	var decoded map[string]any
	if err := json.Unmarshal(rewritten, &decoded); err != nil {
		t.Fatal(err)
	}
	choice := anySlice(decoded["choices"])[0].(map[string]any)
	message := choice["message"].(map[string]any)
	call := anySlice(message["tool_calls"])[0].(map[string]any)
	encodedID := call["id"].(string)
	if !strings.HasPrefix(encodedID, geminiThoughtSignatureToolIDPrefix) {
		t.Fatalf("thought signature was not encoded in tool id: %+v", call)
	}
	if _, exists := call["extra_content"]; exists {
		t.Fatalf("Gemini extension leaked after normalization: %+v", call)
	}
	if decoded["model"] != "public-gemini" {
		t.Fatalf("model echo was not preserved through normalization: %+v", decoded)
	}

	request, err := normalizeGeminiChatProviderRequest([]byte(`{"model":"served","messages":[{"role":"assistant","content":null,"tool_calls":[{"id":"` + encodedID + `","type":"function","function":{"name":"glob","arguments":"{\"pattern\":\"*\"}"}}]},{"role":"tool","tool_call_id":"` + encodedID + `","content":"[]"}]}`))
	if err != nil {
		t.Fatal(err)
	}
	var replay map[string]any
	if err := json.Unmarshal(request, &replay); err != nil {
		t.Fatal(err)
	}
	messages := anySlice(replay["messages"])
	replayedCall := anySlice(messages[0].(map[string]any)["tool_calls"])[0].(map[string]any)
	if replayedCall["id"] != "call-1" || messages[1].(map[string]any)["tool_call_id"] != "call-1" {
		t.Fatalf("original tool ids were not restored: %+v", messages)
	}
	extra := replayedCall["extra_content"].(map[string]any)
	google := extra["google"].(map[string]any)
	if google["thought_signature"] != "opaque-signature" {
		t.Fatalf("thought signature was not restored: %+v", replayedCall)
	}
}

func TestProviderThoughtSignatureNormalizationIsGeminiProfileOnly(t *testing.T) {
	response := []byte(`{"model":"served","choices":[{"message":{"tool_calls":[{"id":"call-1","extra_content":{"google":{"thought_signature":"opaque"}}}]}}]}`)
	if got := rewriteProviderJSONResponse(response, "public", ""); !strings.Contains(string(got), `"id":"call-1"`) || !strings.Contains(string(got), `"thought_signature":"opaque"`) {
		t.Fatalf("non-Gemini response was normalized: %s", got)
	}

	profile, err := config.ResolveProtocolProfile("openai", "", config.BuiltInProtocolProfileCatalog())
	if err != nil {
		t.Fatal(err)
	}
	baseBody := []byte(`{"model":"served","messages":[{"role":"user","content":"hi"}]}`)
	tunnel := edgeservice.SubmitProviderTunnelRequest{BuildBody: func(string) ([]byte, error) { return baseBody, nil }}
	prepared, err := prepareProviderChatToolCallNormalization(tunnel, edgeservice.ProviderPoolCandidate{ProtocolProfile: &profile})
	if err != nil {
		t.Fatal(err)
	}
	got, err := prepared.BuildBody("served")
	if err != nil || string(got) != string(baseBody) {
		t.Fatalf("OpenAI profile body changed: %s err=%v", got, err)
	}
}

func TestProviderChatTokenLimitNormalizationUsesSelectedProfile(t *testing.T) {
	tests := []struct {
		name       string
		profileID  string
		body       string
		wantField  string
		wantAbsent string
		want       float64
	}{
		{name: "generic max tokens to OpenAI completion field", profileID: "openai", body: `{"model":"served","max_tokens":32000,"future":{"keep":true}}`, wantField: "max_completion_tokens", wantAbsent: "max_tokens", want: 32000},
		{name: "OpenAI native field wins", profileID: "openai", body: `{"model":"served","max_tokens":8,"max_completion_tokens":16}`, wantField: "max_completion_tokens", wantAbsent: "max_tokens", want: 16},
		{name: "completion field to Gemini legacy field", profileID: "gemini", body: `{"model":"served","max_completion_tokens":2048}`, wantField: "max_tokens", wantAbsent: "max_completion_tokens", want: 2048},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			profile, err := config.ResolveProtocolProfile(tc.profileID, "", config.BuiltInProtocolProfileCatalog())
			if err != nil {
				t.Fatal(err)
			}
			tunnel := edgeservice.SubmitProviderTunnelRequest{
				Operation: string(config.OperationChatCompletions),
				BuildBody: func(string) ([]byte, error) { return []byte(tc.body), nil },
			}
			prepared, err := prepareProviderChatRequestNormalization(tunnel, edgeservice.ProviderPoolCandidate{ProtocolProfile: &profile})
			if err != nil {
				t.Fatal(err)
			}
			body, err := prepared.BuildBody("served")
			if err != nil {
				t.Fatal(err)
			}
			var got map[string]any
			if err := json.Unmarshal(body, &got); err != nil {
				t.Fatal(err)
			}
			if got[tc.wantField] != tc.want {
				t.Fatalf("%s=%v, want %v; body=%s", tc.wantField, got[tc.wantField], tc.want, body)
			}
			if _, ok := got[tc.wantAbsent]; ok {
				t.Fatalf("%s survived normalization: %s", tc.wantAbsent, body)
			}
			if strings.Contains(tc.body, `"future"`) && !strings.Contains(string(body), `"future":{"keep":true}`) {
				t.Fatalf("unknown provider field changed: %s", body)
			}
		})
	}
}

func TestGeminiChatProviderStreamingThoughtSignatureNormalization(t *testing.T) {
	line := []byte("data: {\"model\":\"served\",\"choices\":[{\"delta\":{\"tool_calls\":[{\"id\":\"call-1\",\"type\":\"function\",\"function\":{\"name\":\"glob\",\"arguments\":\"{}\"},\"extra_content\":{\"google\":{\"thought_signature\":\"opaque\"}}}]}}]}\n\n")
	rewriter := newProviderModelRewriterWithToolCallWire(true, "public", config.ProtocolToolCallWireGeminiChat)
	got := append(rewriter.AppendStream(line), rewriter.FlushStream()...)
	if strings.Contains(string(got), "thought_signature") || !strings.Contains(string(got), geminiThoughtSignatureToolIDPrefix) {
		t.Fatalf("streaming signature was not normalized: %s", got)
	}
}

func TestGeminiChatProviderRejectsMalformedOpaqueToolID(t *testing.T) {
	_, err := normalizeGeminiChatProviderRequest([]byte(`{"messages":[{"role":"assistant","tool_calls":[{"id":"iop_gts_not-base64"}]}]}`))
	if err == nil {
		t.Fatal("malformed IOP Gemini tool id must fail closed")
	}
}

func TestOpenAIChatProviderHTTPNormalizesGenericTokenLimit(t *testing.T) {
	var providerRequest map[string]any
	provider := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		body, _ := io.ReadAll(r.Body)
		if err := json.Unmarshal(body, &providerRequest); err != nil {
			t.Errorf("decode provider request: %v", err)
			w.WriteHeader(http.StatusBadRequest)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"id":"chat-openai","model":"served-openai","choices":[{"index":0,"message":{"role":"assistant","content":"done"},"finish_reason":"stop"}]}`))
	}))
	defer provider.Close()

	profile, err := config.ResolveProtocolProfile("openai", "", config.BuiltInProtocolProfileCatalog())
	if err != nil {
		t.Fatal(err)
	}
	fake := &providerFakeRunService{
		poolDispatchPath:   string(edgeservice.ProviderPoolPathTunnel),
		tunnelProviderURL:  provider.URL,
		tunnelServedTarget: "served-openai",
		poolSelectedCandidate: edgeservice.ProviderPoolCandidate{
			ActualModel: "served-openai", ProviderID: "openai-provider",
			ExecutionPath: string(edgeservice.ProviderPoolPathTunnel), ProfileID: profile.ID,
			ProfileDriver: string(profile.Driver), ProfileCapabilities: append([]string(nil), profile.Capabilities...),
			ProtocolProfile: &profile,
		},
	}
	srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
	srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "openai-route", Providers: map[string]string{"openai-provider": "served-openai"}}})

	request := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{"model":"openai-route","messages":[{"role":"user","content":"hello"}],"max_tokens":32000,"future":{"keep":true}}`))
	response := httptest.NewRecorder()
	srv.handleChatCompletions(response, request)
	if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), `"content":"done"`) {
		t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
	}
	if providerRequest["max_completion_tokens"] != float64(32000) {
		t.Fatalf("max_completion_tokens=%v, request=%+v", providerRequest["max_completion_tokens"], providerRequest)
	}
	if _, ok := providerRequest["max_tokens"]; ok {
		t.Fatalf("legacy max_tokens reached OpenAI provider: %+v", providerRequest)
	}
	if future, ok := providerRequest["future"].(map[string]any); !ok || future["keep"] != true {
		t.Fatalf("unknown provider field changed: %+v", providerRequest)
	}
}

func TestHotPathOpenAIToolsAndEffortUseResponsesOperation(t *testing.T) {
	profile, err := config.ResolveProtocolProfile("openai", "", config.BuiltInProtocolProfileCatalog())
	if err != nil {
		t.Fatal(err)
	}
	tunnel := edgeservice.SubmitProviderTunnelRequest{
		Operation: string(config.OperationChatCompletions),
		Path:      "/v1/chat/completions",
		BuildBody: func(target string) ([]byte, error) {
			return []byte(`{"model":"` + target + `","messages":[{"role":"system","content":"inspect"},{"role":"user","content":"work"}],"tools":[{"type":"function","function":{"name":"bash","parameters":{"type":"object"}}}],"max_tokens":32000,"reasoning_effort":"high","temperature":0,"top_p":1,"stream":true}`), nil
		},
	}
	prepared, err := prepareHotPathChatProviderOperation(tunnel, edgeservice.ProviderPoolCandidate{ProtocolProfile: &profile}, providerRequestRequirements{
		HasTools: true, Stream: true, Effort: "high",
	})
	if err != nil {
		t.Fatal(err)
	}
	if prepared.Operation != string(config.OperationResponses) || prepared.Path != "/v1/responses" {
		t.Fatalf("operation=%q path=%q", prepared.Operation, prepared.Path)
	}
	if prepared.Stream {
		t.Fatal("buffered internal Responses operation retained streaming tunnel metadata")
	}
	body, err := prepared.BuildBody("gpt-5.6-terra")
	if err != nil {
		t.Fatal(err)
	}
	var request map[string]any
	if err := json.Unmarshal(body, &request); err != nil {
		t.Fatal(err)
	}
	if request["max_output_tokens"] != float64(32000) || request["messages"] != nil || request["max_tokens"] != nil {
		t.Fatalf("Responses token/message conversion mismatch: %+v", request)
	}
	reasoning, _ := request["reasoning"].(map[string]any)
	if reasoning["effort"] != "high" || len(anySlice(request["tools"])) != 1 {
		t.Fatalf("Responses effort/tools conversion mismatch: %+v", request)
	}
	if request["temperature"] != nil || request["top_p"] != nil {
		t.Fatalf("reasoning Responses request retained incompatible sampling controls: %+v", request)
	}
}

func TestChatProviderRequirementsAllowPresetOwnedEffort(t *testing.T) {
	requirements := chatProviderRequirements(chatCompletionRequest{Tools: []any{map[string]any{"type": "function"}}})
	if requirements.Effort != "" || !requirements.HasTools {
		t.Fatalf("caller requirements=%+v", requirements)
	}
	presetOptions := map[string]any{"reasoning_effort": "high"}
	if effort, ok := presetOptions["reasoning_effort"].(string); ok {
		requirements.Effort = strings.TrimSpace(effort)
	}
	if requirements.Effort != "high" || !requirements.HasTools {
		t.Fatalf("effective selector requirements=%+v", requirements)
	}
}

func TestHotPathSelectorOutputLimitIsBounded(t *testing.T) {
	tunnel := edgeservice.SubmitProviderTunnelRequest{
		BuildBody: func(string) ([]byte, error) {
			return []byte(`{"model":"selector","max_completion_tokens":32000,"future":true}`), nil
		},
	}
	prepared, err := prepareHotPathSelectorOutputLimit(tunnel)
	if err != nil {
		t.Fatal(err)
	}
	body, err := prepared.BuildBody("selector")
	if err != nil {
		t.Fatal(err)
	}
	var request map[string]any
	if err := json.Unmarshal(body, &request); err != nil {
		t.Fatal(err)
	}
	if request["max_tokens"] != float64(maxHotPathSelectorOutputTokens) || request["max_completion_tokens"] != nil || request["future"] != true {
		t.Fatalf("bounded selector request=%+v", request)
	}
}

func TestOpenAIHotPathResponsesOperationUsesResponsesDecoder(t *testing.T) {
	body := []byte(`{"id":"resp-selector","model":"gpt-5.6-terra","status":"completed","output":[{"type":"function_call","id":"fc-1","call_id":"call-1","name":"bash","arguments":"{\"command\":\"true\"}"}],"usage":{"input_tokens":10,"output_tokens":3}}`)
	stage, err := decodePresetTunnelBody(body, "application/json", "openai", string(config.OperationResponses), string(config.ProtocolDriverOpenAIChat))
	if err != nil {
		t.Fatal(err)
	}
	if stage.ResponseID != "resp-selector" || len(stage.ToolCalls) != 1 || stage.ToolCalls[0].Name != "bash" {
		t.Fatalf("Responses stage=%+v", stage)
	}
}

func TestHotPathSelectorCanonicalWriteToolReplacesCallerCommandSchema(t *testing.T) {
	preset := config.ExecutionPreset{WorkspaceTools: []config.ExecutionWorkspaceToolAlternative{{
		Operations: map[string]config.ExecutionWorkspaceOperation{
			"write": {ToolName: "bash"},
		},
	}}}
	tunnel := edgeservice.SubmitProviderTunnelRequest{BuildBody: func(string) ([]byte, error) {
		return []byte(`{"model":"selector","tools":[{"type":"function","function":{"name":"bash","description":"shell","parameters":{"type":"object","properties":{"command":{"type":"string"}},"required":["command"]}}},{"type":"function","function":{"name":"webfetch","parameters":{"type":"object"}}}]}`), nil
	}}
	prepared, err := prepareHotPathSelectorCanonicalTools(tunnel, "Operation: pair-write", preset)
	if err != nil {
		t.Fatal(err)
	}
	body, err := prepared.BuildBody("selector")
	if err != nil {
		t.Fatal(err)
	}
	var request map[string]any
	if err := json.Unmarshal(body, &request); err != nil {
		t.Fatal(err)
	}
	tools := anySlice(request["tools"])
	if len(tools) != 1 {
		t.Fatalf("canonical tools=%+v", tools)
	}
	function := tools[0].(map[string]any)["function"].(map[string]any)
	parameters := function["parameters"].(map[string]any)
	properties := parameters["properties"].(map[string]any)
	if function["name"] != hotPathArtifactPairToolName || properties["plan_content"] == nil || properties["review_content"] == nil || properties["command"] != nil {
		t.Fatalf("canonical write function=%+v", function)
	}
	if request["tool_choice"] != "required" || request["parallel_tool_calls"] != nil {
		t.Fatalf("pair-write must force one atomic tool call: %+v", request)
	}
}

func TestGeminiChatProviderHTTPToolContinuationRoundTrip(t *testing.T) {
	var providerRequests []map[string]any
	provider := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		body, _ := io.ReadAll(r.Body)
		var request map[string]any
		if err := json.Unmarshal(body, &request); err != nil {
			t.Errorf("decode provider request: %v", err)
			w.WriteHeader(http.StatusBadRequest)
			return
		}
		providerRequests = append(providerRequests, request)
		w.Header().Set("Content-Type", "application/json")
		if len(providerRequests) == 1 {
			_, _ = w.Write([]byte(`{"id":"chat-1","model":"served-gemini","choices":[{"index":0,"message":{"role":"assistant","content":null,"tool_calls":[{"id":"call-1","type":"function","function":{"name":"glob","arguments":"{\"pattern\":\"*\"}"},"extra_content":{"google":{"thought_signature":"opaque-signature"}}}]},"finish_reason":"tool_calls"}]}`))
			return
		}
		_, _ = w.Write([]byte(`{"id":"chat-2","model":"served-gemini","choices":[{"index":0,"message":{"role":"assistant","content":"done"},"finish_reason":"stop"}]}`))
	}))
	defer provider.Close()

	profile, err := config.ResolveProtocolProfile("gemini", "", config.BuiltInProtocolProfileCatalog())
	if err != nil {
		t.Fatal(err)
	}
	fake := &providerFakeRunService{
		poolDispatchPath:   string(edgeservice.ProviderPoolPathTunnel),
		tunnelProviderURL:  provider.URL,
		tunnelServedTarget: "served-gemini",
		poolSelectedCandidate: edgeservice.ProviderPoolCandidate{
			ActualModel: "served-gemini", ProviderID: "gemini-provider",
			ExecutionPath: string(edgeservice.ProviderPoolPathTunnel), ProfileID: profile.ID,
			ProfileDriver: string(profile.Driver), ProfileCapabilities: append([]string(nil), profile.Capabilities...),
			ProtocolProfile: &profile,
		},
	}
	srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
	srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "gemini-route", Providers: map[string]string{"gemini-provider": "served-gemini"}}})

	first := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{"model":"gemini-route","messages":[{"role":"user","content":"list files"}],"tools":[{"type":"function","function":{"name":"glob","parameters":{"type":"object"}}}]}`))
	firstResponse := httptest.NewRecorder()
	srv.handleChatCompletions(firstResponse, first)
	if firstResponse.Code != http.StatusOK {
		t.Fatalf("first status=%d body=%s", firstResponse.Code, firstResponse.Body.String())
	}
	if got := fake.lastTunnelHandle.Dispatch().ProfileToolCallWire; got != config.ProtocolToolCallWireGeminiChat {
		t.Fatalf("selected dispatch tool-call wire=%q", got)
	}
	var firstBody map[string]any
	if err := json.Unmarshal(firstResponse.Body.Bytes(), &firstBody); err != nil {
		t.Fatal(err)
	}
	firstChoice := anySlice(firstBody["choices"])[0].(map[string]any)
	firstMessage := firstChoice["message"].(map[string]any)
	encodedID := anySlice(firstMessage["tool_calls"])[0].(map[string]any)["id"].(string)
	if !strings.HasPrefix(encodedID, geminiThoughtSignatureToolIDPrefix) {
		t.Fatalf("caller did not receive opaque normalized id: %s", firstResponse.Body.String())
	}

	secondBody := `{"model":"gemini-route","messages":[{"role":"user","content":"list files"},{"role":"assistant","content":null,"tool_calls":[{"id":"` + encodedID + `","type":"function","function":{"name":"glob","arguments":"{\"pattern\":\"*\"}"}}]},{"role":"tool","tool_call_id":"` + encodedID + `","content":"[]"}]}`
	second := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(secondBody))
	secondResponse := httptest.NewRecorder()
	srv.handleChatCompletions(secondResponse, second)
	if secondResponse.Code != http.StatusOK || !strings.Contains(secondResponse.Body.String(), `"content":"done"`) {
		t.Fatalf("second status=%d body=%s", secondResponse.Code, secondResponse.Body.String())
	}
	if len(providerRequests) != 2 {
		t.Fatalf("provider requests=%d", len(providerRequests))
	}
	messages := anySlice(providerRequests[1]["messages"])
	replayedCall := anySlice(messages[1].(map[string]any)["tool_calls"])[0].(map[string]any)
	if replayedCall["id"] != "call-1" || messages[2].(map[string]any)["tool_call_id"] != "call-1" {
		t.Fatalf("provider did not receive original tool ids: %+v", messages)
	}
	google := replayedCall["extra_content"].(map[string]any)["google"].(map[string]any)
	if google["thought_signature"] != "opaque-signature" {
		t.Fatalf("provider did not receive restored signature: %+v", replayedCall)
	}
}
