package httpapi

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

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

func TestNormalizeKelingOmniRequestMapsOfficialFields(t *testing.T) {
	normalized, err := normalizeKelingOmniRequest(map[string]any{
		"model_name":       "kling-v3-omni",
		"prompt":           "A rainy street with natural ambience",
		"mode":             "pro",
		"sound":            "on",
		"aspect_ratio":     "9:16",
		"duration":         "5",
		"external_task_id": "external-1",
		"watermark_info":   map[string]any{"enabled": true},
	})
	if err != nil {
		t.Fatalf("normalize Kling request: %v", err)
	}
	if normalized["model"] != "kling-v3-omni" ||
		normalized["modelType"] != "omni_video" ||
		normalized["resolution"] != "1080p" ||
		normalized["aspect_ratio"] != "9:16" ||
		normalized["duration"] != 5 ||
		normalized["audio"] != true ||
		normalized["sound"] != "on" ||
		normalized["watermark"] != true ||
		normalized["external_task_id"] != "external-1" ||
		normalized["_gateway_compatibility"] != kelingOmniCompatibilityMarker {
		t.Fatalf("unexpected normalized request: %+v", normalized)
	}
	content, _ := normalized["content"].([]any)
	if len(content) != 1 {
		t.Fatalf("unexpected content: %+v", normalized["content"])
	}
	text, _ := content[0].(map[string]any)
	if text["type"] != "text" || text["text"] != "A rainy street with natural ambience" {
		t.Fatalf("unexpected text content: %+v", text)
	}
}

func TestCanonicalTaskModelNameNormalizesKelingOmniAliases(t *testing.T) {
	tests := map[string]string{
		"kling-o1":       "kling-video-o1",
		"kling-video-o1": "kling-video-o1",
		"kling-3.0-omni": "kling-v3-omni",
		"kling-3-omni":   "kling-v3-omni",
		"kling-v3-omni":  "kling-v3-omni",
	}
	for input, expected := range tests {
		if got := canonicalTaskModelName("videos.generations", input); got != expected {
			t.Fatalf("canonicalTaskModelName(%q) = %q, want %q", input, got, expected)
		}
	}
	if got := canonicalTaskModelName("chat.completions", "kling-o1"); got != "kling-o1" {
		t.Fatalf("non-video model must not be rewritten, got %q", got)
	}
}

func TestNormalizeKelingOmniRequestBuildsMultiShotMedia(t *testing.T) {
	normalized, err := normalizeKelingOmniRequest(map[string]any{
		"model_name":   "kling-3.0-omni",
		"multi_shot":   true,
		"shot_type":    "customize",
		"aspect_ratio": "16:9",
		"duration":     5,
		"multi_prompt": []any{
			map[string]any{"index": 1, "prompt": "First shot", "duration": "2"},
			map[string]any{"index": 2, "prompt": "Second shot", "duration": 3},
		},
		"image_list": []any{
			map[string]any{"image_url": "https://example.com/reference.png"},
		},
		"element_list": []any{
			map[string]any{"element_id": float64(123)},
		},
	})
	if err != nil {
		t.Fatalf("normalize multi-shot request: %v", err)
	}
	if normalized["model"] != "kling-v3-omni" || normalized["duration"] != 5 || normalized["multi_shot"] != true || normalized["shot_type"] != "customize" {
		t.Fatalf("unexpected multi-shot fields: %+v", normalized)
	}
	content, _ := normalized["content"].([]any)
	if len(content) != 4 {
		t.Fatalf("expected image, element and two shot prompts, got %+v", content)
	}
}

