package httpapi

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

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

func TestPlanTaskResponseTreatsAPIV1ChatCompletionsAsSynchronousCompatibleResponse(t *testing.T) {
	req := httptest.NewRequest(http.MethodPost, "/api/v1/chat/completions", nil)
	req.Header.Set("X-Async", "true")

	plan := planTaskResponse("chat.completions", false, map[string]any{"stream": true}, req)

	if plan.asyncMode {
		t.Fatal("/api/v1/chat/completions must not enter async task mode")
	}
	if !plan.compatibleMode {
		t.Fatal("/api/v1/chat/completions should return OpenAI-compatible response payloads")
	}
	if !plan.streamMode {
		t.Fatal("stream=true should select SSE streaming mode")
	}
}

func TestPlanTaskResponseTreatsAPIV1EmbeddingAndRerankAsSynchronousCompatibleResponse(t *testing.T) {
	for _, item := range []struct {
		kind string
		path string
	}{
		{kind: "embeddings", path: "/api/v1/embeddings"},
		{kind: "reranks", path: "/api/v1/reranks"},
	} {
		t.Run(item.kind, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodPost, item.path, nil)
			req.Header.Set("X-Async", "true")

			plan := planTaskResponse(item.kind, false, map[string]any{"stream": true}, req)

			if plan.asyncMode {
				t.Fatalf("%s must not enter async task mode", item.path)
			}
			if !plan.compatibleMode {
				t.Fatalf("%s should return compatible response payloads", item.path)
			}
			if plan.streamMode {
				t.Fatal("embedding and rerank endpoints should stay JSON-only even when stream=true is present")
			}
		})
	}
}

func TestPlanTaskResponseUsesCompatibleAPIV1MediaResponsesAndKeepsAsyncOptIn(t *testing.T) {
	defaultRequest := httptest.NewRequest(http.MethodPost, "/api/v1/images/generations", nil)
	defaultPlan := planTaskResponse("images.generations", true, map[string]any{}, defaultRequest)
	if defaultPlan.asyncMode || !defaultPlan.compatibleMode {
		t.Fatalf("canonical /api/v1 media endpoints should default to synchronous compatible responses, got %+v", defaultPlan)
	}

	asyncRequest := httptest.NewRequest(http.MethodPost, "/api/v1/images/generations", nil)
	asyncRequest.Header.Set("X-Async", "true")
	asyncPlan := planTaskResponse("images.generations", true, map[string]any{}, asyncRequest)
	if !asyncPlan.asyncMode || !asyncPlan.compatibleMode {
		t.Fatalf("canonical /api/v1 media endpoints should keep compatible mode when X-Async is enabled, got %+v", asyncPlan)
	}
}

func TestPlanTaskResponseKeepsCompatibleSyncForAudioOpenAPIUnlessAsync(t *testing.T) {
	for _, item := range []struct {
		kind string
		path string
	}{
		{kind: "song.generations", path: "/api/v1/song/generations"},
		{kind: "music.generations", path: "/api/v1/music/generations"},
		{kind: "speech.generations", path: "/api/v1/speech/generations"},
	} {
		t.Run(item.kind, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodPost, item.path, nil)
			plan := planTaskResponse(item.kind, true, map[string]any{"stream": true}, req)
			if plan.asyncMode {
				t.Fatalf("%s should default to synchronous compatible response", item.path)
			}
			if !plan.compatibleMode {
				t.Fatalf("%s should return compatible response payloads", item.path)
			}
			if plan.streamMode {
				t.Fatal("audio OpenAPI endpoints should stay JSON-only even when stream=true is present")
			}

			asyncReq := httptest.NewRequest(http.MethodPost, item.path, nil)
			asyncReq.Header.Set("X-Async", "true")
			asyncPlan := planTaskResponse(item.kind, true, map[string]any{}, asyncReq)
			if !asyncPlan.asyncMode || !asyncPlan.compatibleMode {
				t.Fatalf("%s should support X-Async while keeping compatible mode, got %+v", item.path, asyncPlan)
			}
		})
	}
}

