package clients

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

	"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)

func TestOpenAIResponsesNativeUsesResponsesEndpointAndPreservesVendorIDs(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/responses" {
			t.Fatalf("expected /responses, got %s", r.URL.Path)
		}
		var body map[string]any
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			t.Fatal(err)
		}
		if body["messages"] != nil {
			t.Fatalf("native Responses request must not contain messages: %+v", body)
		}
		input, _ := body["input"].([]any)
		if len(input) != 1 {
			t.Fatalf("native Responses request must translate controlled messages to input: %+v", body)
		}
		if body["previous_response_id"] != "resp_upstream_parent" {
			t.Fatalf("expected translated upstream previous id, got %+v", body["previous_response_id"])
		}
		_ = json.NewEncoder(w).Encode(map[string]any{
			"id": "resp_upstream_child", "object": "response", "status": "completed", "previous_response_id": "resp_upstream_parent",
			"output": []any{
				map[string]any{
					"type":    "message",
					"content": []any{map[string]any{"type": "output_text", "text": "ok"}},
				},
			},
			"usage": map[string]any{"input_tokens": 2, "output_tokens": 1, "total_tokens": 3},
		})
	}))
	defer server.Close()

	response, err := (OpenAIClient{}).Run(context.Background(), Request{
		Kind: "responses", Model: "Demo",
		Body:             map[string]any{"messages": []any{map[string]any{"role": "user", "content": "hello"}}},
		Candidate:        store.RuntimeModelCandidate{BaseURL: server.URL, ProviderModelName: "demo", Credentials: map[string]any{"apiKey": "secret"}},
		UpstreamProtocol: ProtocolOpenAIResponses, PublicResponseID: "resp_12345678901234567890123456789012",
		PublicPreviousResponseID: "resp_abcdefghijklmnopqrstuvwxyz123456", UpstreamPreviousResponseID: "resp_upstream_parent",
	})
	if err != nil {
		t.Fatal(err)
	}
	if response.Result["id"] != "resp_upstream_child" || response.Result["previous_response_id"] != "resp_upstream_parent" {
		t.Fatalf("vendor response ids were not preserved: %+v", response.Result)
	}
	if response.PublicResponseID != "resp_upstream_child" || response.UpstreamResponseID != "resp_upstream_child" || response.UpstreamEndpoint != "/responses" || response.ResponseConverted {
		t.Fatalf("unexpected native response metadata: %+v", response)
	}
}