func TestNormalizeKelingOmniRequestRejectsUnsupportedCombinations(t *testing.T) {
	tests := []struct {
		name string
		body map[string]any
	}{
		{
			name: "callback",
			body: map[string]any{"callback_url": "https://example.com/callback"},
		},
		{
			name: "unknown model",
			body: map[string]any{"model_name": "kling-unknown", "prompt": "x", "aspect_ratio": "16:9"},
		},
		{
			name: "o1 duration",
			body: map[string]any{"model_name": "kling-video-o1", "prompt": "x", "aspect_ratio": "16:9", "duration": 11},
		},
		{
			name: "o1 text to video three seconds",
			body: map[string]any{"model_name": "kling-video-o1", "prompt": "x", "aspect_ratio": "16:9", "duration": 3},
		},
		{
			name: "o1 generated audio",
			body: map[string]any{"model_name": "kling-video-o1", "prompt": "x", "sound": "on", "aspect_ratio": "16:9", "duration": 5},
		},
		{
			name: "video sound",
			body: map[string]any{
				"model_name": "kling-v3-omni",
				"prompt":     "edit",
				"sound":      "on",
				"video_list": []any{map[string]any{"video_url": "https://example.com/base.mp4", "refer_type": "base"}},
			},
		},
		{
			name: "first frame ratio",
			body: map[string]any{
				"prompt":       "animate",
				"aspect_ratio": "16:9",
				"image_list":   []any{map[string]any{"image_url": "https://example.com/first.png", "type": "first_frame"}},
			},
		},
	}
	for _, item := range tests {
		t.Run(item.name, func(t *testing.T) {
			_, err := normalizeKelingOmniRequest(item.body)
			if err == nil || err.Code != 1201 && err.Code != 1203 {
				t.Fatalf("expected official compatibility error, got %v", err)
			}
		})
	}
}

func TestNormalizeKelingO1AllowsThreeSecondsWithReferenceImage(t *testing.T) {
	normalized, err := normalizeKelingOmniRequest(map[string]any{
		"model_name":   "kling-video-o1",
		"prompt":       "Use the landscape as a visual reference",
		"aspect_ratio": "16:9",
		"duration":     3,
		"image_list": []any{
			map[string]any{"image_url": "https://placehold.co/1024x1024/png"},
		},
	})
	if err != nil {
		t.Fatalf("reference-image O1 request should allow three seconds: %v", err)
	}
	if normalized["duration"] != 3 {
		t.Fatalf("unexpected duration: %+v", normalized)
	}
}

func TestKelingCompatTaskDataAndOwnership(t *testing.T) {
	createdAt := time.Unix(100, 0)
	task := store.GatewayTask{
		ID:                "task-1",
		Kind:              "videos.generations",
		GatewayUserID:     "user-1",
		Status:            "succeeded",
		FinalChargeAmount: 2.5,
		Request: map[string]any{
			"_gateway_compatibility": kelingOmniCompatibilityMarker,
			"external_task_id":       "external-1",
			"watermark":              true,
		},
		Result: map[string]any{"data": []any{map[string]any{
			"id":            "video-1",
			"url":           "https://example.com/video.mp4",
			"watermark_url": "https://example.com/watermarked.mp4",
			"duration":      "5",
		}}},
		CreatedAt: createdAt,
		UpdatedAt: createdAt.Add(time.Second),
	}
	if !isKelingCompatTask(task) || !kelingCompatTaskOwnedBy(task, &auth.User{GatewayUserID: "user-1"}) {
		t.Fatalf("expected task ownership and compatibility marker")
	}
	if kelingCompatTaskOwnedBy(task, &auth.User{GatewayUserID: "user-2"}) {
		t.Fatalf("cross-user task access must be rejected")
	}
	data := kelingCompatTaskData(task)
	if data["task_status"] != "succeed" {
		t.Fatalf("unexpected task data: %+v", data)
	}
	if _, ok := data["final_unit_deduction"]; ok {
		t.Fatalf("official compatibility response must not expose billing fields: %+v", data)
	}
	result, _ := data["task_result"].(map[string]any)
	videos, _ := result["videos"].([]any)
	video, _ := videos[0].(map[string]any)
	if video["id"] != "video-1" || video["watermark_url"] != "https://example.com/watermarked.mp4" || video["duration"] != "5" {
		t.Fatalf("unexpected compatible video: %+v", video)
	}
	submission := kelingCompatSubmissionData(task)
	if submission["task_status"] != "submitted" || submission["task_result"] != nil {
		t.Fatalf("submission response must be stable and minimal: %+v", submission)
	}
}