func TestAPIKeyScopeAllowedRecognizesAudioAndMusicAliases(t *testing.T) {
	if !apiKeyScopeAllowed(&auth.User{APIKeyID: "key", APIKeyScopes: []string{"audio_generate"}}, "song.generations") {
		t.Fatal("audio_generate scope should allow song generations")
	}
	if !apiKeyScopeAllowed(&auth.User{APIKeyID: "key", APIKeyScopes: []string{"text_to_speech"}}, "speech.generations") {
		t.Fatal("text_to_speech scope should allow speech generations")
	}
	if apiKeyScopeAllowed(&auth.User{APIKeyID: "key", APIKeyScopes: []string{"image"}}, "speech.generations") {
		t.Fatal("image scope should not allow speech generations")
	}
}

func TestWriteCompatibleTaskResponseReturnsJSONWhenStreamIsFalse(t *testing.T) {
	executor := &fakeTaskExecutor{output: map[string]any{"id": "chatcmpl-test", "object": "chat.completion"}}
	req := httptest.NewRequest(http.MethodPost, "/api/v1/chat/completions", nil)
	recorder := httptest.NewRecorder()

	writeCompatibleTaskResponse(context.Background(), recorder, req, executor, "chat.completions", "gpt-test", store.GatewayTask{ID: "task-test"}, &auth.User{}, false, false)

	if recorder.Code != http.StatusOK {
		t.Fatalf("status=%d want=%d body=%s", recorder.Code, http.StatusOK, recorder.Body.String())
	}
	if executor.executeCalls != 1 || executor.streamCalls != 0 {
		t.Fatalf("expected non-stream execute only, got execute=%d stream=%d", executor.executeCalls, executor.streamCalls)
	}
	var body map[string]any
	if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
		t.Fatalf("decode response body: %v body=%s", err, recorder.Body.String())
	}
	if body["object"] != "chat.completion" {
		t.Fatalf("unexpected compatible JSON response: %+v", body)
	}
}

func TestWriteCompatibleTaskResponseMapsInvalidParameterToBadRequest(t *testing.T) {
	executor := &fakeTaskExecutor{
		runErr: &clients.ClientError{
			Code:      "invalid_parameter",
			Message:   "reasoning_effort must be one of: none, minimal, low, medium, high, xhigh, max",
			Retryable: false,
		},
	}
	req := httptest.NewRequest(http.MethodPost, "/api/v1/chat/completions", nil)
	recorder := httptest.NewRecorder()

	writeCompatibleTaskResponse(context.Background(), recorder, req, executor, "chat.completions", "gpt-test", store.GatewayTask{ID: "task-test"}, &auth.User{}, false, false)

	if recorder.Code != http.StatusBadRequest {
		t.Fatalf("status=%d want=%d body=%s", recorder.Code, http.StatusBadRequest, recorder.Body.String())
	}
	if !strings.Contains(recorder.Body.String(), "invalid_parameter") {
		t.Fatalf("response should include invalid_parameter code: %s", recorder.Body.String())
	}
	if !strings.Contains(recorder.Body.String(), `"type":"invalid_request_error"`) || !strings.Contains(recorder.Body.String(), `"param":null`) {
		t.Fatalf("response should use OpenAI error fields: %s", recorder.Body.String())
	}
}

func TestWriteCompatibleTaskResponseReturnsSSEWhenStreamIsTrue(t *testing.T) {
	executor := &fakeTaskExecutor{
		deltas: []clients.StreamDeltaEvent{{Text: "hel"}, {Text: "lo"}},
		output: map[string]any{"id": "chatcmpl-test", "object": "chat.completion", "usage": map[string]any{"prompt_tokens": 1, "completion_tokens": 2, "total_tokens": 3}},
	}
	req := httptest.NewRequest(http.MethodPost, "/api/v1/chat/completions", nil)
	recorder := httptest.NewRecorder()

	writeCompatibleTaskResponse(context.Background(), recorder, req, executor, "chat.completions", "gpt-test", store.GatewayTask{ID: "task-test"}, &auth.User{}, true, true)

	if executor.executeCalls != 0 || executor.streamCalls != 1 {
		t.Fatalf("expected stream execute only, got execute=%d stream=%d", executor.executeCalls, executor.streamCalls)
	}
	if contentType := recorder.Header().Get("Content-Type"); contentType != "text/event-stream" {
		t.Fatalf("Content-Type=%q want text/event-stream", contentType)
	}
	body := recorder.Body.String()
	for _, want := range []string{`data: {`, `"role":"assistant"`, `"created":`, `"system_fingerprint":`, `"content":"hel"`, `"content":"lo"`, `"finish_reason":"stop"`, `"usage":{"completion_tokens":2,"prompt_tokens":1,"total_tokens":3}`, "data: [DONE]"} {
		if !strings.Contains(body, want) {
			t.Fatalf("SSE body missing %s: %s", want, body)
		}
	}
	if strings.Contains(body, "event: message") {
		t.Fatalf("chat completions stream should use OpenAI data-only SSE frames: %s", body)
	}
}