func TestOpenAIResponsesChatFallbackPreservesHistoryToolsUsageAndReasoningInternally(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/chat/completions" {
			t.Fatalf("expected /chat/completions, got %s", r.URL.Path)
		}
		var body map[string]any
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			t.Fatal(err)
		}
		messages, _ := body["messages"].([]any)
		if len(messages) != 4 {
			t.Fatalf("expected prior user/assistant plus current system/user/tool messages, got %+v", messages)
		}
		if body["max_tokens"] != float64(128) || body["reasoning_effort"] != "high" {
			t.Fatalf("expected mapped max/reasoning fields: %+v", body)
		}
		tools, _ := body["tools"].([]any)
		if len(tools) != 1 {
			t.Fatalf("expected mapped function tool: %+v", body["tools"])
		}
		_ = json.NewEncoder(w).Encode(map[string]any{
			"id": "chatcmpl-upstream", "object": "chat.completion", "created": 1710000000, "model": "demo",
			"choices": []any{map[string]any{"index": 0, "finish_reason": "tool_calls", "message": map[string]any{
				"role": "assistant", "content": "visible", "reasoning_content": "hidden reasoning",
				"tool_calls": []any{map[string]any{"id": "call_stable", "type": "function", "function": map[string]any{"name": "lookup", "arguments": "{\"x\":1}"}}},
			}}},
			"usage": map[string]any{"prompt_tokens": 20, "completion_tokens": 5, "total_tokens": 25, "prompt_tokens_details": map[string]any{"cached_tokens": 7}},
		})
	}))
	defer server.Close()

	response, err := (OpenAIClient{}).Run(context.Background(), Request{
		Kind: "responses", Model: "Demo", Candidate: store.RuntimeModelCandidate{BaseURL: server.URL, ProviderModelName: "demo", Credentials: map[string]any{"apiKey": "secret"}},
		UpstreamProtocol: ProtocolOpenAIChatCompletions, PublicResponseID: "resp_12345678901234567890123456789012",
		PreviousResponseTurns: []ResponseTurn{{
			Request: map[string]any{"input": "prior user", "instructions": "must not be replayed"},
			Internal: map[string]any{
				"choices": []any{map[string]any{
					"message": map[string]any{"role": "assistant", "content": "prior assistant"},
				}},
			},
		}},
		Body: map[string]any{
			"instructions": "current instruction", "previous_response_id": "resp_parent", "input": []any{map[string]any{"type": "function_call_output", "call_id": "call_prior", "output": "done"}},
			"max_output_tokens": 128, "reasoning": map[string]any{"effort": "high"},
			"tools": []any{map[string]any{"type": "function", "name": "lookup", "description": "demo", "parameters": map[string]any{"type": "object"}}},
		},
	})
	if err != nil {
		t.Fatal(err)
	}
	if !response.ResponseConverted || response.UpstreamProtocol != ProtocolOpenAIChatCompletions || response.UpstreamEndpoint != "/chat/completions" {
		t.Fatalf("unexpected conversion metadata: %+v", response)
	}
	if response.Result["previous_response_id"] != "resp_parent" {
		t.Fatalf("Chat fallback response lost previous_response_id: %+v", response.Result)
	}
	output, _ := response.Result["output"].([]any)
	if len(output) != 2 {
		t.Fatalf("expected message plus function call: %+v", response.Result)
	}
	call, _ := output[1].(map[string]any)
	if call["call_id"] != "call_stable" || call["arguments"] != "{\"x\":1}" {
		t.Fatalf("function call continuity lost: %+v", call)
	}
	if strings.Contains(string(mustJSON(t, response.Result)), "hidden reasoning") {
		t.Fatalf("visible Responses result leaked reasoning: %+v", response.Result)
	}
	if !strings.Contains(string(mustJSON(t, response.InternalResult)), "hidden reasoning") {
		t.Fatalf("internal snapshot must retain reasoning for continuation: %+v", response.InternalResult)
	}
	usage, _ := response.Result["usage"].(map[string]any)
	details, _ := usage["input_tokens_details"].(map[string]any)
	if details["cached_tokens"] != 7 {
		t.Fatalf("cached usage was not preserved: %+v", usage)
	}
}

func TestOpenAIResponsesNativeStreamForwardsEventsWithoutChatAggregation(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/event-stream")
		_, _ = w.Write([]byte("event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_upstream\",\"object\":\"response\",\"status\":\"in_progress\"}}\n\n"))
		_, _ = w.Write([]byte("event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"response_id\":\"resp_upstream\",\"delta\":\"hello\"}\n\n"))
		_, _ = w.Write([]byte("event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_upstream\",\"object\":\"response\",\"status\":\"completed\",\"output\":[],\"usage\":{\"input_tokens\":1,\"output_tokens\":1,\"total_tokens\":2}}}\n\n"))
	}))
	defer server.Close()
	events := make([]StreamDeltaEvent, 0)
	response, err := (OpenAIClient{}).Run(context.Background(), Request{
		Kind: "responses", Model: "Demo", Body: map[string]any{"input": "hello", "stream": true}, Stream: true,
		Candidate:        store.RuntimeModelCandidate{BaseURL: server.URL, ProviderModelName: "demo", Credentials: map[string]any{"apiKey": "secret"}},
		UpstreamProtocol: ProtocolOpenAIResponses, PublicResponseID: "resp_12345678901234567890123456789012",
		StreamDelta: func(event StreamDeltaEvent) error { events = append(events, event); return nil },
	})
	if err != nil {
		t.Fatal(err)
	}
	if len(events) != 3 || events[0].Event["type"] != "response.created" || events[1].Event["type"] != "response.output_text.delta" || events[2].Event["type"] != "response.completed" {
		t.Fatalf("unexpected forwarded events: %+v", events)
	}
	if response.Result["object"] != "response" || response.Result["choices"] != nil || response.UpstreamResponseID != "resp_upstream" {
		t.Fatalf("native stream was rewritten as Chat: %+v", response)
	}
}

