package httpapi

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

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

func TestEasyAITaskResultURLResponseIsCompact(t *testing.T) {
	task := store.GatewayTask{
		ID: "image-url-compact", Kind: "images.generations", Status: "succeeded",
		Result: map[string]any{"data": []any{map[string]any{
			"type": "image", "url": "https://cdn.example/result.png", "mime_type": "image/png",
			"provider_payload": strings.Repeat("x", 4096), "upload": map[string]any{"objectKey": "secret"},
		}}},
	}
	runtime.GC()
	var before runtime.MemStats
	runtime.ReadMemStats(&before)
	response := easyAITaskResultResponse(task)
	payload, err := json.Marshal(response)
	if err != nil {
		t.Fatal(err)
	}
	var after runtime.MemStats
	runtime.ReadMemStats(&after)
	if len(payload) >= maxAsyncMediaResultResponseBytes {
		t.Fatalf("URL response bytes=%d", len(payload))
	}
	if allocated := after.TotalAlloc - before.TotalAlloc; allocated >= 1<<20 {
		t.Fatalf("URL response allocated %d bytes", allocated)
	}
	if !json.Valid(payload) {
		t.Fatal("URL response is not valid JSON")
	}
	if strings.Contains(string(payload), "provider_payload") || strings.Contains(string(payload), "objectKey") {
		t.Fatalf("internal provider or object-storage fields leaked: %s", payload)
	}
}

func TestWriteAsyncMediaResultJSONSetsURLAndDeprecationHeaders(t *testing.T) {
	recorder := httptest.NewRecorder()
	writeAsyncMediaResultJSON(recorder, map[string]any{
		"status": "success", "data": []any{map[string]any{"url": "https://cdn.example/result.png"}},
	}, nil)
	if recorder.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if recorder.Header().Get("X-Gateway-Response-Format") != "url" || recorder.Header().Get("Deprecation") != "true" {
		t.Fatalf("unexpected headers: %#v", recorder.Header())
	}
}