func TestWriteCompatibleTaskResponseStreamsCachedUsageProviderShapes(t *testing.T) {
	cases := []struct {
		name     string
		usage    map[string]any
		fragment string
	}{
		{
			name: "aliyun dashscope",
			usage: map[string]any{
				"prompt_tokens":     10,
				"completion_tokens": 2,
				"total_tokens":      12,
				"prompt_tokens_details": map[string]any{
					"cached_tokens": 7,
				},
			},
			fragment: `"prompt_tokens_details":{"cached_tokens":7}`,
		},
		{
			name: "volces responses",
			usage: map[string]any{
				"prompt_tokens":     9,
				"completion_tokens": 3,
				"total_tokens":      12,
				"input_tokens_details": map[string]any{
					"cached_tokens": 6,
				},
			},
			fragment: `"input_tokens_details":{"cached_tokens":6}`,
		},
		{
			name: "deepseek",
			usage: map[string]any{
				"prompt_tokens":            8,
				"completion_tokens":        4,
				"total_tokens":             12,
				"prompt_cache_hit_tokens":  5,
				"prompt_cache_miss_tokens": 3,
			},
			fragment: `"prompt_cache_hit_tokens":5`,
		},
		{
			name: "glm",
			usage: map[string]any{
				"prompt_tokens":     11,
				"completion_tokens": 1,
				"total_tokens":      12,
				"prompt_tokens_details": map[string]any{
					"cached_tokens": 4,
				},
			},
			fragment: `"prompt_tokens_details":{"cached_tokens":4}`,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			executor := &fakeTaskExecutor{
				deltas: []clients.StreamDeltaEvent{{Text: "ok"}},
				output: map[string]any{
					"id":     "chatcmpl-cache",
					"object": "chat.completion",
					"usage":  tc.usage,
				},
			}
			req := httptest.NewRequest(http.MethodPost, "/api/v1/chat/completions", nil)
			recorder := httptest.NewRecorder()

			writeCompatibleTaskResponse(context.Background(), recorder, req, executor, "chat.completions", "cache-test", store.GatewayTask{ID: "task-test"}, &auth.User{}, true, true)

			body := recorder.Body.String()
			if !strings.Contains(body, tc.fragment) {
				t.Fatalf("SSE body missing cached usage fragment %s: %s", tc.fragment, body)
			}
			if !strings.Contains(body, `"choices":[]`) || !strings.Contains(body, "data: [DONE]") {
				t.Fatalf("SSE body should include final usage chunk and done marker: %s", body)
			}
		})
	}
}