func TestOpenAIResponsesChatFallbackStreamsFunctionArgumentFragments(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/chat/completions" {
			t.Fatalf("expected chat fallback endpoint, got %s", r.URL.Path)
		}
		w.Header().Set("Content-Type", "text/event-stream")
		_, _ = w.Write([]byte("data: {\"id\":\"chatcmpl-tool\",\"object\":\"chat.completion.chunk\",\"model\":\"demo\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"id\":\"call_stream_0\",\"type\":\"function\",\"function\":{\"name\":\"lookup_x\",\"arguments\":\"{\\\"x\\\":\"}},{\"index\":1,\"id\":\"call_stream_1\",\"type\":\"function\",\"function\":{\"name\":\"lookup_y\",\"arguments\":\"{\\\"y\\\":\"}}]},\"finish_reason\":null}]}\n\n"))
		_, _ = w.Write([]byte("data: {\"id\":\"chatcmpl-tool\",\"object\":\"chat.completion.chunk\",\"model\":\"demo\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"1}\"}},{\"index\":1,\"function\":{\"arguments\":\"2}\"}}]},\"finish_reason\":null}]}\n\n"))
		_, _ = w.Write([]byte("data: {\"id\":\"chatcmpl-tool\",\"object\":\"chat.completion.chunk\",\"model\":\"demo\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"tool_calls\"}],\"usage\":{\"prompt_tokens\":2,\"completion_tokens\":3,\"total_tokens\":5}}\n\n"))
		_, _ = w.Write([]byte("data: [DONE]\n\n"))
	}))
	defer server.Close()

	events := make([]StreamDeltaEvent, 0)
	response, err := (OpenAIClient{}).Run(context.Background(), Request{
		Kind: "responses", Model: "Demo", Body: map[string]any{
			"input": "call it", "stream": true,
			"tools": []any{map[string]any{"type": "function", "name": "lookup", "parameters": map[string]any{"type": "object"}}},
		}, Stream: true,
		Candidate:        store.RuntimeModelCandidate{BaseURL: server.URL, ProviderModelName: "demo", Credentials: map[string]any{"apiKey": "secret"}},
		UpstreamProtocol: ProtocolOpenAIChatCompletions, PublicResponseID: "resp_12345678901234567890123456789012",
		StreamDelta: func(event StreamDeltaEvent) error { events = append(events, event); return nil },
	})
	if err != nil {
		t.Fatal(err)
	}
	types := make([]string, 0, len(events))
	fragments := map[string]string{}
	outputIndexes := map[int]bool{}
	for _, event := range events {
		eventType := stringFromAny(event.Event["type"])
		types = append(types, eventType)
		if sequence := intFromAny(event.Event["sequence_number"]); sequence != len(types)-1 {
			t.Fatalf("converted event sequence is not contiguous at %d: %+v", len(types)-1, event.Event)
		}
		if eventType == "response.function_call_arguments.delta" {
			itemID := stringFromAny(event.Event["item_id"])
			fragments[itemID] += stringFromAny(event.Event["delta"])
			outputIndexes[intFromAny(event.Event["output_index"])] = true
		}
	}
	if !containsTestString(types, "response.created") || !containsTestString(types, "response.in_progress") || !containsTestString(types, "response.output_item.added") || !containsTestString(types, "response.function_call_arguments.done") || len(fragments) != 2 || len(outputIndexes) != 2 {
		t.Fatalf("unexpected converted stream events types=%v fragments=%v indexes=%v", types, fragments, outputIndexes)
	}
	output, _ := response.Result["output"].([]any)
	if len(output) != 2 {
		t.Fatalf("expected two final function calls: %+v", response.Result)
	}
	first, _ := output[0].(map[string]any)
	second, _ := output[1].(map[string]any)
	if first["call_id"] != "call_stream_0" || first["arguments"] != "{\"x\":1}" || second["call_id"] != "call_stream_1" || second["arguments"] != "{\"y\":2}" {
		t.Fatalf("parallel streamed calls were not aggregated: %+v", output)
	}
}

func TestChatResultToResponseMapsIncompleteFinishReason(t *testing.T) {
	response := ChatResultToResponse(map[string]any{
		"choices": []any{map[string]any{"finish_reason": "length", "message": map[string]any{"role": "assistant", "content": "partial"}}},
	}, "resp_12345678901234567890123456789012", "demo", map[string]any{})
	if response["status"] != "incomplete" {
		t.Fatalf("expected incomplete response, got %+v", response)
	}
	details, _ := response["incomplete_details"].(map[string]any)
	if details["reason"] != "max_output_tokens" {
		t.Fatalf("finish reason was not mapped: %+v", details)
	}
}