func TestWriteAsyncMediaResultJSONRejectsOversizedPayload(t *testing.T) {
	recorder := httptest.NewRecorder()
	writeAsyncMediaResultJSON(recorder, map[string]any{"data": strings.Repeat("x", maxAsyncMediaResultResponseBytes)}, nil)
	if recorder.Code != http.StatusInternalServerError || !strings.Contains(recorder.Body.String(), "result_response_too_large") {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
}

func TestConcurrentURLResultResponsesRemainBounded(t *testing.T) {
	task := store.GatewayTask{
		ID: "image-url-load", Kind: "images.generations", Status: "succeeded",
		Result: map[string]any{"data": []any{map[string]any{
			"type": "image", "url": "https://cdn.example/result.png", "mime_type": "image/png",
		}}},
	}
	runtime.GC()
	var before runtime.MemStats
	runtime.ReadMemStats(&before)

	var wait sync.WaitGroup
	errors := make(chan string, 1000)
	for index := 0; index < 1000; index++ {
		wait.Add(1)
		go func() {
			defer wait.Done()
			payload, err := json.Marshal(easyAITaskResultResponse(task))
			if err != nil {
				errors <- err.Error()
				return
			}
			if len(payload) >= maxAsyncMediaResultResponseBytes {
				errors <- "response exceeded URL-only size gate"
			}
		}()
	}
	wait.Wait()
	close(errors)
	for message := range errors {
		t.Fatal(message)
	}
	var after runtime.MemStats
	runtime.ReadMemStats(&after)
	if allocated := after.TotalAlloc - before.TotalAlloc; allocated >= 256<<20 {
		t.Fatalf("1000 concurrent URL responses allocated %d bytes", allocated)
	}
}

func TestEasyAITaskAcceptedResponseKeepsGatewayFieldsAndAddsLegacyFields(t *testing.T) {
	task := store.GatewayTask{
		ID:        "task-accepted-1",
		Kind:      "speech.generations",
		Status:    "queued",
		AsyncMode: true,
	}

	got := easyAITaskAcceptedResponse(task)
	if got["status"] != "submitted" || got["task_id"] != task.ID || got["taskId"] != task.ID {
		t.Fatalf("unexpected EasyAI submission identity: %+v", got)
	}
	if _, ok := got["task"].(store.GatewayTask); !ok {
		t.Fatalf("Gateway task envelope was removed: %+v", got)
	}
	next, _ := got["next"].(map[string]string)
	if next["detail"] != "/api/v1/tasks/"+task.ID ||
		next["events"] != "/api/v1/tasks/"+task.ID+"/events" ||
		next["result"] != "/api/v1/ai/result/"+task.ID {
		t.Fatalf("unexpected next links: %+v", next)
	}
}

func TestEasyAITaskResultStatus(t *testing.T) {
	for _, item := range []struct {
		input string
		want  string
	}{
		{input: "queued", want: "submitted"},
		{input: "pending", want: "submitted"},
		{input: "running", want: "process"},
		{input: "processing", want: "process"},
		{input: "succeeded", want: "success"},
		{input: "completed", want: "success"},
		{input: "failed", want: "failed"},
		{input: "cancelled", want: "failed"},
	} {
		t.Run(item.input, func(t *testing.T) {
			if got := easyAITaskResultStatus(item.input); got != item.want {
				t.Fatalf("easyAITaskResultStatus(%q)=%q, want %q", item.input, got, item.want)
			}
		})
	}
}

func TestEasyAITaskResultResponseNormalizesMediaOutputs(t *testing.T) {
	createdAt := time.Date(2026, 7, 23, 10, 0, 0, 0, time.UTC)
	for _, item := range []struct {
		name      string
		task      store.GatewayTask
		wantType  string
		wantURL   string
		wantCount int
	}{
		{
			name: "image alias",
			task: store.GatewayTask{
				ID: "image-1", Kind: "images.generations", Status: "succeeded", CreatedAt: createdAt,
				Result: map[string]any{"data": []any{map[string]any{
					"image_url": "https://example.com/output.png",
				}}},
			},
			wantType: "image", wantURL: "https://example.com/output.png", wantCount: 1,
		},
		{
			name: "video raw content",
			task: store.GatewayTask{
				ID: "video-1", Kind: "videos.generations", Status: "succeeded", CreatedAt: createdAt,
				Result: map[string]any{"raw": map[string]any{"content": map[string]any{
					"video_url": "https://example.com/output.mp4",
					"duration":  5,
				}}},
			},
			wantType: "video", wantURL: "https://example.com/output.mp4", wantCount: 1,
		},
		{
			name: "audio alias",
			task: store.GatewayTask{
				ID: "audio-1", Kind: "speech.generations", Status: "succeeded", CreatedAt: createdAt,
				Result: map[string]any{"data": []any{map[string]any{
					"audio_url": "https://example.com/output.wav",
				}}},
			},
			wantType: "audio", wantURL: "https://example.com/output.wav", wantCount: 1,
		},
		{
			name: "voice clone without media",
			task: store.GatewayTask{
				ID: "voice-1", Kind: "voice.clone", Status: "succeeded", CreatedAt: createdAt,
				Result: map[string]any{"voice_id": "voice-test-1", "cloned_voice": map[string]any{"voiceId": "voice-test-1"}},
			},
			wantCount: 0,
		},
	} {
		t.Run(item.name, func(t *testing.T) {
			got := easyAITaskResultResponse(item.task)
			if got["status"] != "success" || got["task_id"] != item.task.ID || got["created"] != createdAt.UnixMilli() {
				t.Fatalf("unexpected task result identity: %+v", got)
			}
			data, _ := got["data"].([]any)
			if len(data) != item.wantCount {
				t.Fatalf("unexpected output count: got=%d response=%+v", len(data), got)
			}
			outputContent, _ := got["output_content"].([]any)
			if len(outputContent) != item.wantCount {
				t.Fatalf("output_content is not synchronized: %+v", got)
			}
			if item.wantCount == 0 {
				if got["voice_id"] != nil || got["cloned_voice"] != nil {
					t.Fatalf("provider-specific fields leaked: %+v", got)
				}
				return
			}
			output, _ := data[0].(map[string]any)
			if output["type"] != item.wantType || output["url"] != item.wantURL {
				t.Fatalf("unexpected normalized output: %+v", output)
			}
			if _, exposed := output["b64_json"]; exposed {
				t.Fatalf("base64 output was exposed: %+v", output)
			}
			urls, _ := got["output"].([]string)
			if len(urls) != 1 || urls[0] != item.wantURL {
				t.Fatalf("unexpected output URL list: %+v", got["output"])
			}
		})
	}
}

func TestEasyAITaskResultResponseDropsBase64ImageOutput(t *testing.T) {
	base64Payload := "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9ZlB8AAAAASUVORK5CYII="
	task := store.GatewayTask{
		ID: "image-base64-1", Kind: "images.generations", Status: "succeeded",
		Result: map[string]any{"data": []any{map[string]any{
			"b64_json":  base64Payload,
			"mime_type": "image/png",
		}}},
	}

	got := easyAITaskResultResponse(task)
	data, _ := got["data"].([]any)
	if len(data) != 0 {
		t.Fatalf("Base64 output leaked into URL-only response: %+v", got)
	}
	urls, _ := got["output"].([]string)
	if len(urls) != 0 {
		t.Fatalf("base64-only output must not create URL aliases: %+v", urls)
	}
}

func TestEasyAITaskResultResponseDropsImageDataURL(t *testing.T) {
	base64Payload := "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9ZlB8AAAAASUVORK5CYII="
	task := store.GatewayTask{
		ID: "image-data-url-1", Kind: "images.generations", Status: "succeeded",
		Result: map[string]any{"data": []any{map[string]any{
			"url": "data:image/png;base64," + base64Payload,
		}}},
	}

	got := easyAITaskResultResponse(task)
	data, _ := got["data"].([]any)
	if len(data) != 0 {
		t.Fatalf("data URL leaked into URL-only response: %+v", got)
	}
}

func TestEasyAITaskResultResponseForwardsSafeUpstreamParameterMessage(t *testing.T) {
	raw := "Duplicate parameter: 'image'. Use image[]=<value> for multiple values."
	legacy := publicerror.Error{
		Code:       "upstream_invalid_request",
		Message:    "The upstream service rejected the request parameters.",
		Category:   "upstream",
		Source:     "upstream",
		HTTPStatus: http.StatusBadRequest,
		Version:    "v1",
	}
	response := easyAITaskResultResponse(store.GatewayTask{
		ID:           "image-error-1",
		Kind:         "images.edits",
		Status:       "failed",
		ErrorCode:    "http_400",
		ErrorMessage: raw,
		PublicError:  &legacy,
	})
	standard, ok := response["error"].(publicerror.Error)
	if !ok {
		t.Fatalf("unexpected EasyAI error payload: %+v", response)
	}
	upstream, _ := standard.Details["upstreamError"].(map[string]any)
	if response["message"] != raw || standard.Message != raw || upstream["message"] != raw {
		t.Fatalf("EasyAI result did not forward safe upstream diagnostics: %+v", response)
	}
}

func TestEasyAITaskResultResponseDropsAudioDataURL(t *testing.T) {
	base64Payload := "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjYwLjMuMTAwAAAAAAAAAAAAAAD/"
	task := store.GatewayTask{
		ID: "audio-data-url-1", Kind: "speech.generations", Status: "succeeded",
		Result: map[string]any{"data": []any{map[string]any{
			"audio_url": "data:audio/mpeg;base64," + base64Payload,
		}}},
	}

	got := easyAITaskResultResponse(task)
	data, _ := got["data"].([]any)
	if len(data) != 0 {
		t.Fatalf("audio data URL leaked into URL-only response: %+v", got)
	}
	urls, _ := got["output"].([]string)
	if len(urls) != 0 {
		t.Fatalf("base64-only audio must not create URL aliases: %+v", urls)
	}
}

func TestEasyAITaskResultResponseMapsVectorFormats(t *testing.T) {
	for _, item := range []struct {
		format   string
		wantType string
	}{
		{format: "svg", wantType: "image"},
		{format: "png", wantType: "image"},
		{format: "pdf", wantType: "file"},
		{format: "eps", wantType: "file"},
	} {
		t.Run(item.format, func(t *testing.T) {
			task := store.GatewayTask{
				ID: "vector-" + item.format, Kind: "images.vectorize", Status: "succeeded",
				Request: map[string]any{"format": item.format},
				Result:  map[string]any{"data": []any{map[string]any{"url": "https://example.com/output." + item.format}}},
			}
			got := easyAITaskResultResponse(task)
			data, _ := got["data"].([]any)
			output, _ := data[0].(map[string]any)
			if output["type"] != item.wantType {
				t.Fatalf("format %s mapped to %v, want %s", item.format, output["type"], item.wantType)
			}
		})
	}
}

func TestEasyAITaskResultResponseClearsFailedOutputs(t *testing.T) {
	task := store.GatewayTask{
		ID: "failed-1", Kind: "videos.upscales", Status: "cancelled",
		ErrorCode: "cancelled", ErrorMessage: "任务已取消",
		Result: map[string]any{"data": []any{map[string]any{
			"url": "https://example.com/partial.mp4",
		}}},
	}
	got := easyAITaskResultResponse(task)
	data, _ := got["data"].([]any)
	outputContent, _ := got["output_content"].([]any)
	if got["status"] != "failed" || got["code"] != "cancelled" || got["message"] != "任务已取消" ||
		len(data) != 0 || len(outputContent) != 0 {
		t.Fatalf("unexpected failed response: %+v", got)
	}
}

func TestEasyAISynchronousVectorAndFileUploadResponsesAreAdditive(t *testing.T) {
	vectorTask := store.GatewayTask{ID: "vector-sync-1", Kind: "images.vectorize"}
	vector := easyAISynchronousTaskResponse(vectorTask, map[string]any{
		"status": "success",
		"data":   []any{map[string]any{"url": "https://example.com/output.svg"}},
	})
	if vector["status"] != "success" || vector["taskId"] != vectorTask.ID || vector["task_id"] != vectorTask.ID {
		t.Fatalf("unexpected vector response: %+v", vector)
	}

	upload := easyAIFileUploadResponse(map[string]any{
		"id": "file-1", "url": "https://example.com/upload.png", "filename": "upload.png",
	})
	data, _ := upload["data"].(map[string]any)
	if upload["status"] != "success" || upload["url"] != data["url"] || upload["id"] != data["id"] {
		t.Fatalf("unexpected upload response: %+v", upload)
	}
}

func TestEasyAIAsyncMediaRequestRequiresExplicitHeader(t *testing.T) {
	syncRequest := httptest.NewRequest(http.MethodPost, "/api/v1/images/generations", nil)
	if easyAIAsyncMediaRequest("images.generations", syncRequest) {
		t.Fatal("image request without X-Async must stay synchronous")
	}
	asyncRequest := httptest.NewRequest(http.MethodPost, "/api/v1/images/generations", nil)
	asyncRequest.Header.Set("X-Async", "true")
	if !easyAIAsyncMediaRequest("images.generations", asyncRequest) {
		t.Fatal("image request with X-Async must enable EasyAI async compatibility")
	}
	if easyAIAsyncMediaRequest("chat.completions", asyncRequest) {
		t.Fatal("chat completions must not enter EasyAI media async compatibility")
	}
}

func TestWriteEasyAIAsyncErrorKeepsNestedGatewayError(t *testing.T) {
	recorder := httptest.NewRecorder()
	writeEasyAIAsyncError(recorder, http.StatusBadRequest, "invalid request", map[string]any{"param": "model"}, "invalid_parameter")

	var got map[string]any
	if err := json.NewDecoder(recorder.Body).Decode(&got); err != nil {
		t.Fatal(err)
	}
	errorPayload, _ := got["error"].(map[string]any)
	data, _ := got["data"].([]any)
	if got["status"] != "failed" || got["code"] != "invalid_parameter" ||
		errorPayload["message"] != "invalid request" || len(data) != 0 {
		t.Fatalf("unexpected async error response: %+v", got)
	}
}