func TestRequireKelingAPIKeyWritesOfficialAuthEnvelope(t *testing.T) {
	server := &Server{auth: auth.New("secret", "", "")}
	server.auth.LocalAPIKeyVerifier = func(_ context.Context, key string) (*auth.User, error) {
		if key != "sk-gw-valid" {
			return nil, auth.ErrUnauthorized
		}
		return &auth.User{ID: "user-1", GatewayUserID: "user-1", APIKeyID: "key-1", APIKeyScopes: []string{"video"}}, nil
	}
	handler := server.requireKelingAPIKey(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if _, ok := auth.UserFromContext(r.Context()); !ok {
			t.Fatal("authenticated user is missing")
		}
		w.WriteHeader(http.StatusNoContent)
	}))

	missing := httptest.NewRecorder()
	handler.ServeHTTP(missing, httptest.NewRequest(http.MethodPost, "/v1/videos/omni-video", nil))
	if missing.Code != http.StatusUnauthorized {
		t.Fatalf("missing auth status=%d body=%s", missing.Code, missing.Body.String())
	}
	var missingBody KelingCompatibleEnvelope
	if err := json.Unmarshal(missing.Body.Bytes(), &missingBody); err != nil || missingBody.Code != 1001 || missingBody.RequestID == "" {
		t.Fatalf("unexpected missing auth envelope: %+v err=%v", missingBody, err)
	}

	invalid := httptest.NewRecorder()
	invalidRequest := httptest.NewRequest(http.MethodPost, "/v1/videos/omni-video", nil)
	invalidRequest.Header.Set("Authorization", "Bearer sk-gw-invalid")
	handler.ServeHTTP(invalid, invalidRequest)
	var invalidBody KelingCompatibleEnvelope
	if err := json.Unmarshal(invalid.Body.Bytes(), &invalidBody); err != nil || invalid.Code != http.StatusUnauthorized || invalidBody.Code != 1002 {
		t.Fatalf("unexpected invalid auth envelope: status=%d body=%+v err=%v", invalid.Code, invalidBody, err)
	}

	valid := httptest.NewRecorder()
	validRequest := httptest.NewRequest(http.MethodPost, "/v1/videos/omni-video", nil)
	validRequest.Header.Set("Authorization", "Bearer sk-gw-valid")
	handler.ServeHTTP(valid, validRequest)
	if valid.Code != http.StatusNoContent {
		t.Fatalf("valid API Key status=%d body=%s", valid.Code, valid.Body.String())
	}
}

func TestKelingCompatErrorImplementsError(t *testing.T) {
	err := newKelingCompatError(http.StatusBadRequest, 1201, "invalid")
	if !errors.Is(err, err) || err.Error() != "invalid" {
		t.Fatalf("unexpected error behavior: %v", err)
	}
}

func TestKelingCompatBusinessCodeMapping(t *testing.T) {
	tests := []struct {
		code    string
		message string
		want    int
	}{
		{code: "insufficient_balance", want: 1101},
		{code: "permission_denied", want: 1103},
		{code: "invalid_parameter", want: 1201},
		{code: "no_model_candidate", want: 1203},
		{code: "rate_limit_exceeded", want: 1302},
		{code: "concurrency_limit", want: 1303},
		{code: "network", want: 5001},
		{code: "unknown", want: 5000},
	}
	for _, item := range tests {
		if got := kelingCompatBusinessCode(item.code, item.message); got != item.want {
			t.Fatalf("code=%q message=%q got=%d want=%d", item.code, item.message, got, item.want)
		}
	}
}

func TestKelingCompatCandidatesSupport4K(t *testing.T) {
	candidates := []store.RuntimeModelCandidate{
		{Provider: "keling", Capabilities: map[string]any{"omni_video": map[string]any{"output_resolutions": []any{"720p", "1080p"}}}},
		{Provider: "keling", Capabilities: map[string]any{"omni_video": map[string]any{"output_resolutions": []any{"2160p"}}}},
	}
	if !kelingCompatCandidatesSupport4K(candidates) {
		t.Fatal("expected 2160p Keling capability to enable mode=4k")
	}
	if kelingCompatCandidatesSupport4K(candidates[:1]) {
		t.Fatal("mode=4k must stay disabled without an explicit capability")
	}
}