func TestNativeResponsesStreamSupportsMultilineDataFrames(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "text/event-stream")
		_, _ = w.Write([]byte("event: response.completed\n"))
		_, _ = w.Write([]byte("data: {\"type\":\"response.completed\",\n"))
		_, _ = w.Write([]byte("data: \"response\":{\"id\":\"resp_vendor-with-dash\",\"object\":\"response\",\"status\":\"completed\",\"output\":[]}}\n\n"))
	}))
	defer server.Close()

	response, err := (OpenAIClient{}).Run(context.Background(), Request{
		Kind: "responses", Model: "Demo", Body: map[string]any{"input": "hello", "stream": true}, Stream: true,
		Candidate:        store.RuntimeModelCandidate{BaseURL: server.URL, ProviderModelName: "demo", Credentials: map[string]any{"apiKey": "secret"}},
		UpstreamProtocol: ProtocolOpenAIResponses,
	})
	if err != nil {
		t.Fatal(err)
	}
	if response.Result["id"] != "resp_vendor-with-dash" {
		t.Fatalf("multiline frame was not decoded: %+v", response.Result)
	}
}

func TestChatResponsesStreamAdapterEmitsTextItemLifecycle(t *testing.T) {
	adapter := newChatResponsesStreamAdapter("resp_12345678901234567890123456789012", "demo")
	events := make([]string, 0)
	onDelta := func(event StreamDeltaEvent) error {
		events = append(events, stringFromAny(event.Event["type"]))
		return nil
	}
	err := adapter.delta(StreamDeltaEvent{Event: map[string]any{
		"choices": []any{map[string]any{"delta": map[string]any{"content": "hello"}}},
	}}, onDelta)
	if err != nil {
		t.Fatal(err)
	}
	result := ChatResultToResponse(map[string]any{
		"choices": []any{map[string]any{"message": map[string]any{"role": "assistant", "content": "hello"}}},
	}, adapter.publicID, "demo", map[string]any{})
	if err := adapter.done(result, onDelta); err != nil {
		t.Fatal(err)
	}
	want := []string{
		"response.created", "response.in_progress", "response.output_item.added", "response.content_part.added",
		"response.output_text.delta", "response.output_text.done", "response.content_part.done", "response.output_item.done",
	}
	if strings.Join(events, ",") != strings.Join(want, ",") {
		t.Fatalf("unexpected text event lifecycle got=%v want=%v", events, want)
	}
}

func TestResponsesChatFallbackRejectsBuiltInToolsAndUnknownParameters(t *testing.T) {
	_, err := ResponsesRequestToChat(map[string]any{"input": "hello", "tools": []any{map[string]any{"type": "web_search_preview"}}}, nil)
	if ErrorCode(err) != "unsupported_response_tool" {
		t.Fatalf("expected unsupported_response_tool, got %v", err)
	}
	_, err = ResponsesRequestToChat(map[string]any{"input": "hello", "conversation": "conv_1"}, nil)
	if ErrorCode(err) != "unsupported_response_parameter" {
		t.Fatalf("expected unsupported_response_parameter, got %v", err)
	}
}

func TestResponsesChatFallbackKeepsClientManagedStateAuthoritative(t *testing.T) {
	input := []any{
		map[string]any{"type": "message", "role": "user", "content": "first"},
		map[string]any{"type": "message", "role": "assistant", "content": "second"},
		map[string]any{"type": "message", "role": "user", "content": "third"},
	}
	body, err := ResponsesRequestToChat(map[string]any{"input": input}, nil)
	if err != nil {
		t.Fatal(err)
	}
	messages, _ := body["messages"].([]any)
	if len(messages) != len(input) {
		t.Fatalf("client-managed input was supplemented by Gateway history: %+v", messages)
	}
	for index, raw := range messages {
		message, _ := raw.(map[string]any)
		expected, _ := input[index].(map[string]any)
		if message["role"] != expected["role"] || message["content"] != expected["content"] {
			t.Fatalf("client-managed message %d changed: got=%+v want=%+v", index, message, expected)
		}
	}
}

func mustJSON(t *testing.T, value any) []byte {
	t.Helper()
	data, err := json.Marshal(value)
	if err != nil {
		t.Fatal(err)
	}
	return data
}

func containsTestString(values []string, expected string) bool {
	for _, value := range values {
		if value == expected {
			return true
		}
	}
	return false
}