func TestWriteCompatibleTaskResponseStreamsStructuredToolAndReasoningDeltas(t *testing.T) {
	executor := &fakeTaskExecutor{
		deltas: []clients.StreamDeltaEvent{
			{Event: map[string]any{"id": "chatcmpl-upstream", "object": "chat.completion.chunk", "created": float64(1710000000), "model": "deepseek-v4", "system_fingerprint": "fp-test", "choices": []any{map[string]any{"index": float64(0), "delta": map[string]any{"reasoning_details": []any{map[string]any{"type": "reasoning.text", "text": "detail-"}, map[string]any{"type": "reasoning.summary", "summary": "summary"}, map[string]any{"type": "reasoning.encrypted", "data": "secret"}}}, "finish_reason": nil}}}},
			{Event: map[string]any{"id": "chatcmpl-upstream", "object": "chat.completion.chunk", "created": float64(1710000000), "model": "deepseek-v4", "system_fingerprint": "fp-test", "choices": []any{map[string]any{"index": float64(0), "delta": map[string]any{"content": "<think>tagged</think>answer"}, "finish_reason": nil}}}},
			{Event: map[string]any{"id": "chatcmpl-upstream", "object": "chat.completion.chunk", "created": float64(1710000000), "model": "deepseek-v4", "system_fingerprint": "fp-test", "choices": []any{map[string]any{"index": float64(0), "delta": map[string]any{"functionCall": map[string]any{"name": "legacy_lookup", "arguments": "{\"city\":\"Boston\"}"}}, "finish_reason": nil}}}},
			{Event: map[string]any{"id": "chatcmpl-upstream", "object": "chat.completion.chunk", "created": float64(1710000000), "model": "deepseek-v4", "system_fingerprint": "fp-test", "choices": []any{map[string]any{"index": float64(0), "delta": map[string]any{"tool_calls": []any{map[string]any{"index": float64(0), "id": "call_1", "type": "function", "function": map[string]any{"name": "lookup", "arguments": "{\"q\":"}}}}, "finish_reason": nil}}}},
			{Event: map[string]any{"id": "chatcmpl-upstream", "object": "chat.completion.chunk", "created": float64(1710000000), "model": "deepseek-v4", "system_fingerprint": "fp-test", "choices": []any{map[string]any{"index": float64(0), "delta": map[string]any{"tool_calls": []any{map[string]any{"index": float64(0), "function": map[string]any{"arguments": "\"weather\"}"}}}}, "finish_reason": "tool_calls"}}}},
			{Event: map[string]any{"id": "chatcmpl-upstream", "object": "chat.completion.chunk", "created": float64(1710000000), "model": "deepseek-v4", "choices": []any{}, "usage": map[string]any{"prompt_tokens": float64(4), "completion_tokens": float64(5), "total_tokens": float64(9)}}},
		},
		output: map[string]any{"id": "chatcmpl-upstream", "object": "chat.completion", "model": "deepseek-v4"},
	}
	req := httptest.NewRequest(http.MethodPost, "/api/v1/chat/completions", nil)
	recorder := httptest.NewRecorder()

	writeCompatibleTaskResponse(context.Background(), recorder, req, executor, "chat.completions", "gpt-test", store.GatewayTask{ID: "task-test"}, &auth.User{}, true, true)

	body := recorder.Body.String()
	roleIndex := strings.Index(body, `"role":"assistant"`)
	reasoningIndex := strings.Index(body, `"reasoning_content":"detail-summary"`)
	if roleIndex < 0 || reasoningIndex < 0 || roleIndex > reasoningIndex {
		t.Fatalf("assistant role should be emitted before structured deltas: %s", body)
	}
	for _, want := range []string{`"system_fingerprint":"fp-test"`, `"created":1710000000`, `"reasoning_content":"tagged"`, `"content":"answer"`, `"function_call":{"arguments":"{\"city\":\"Boston\"}","name":"legacy_lookup"}`, `"tool_calls":[{"function":{"arguments":"{\"q\":"`, `"finish_reason":"tool_calls"`, `"choices":[],"created":1710000000`, `"usage":{"completion_tokens":5,"prompt_tokens":4,"total_tokens":9}`, "data: [DONE]"} {
		if !strings.Contains(body, want) {
			t.Fatalf("SSE body missing %s: %s", want, body)
		}
	}
	if strings.Contains(body, "reasoning_details") || strings.Contains(body, "<think>") || strings.Contains(body, "functionCall") {
		t.Fatalf("provider-specific reasoning/tool fields should be converted away: %s", body)
	}
}

type fakeTaskExecutor struct {
	executeCalls int
	streamCalls  int
	deltas       []clients.StreamDeltaEvent
	output       map[string]any
	runErr       error
}

func (f *fakeTaskExecutor) Execute(context.Context, store.GatewayTask, *auth.User) (runner.Result, error) {
	f.executeCalls++
	if f.runErr != nil {
		return runner.Result{}, f.runErr
	}
	return runner.Result{Output: f.output}, nil
}

func (f *fakeTaskExecutor) ExecuteStream(_ context.Context, _ store.GatewayTask, _ *auth.User, onDelta clients.StreamDelta) (runner.Result, error) {
	f.streamCalls++
	if f.runErr != nil {
		return runner.Result{}, f.runErr
	}
	for _, delta := range f.deltas {
		if err := onDelta(delta); err != nil {
			return runner.Result{}, err
		}
	}
	return runner.Result{Output: f.output}, nil
}
