package runner

import (
	"bytes"
	"context"
	"encoding/base64"
	"net/http"
	"net/http/httptest"
	"os"
	"strings"
	"testing"
	"time"

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

func TestGeneratedAssetDecisionSkipsURLResultAndStripsInlinePayload(t *testing.T) {
	item := map[string]any{
		"b64_json": base64.StdEncoding.EncodeToString([]byte("inline image")),
		"url":      "https://cdn.example.com/generated.png",
	}

	decision, err := generatedAssetDecisionForItem("images.generations", item, defaultGeneratedAssetUploadPolicy())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if decision.Inline != nil {
		t.Fatalf("URL media should not be uploaded by the default policy")
	}
	if !containsString(decision.StripKeys, "b64_json") {
		t.Fatalf("inline payload should be stripped when URL is already available: %+v", decision.StripKeys)
	}
}

func TestGeneratedAssetDecisionUploadsInlineImageBase64(t *testing.T) {
	item := map[string]any{
		"b64_json":  base64.StdEncoding.EncodeToString([]byte("inline image")),
		"mime_type": "image/jpeg",
	}

	decision, err := generatedAssetDecisionForItem("images.generations", item, defaultGeneratedAssetUploadPolicy())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if decision.Inline == nil {
		t.Fatalf("expected inline image to be uploaded")
	}
	if decision.Inline.Kind != "image" || decision.Inline.ContentType != "image/jpeg" {
		t.Fatalf("unexpected inline image metadata: %+v", decision.Inline)
	}
	if !containsString(decision.StripKeys, "b64_json") {
		t.Fatalf("uploaded inline payload should be stripped: %+v", decision.StripKeys)
	}
}

func TestUploadGeneratedAssetsReusesDuplicateSHAWithinTask(t *testing.T) {
	putCount := 0
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPut {
			http.Error(w, "unexpected method", http.StatusMethodNotAllowed)
			return
		}
		putCount++
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()
	service := &Service{}
	service.directOSS = &directOSSUploader{
		endpoint: server.URL, bucket: "bucket", accessKeyID: "access-id", accessKeySecret: "access-secret", objectPrefix: "media",
	}
	payload := append([]byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a}, bytes.Repeat([]byte{0}, 32)...)
	encoded := base64.StdEncoding.EncodeToString(payload)
	result := map[string]any{"data": []any{
		map[string]any{"b64_json": encoded, "mime_type": "image/png"},
		map[string]any{"b64_json": encoded, "mime_type": "image/png"},
	}}

	stored, err := service.uploadGeneratedAssets(t.Context(), "task-duplicate", "images.generations", "", result)
	if err != nil {
		t.Fatal(err)
	}
	items := stored["data"].([]any)
	first := items[0].(map[string]any)
	second := items[1].(map[string]any)
	if putCount != 1 || stringFromAny(first["url"]) == "" || first["url"] != second["url"] {
		t.Fatalf("duplicate SHA was not reused: puts=%d first=%#v second=%#v", putCount, first, second)
	}
	if TaskResultHasInlineBinary(stored) || first["b64_json"] != nil || second["b64_json"] != nil {
		t.Fatalf("inline payload remained after deduplicated upload: %#v", stored)
	}
}

func TestMediaResultMaterializationConcurrencyIsBounded(t *testing.T) {
	service := New(config.Config{MediaMaterializationConcurrency: 1}, nil, nil)
	result := map[string]any{
		"data": []any{map[string]any{
			"b64_json":  base64.StdEncoding.EncodeToString([]byte("inline image")),
			"mime_type": "image/png",
		}},
	}
	release, err := service.acquireMediaResultSlot(context.Background(), result)
	if err != nil {
		t.Fatalf("acquire first media result slot: %v", err)
	}

	waitCtx, cancel := context.WithTimeout(context.Background(), 25*time.Millisecond)
	defer cancel()
	if _, err := service.acquireMediaResultSlot(waitCtx, result); err == nil {
		t.Fatal("second media result materialization bypassed configured concurrency")
	}
	release()

	release, err = service.acquireMediaResultSlot(context.Background(), result)
	if err != nil {
		t.Fatalf("acquire released media result slot: %v", err)
	}
	release()
}

func TestMediaTaskPreacquiresMaterializationSlotForInlineResultTypes(t *testing.T) {
	t.Parallel()
	for _, modelType := range []string{
		"image_generate",
		"image_edit",
		"audio_generate",
		"text_to_speech",
		"voice_clone",
		"omni",
	} {
		if !mediaTaskNeedsPreUpstreamMaterializationSlot(modelType) {
			t.Fatalf("%s should preacquire the media materialization slot", modelType)
		}
	}
	for _, modelType := range []string{
		"text_generate",
		"text_embedding",
		"text_rerank",
		"video_generate",
		"image_to_video",
	} {
		if mediaTaskNeedsPreUpstreamMaterializationSlot(modelType) {
			t.Fatalf("%s should not hold a media materialization slot across the upstream call", modelType)
		}
	}
}

func TestGeneratedAssetDecisionUploadsVectorDocumentWithoutChangingType(t *testing.T) {
	item := map[string]any{
		"type":      "file",
		"b64_json":  base64.StdEncoding.EncodeToString([]byte("%PDF-1.7\n")),
		"mime_type": "application/pdf",
	}
	decision, err := generatedAssetDecisionForItem("images.vectorize", item, defaultGeneratedAssetUploadPolicy())
	if err != nil {
		t.Fatal(err)
	}
	if decision.Inline == nil || decision.Inline.Kind != "file" || decision.Inline.ContentType != "application/pdf" {
		t.Fatalf("unexpected vector document decision: %+v", decision)
	}
	contentType := resolvedGeneratedAssetContentType(decision.Inline.ContentType, decision.Inline.Kind, decision.Inline.Bytes)
	if contentType != "application/pdf" || fileExtensionForContentType(contentType, "file") != ".pdf" {
		t.Fatalf("vector document type changed: contentType=%s", contentType)
	}
}

func TestGeneratedAssetDecisionUploadsInlineVideoBuffer(t *testing.T) {
	item := map[string]any{
		"type":         "video",
		"video_buffer": []any{float64(0), float64(1), float64(2), float64(3)},
	}

	decision, err := generatedAssetDecisionForItem("videos.generations", item, defaultGeneratedAssetUploadPolicy())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if decision.Inline == nil {
		t.Fatalf("expected inline video buffer to be uploaded")
	}
	if decision.Inline.Kind != "video" || decision.Inline.ContentType != "video/mp4" {
		t.Fatalf("unexpected inline video metadata: %+v", decision.Inline)
	}
	if !containsString(decision.StripKeys, "video_buffer") {
		t.Fatalf("uploaded video buffer should be stripped: %+v", decision.StripKeys)
	}
}

func TestGeneratedAssetDecisionUploadsDataURL(t *testing.T) {
	item := map[string]any{
		"url": "data:image/webp;base64," + base64.StdEncoding.EncodeToString([]byte("inline webp")),
	}

	decision, err := generatedAssetDecisionForItem("images.generations", item, defaultGeneratedAssetUploadPolicy())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if decision.Inline == nil {
		t.Fatalf("expected data URL to be uploaded")
	}
	if decision.Inline.SourceKey != "url" || decision.Inline.ContentType != "image/webp" {
		t.Fatalf("unexpected data URL metadata: %+v", decision.Inline)
	}
	if !containsString(decision.StripKeys, "url") {
		t.Fatalf("uploaded data URL field should be stripped: %+v", decision.StripKeys)
	}
}

func TestGeneratedAssetDecisionUploadsAudioContentDataURL(t *testing.T) {
	item := map[string]any{
		"type":      "audio",
		"content":   "data:audio/mpeg;base64," + base64.StdEncoding.EncodeToString([]byte("inline audio")),
		"mime_type": "audio/mpeg",
	}

	decision, err := generatedAssetDecisionForItem("speech.generations", item, defaultGeneratedAssetUploadPolicy())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if decision.Inline == nil {
		t.Fatalf("expected inline audio content to be uploaded")
	}
	if decision.Inline.Kind != "audio" || decision.Inline.ContentType != "audio/mpeg" || decision.Inline.SourceKey != "content" {
		t.Fatalf("unexpected inline audio metadata: %+v", decision.Inline)
	}
	if !containsString(decision.StripKeys, "content") {
		t.Fatalf("uploaded audio content should be stripped: %+v", decision.StripKeys)
	}
}

func TestGeneratedAssetDecisionDoesNotUploadPlainTextContent(t *testing.T) {
	item := map[string]any{
		"type":    "text",
		"content": base64.StdEncoding.EncodeToString([]byte(strings.Repeat("plain text ", 20))),
	}

	decision, err := generatedAssetDecisionForItem("speech.generations", item, defaultGeneratedAssetUploadPolicy())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if decision.Inline != nil || len(decision.StripKeys) > 0 {
		t.Fatalf("plain text content should not be treated as generated media: %+v", decision)
	}
}

func TestGeneratedAssetDecisionUploadsURLWhenPolicyUploadAll(t *testing.T) {
	item := map[string]any{
		"type":      "video",
		"video_url": "https://cdn.example.com/generated.mp4",
	}

	decision, err := generatedAssetDecisionForItem("videos.generations", item, generatedAssetUploadPolicy{UploadInlineMedia: true, UploadURLMedia: true})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if decision.URL == nil {
		t.Fatalf("expected URL media to be uploaded")
	}
	if decision.URL.Kind != "video" || decision.URL.SourceKey != "video_url" {
		t.Fatalf("unexpected URL media metadata: %+v", decision.URL)
	}
	if !containsString(decision.StripKeys, "video_url") {
		t.Fatalf("uploaded URL field should be stripped: %+v", decision.StripKeys)
	}
}

func TestGeneratedAssetDecisionRejectsUnsupportedURLScheme(t *testing.T) {
	_, err := generatedAssetDecisionForItem("images.generations", map[string]any{
		"url": "ftp://files.example/result.png", "type": "image",
	}, defaultGeneratedAssetUploadPolicy())
	if clients.ErrorCode(err) != "invalid_upstream_result" {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestGeneratedAssetUploadPolicyFromName(t *testing.T) {
	tests := []struct {
		name       string
		policyName string
		want       generatedAssetUploadPolicy
	}{
		{
			name:       "default",
			policyName: store.FileStorageResultUploadPolicyDefault,
			want:       generatedAssetUploadPolicy{UploadInlineMedia: true, UploadURLMedia: false, PreserveInlineMedia: false},
		},
		{
			name:       "upload all",
			policyName: store.FileStorageResultUploadPolicyUploadAll,
			want:       generatedAssetUploadPolicy{UploadInlineMedia: true, UploadURLMedia: true, PreserveInlineMedia: false},
		},
		{
			name:       "legacy upload none becomes default",
			policyName: store.FileStorageResultUploadPolicyUploadNone,
			want:       generatedAssetUploadPolicy{UploadInlineMedia: true, UploadURLMedia: false, PreserveInlineMedia: false},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := generatedAssetUploadPolicyFromName(tt.policyName)
			if got != tt.want {
				t.Fatalf("unexpected policy: got %+v, want %+v", got, tt.want)
			}
		})
	}
}

func TestAcceptanceRunForcesURLMaterializationOnlyForAcceptanceTask(t *testing.T) {
	configured := generatedAssetUploadPolicy{UploadInlineMedia: true}
	if got := generatedAssetUploadPolicyForAcceptanceRun(configured, ""); got.UploadURLMedia {
		t.Fatal("ordinary task unexpectedly enabled URL materialization")
	}
	got := generatedAssetUploadPolicyForAcceptanceRun(configured, "acceptance-run-id")
	if !got.UploadInlineMedia || !got.UploadURLMedia {
		t.Fatalf("acceptance policy=%+v", got)
	}
}

func TestAcceptanceGeneratedMediaAllowsOnlyExactEmulatorOrigin(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "video/mp4")
		_, _ = w.Write([]byte{0, 0, 0, 16, 'f', 't', 'y', 'p', 'i', 's', 'o', 'm'})
	}))
	defer server.Close()
	service := &Service{}
	asset := &generatedURLAsset{URL: server.URL + "/media/result.mp4", Kind: "video"}
	payload, contentType, err := service.readGeneratedURLAsset(t.Context(), asset, server.URL+"/v1")
	if err != nil {
		t.Fatalf("read exact acceptance origin: %v", err)
	}
	if len(payload) != 12 || contentType != "video/mp4" {
		t.Fatalf("payload=%d contentType=%q", len(payload), contentType)
	}
	for _, allowed := range []string{
		strings.Replace(server.URL, "http://", "https://", 1),
		server.URL + "1",
		strings.Replace(server.URL, "http://", "http://user@", 1),
	} {
		if sameHTTPOrigin(asset.URL, allowed) {
			t.Fatalf("unexpected origin match: %q", allowed)
		}
	}
}

func TestFinalizeGeneratedAssetsUploadsNestedInlineBinaryUnderDefaultPolicy(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }))
	defer server.Close()
	service := &Service{}
	channels := []store.FileStorageChannel{testObjectStorageChannel("s3", server.URL, "s3-result")}
	payload := append([]byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a}, bytes.Repeat([]byte{0}, 160)...)
	encoded := base64.StdEncoding.EncodeToString(payload)
	result := map[string]any{
		"id": "image-edit",
		"provider_result": map[string]any{
			"binary_data_base64": encoded,
			"mime_type":          "image/png",
		},
	}

	finalized, err := service.finalizeGeneratedAssets(
		context.Background(),
		"task-nested-binary",
		"images.edits",
		result,
		defaultGeneratedAssetUploadPolicy(),
		channels,
		true,
		0,
	)
	if err != nil {
		t.Fatalf("finalize generated assets: %v", err)
	}
	if TaskResultHasInlineBinary(finalized) {
		t.Fatal("finalized result still contains inline binary")
	}
	nested := finalized["provider_result"].(map[string]any)
	if _, exists := nested["binary_data_base64"]; exists {
		t.Fatalf("nested Base64 field was retained: %+v", nested)
	}
	if urlValue := stringFromAny(nested["url"]); !strings.HasPrefix(urlValue, "https://cdn.example.com/media/image_result/") {
		t.Fatalf("unexpected object storage URL: %s", urlValue)
	}
	if nested["upload"] == nil {
		t.Fatalf("nested upload metadata was not retained: %+v", nested)
	}
}

func TestFinalizeGeneratedAssetsMaterializesDetectedMediaAndKeepsOpaqueMetadata(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }))
	defer server.Close()
	service := &Service{}
	channels := []store.FileStorageChannel{testObjectStorageChannel("s3", server.URL, "s3-result")}
	payload := append([]byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a}, bytes.Repeat([]byte{0}, 4096)...)
	opaque := base64.StdEncoding.EncodeToString([]byte(strings.Repeat("opaque metadata ", 400)))
	result := map[string]any{
		"provider_payload":  base64.StdEncoding.EncodeToString(payload),
		"thought_signature": opaque,
		"thinking_bytes":    opaque,
	}

	finalized, err := service.finalizeGeneratedAssets(
		t.Context(),
		"task-provider-payload",
		"images.generations",
		result,
		defaultGeneratedAssetUploadPolicy(),
		channels,
		true,
		0,
	)
	if err != nil {
		t.Fatal(err)
	}
	if TaskResultHasInlineBinary(finalized) {
		t.Fatalf("finalized result still contains generated media: %#v", finalized)
	}
	reference, ok := finalized["provider_payload"].(map[string]any)
	if !ok || reference["url"] == nil || reference["upload"] == nil || reference["assetRef"] != nil {
		t.Fatalf("detected media was not objectified: %#v", finalized["provider_payload"])
	}
	if finalized["thought_signature"] != opaque {
		t.Fatal("opaque provider metadata must remain unchanged")
	}
	if finalized["thinking_bytes"] != opaque {
		t.Fatal("opaque provider bytes metadata must remain unchanged")
	}
}

func TestFinalizeGeneratedAssetsMaterializesGeminiImageData(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }))
	defer server.Close()
	service := &Service{}
	channels := []store.FileStorageChannel{testObjectStorageChannel("s3", server.URL, "s3-result")}
	payload := append([]byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a}, bytes.Repeat([]byte{0}, 256)...)
	result := map[string]any{
		"id":      "gemini-image",
		"created": float64(1),
		"model":   "gemini-3-pro-image",
		"data": []any{map[string]any{
			"b64_json":  base64.StdEncoding.EncodeToString(payload),
			"mime_type": "image/png",
		}},
	}

	finalized, err := service.finalizeGeneratedAssets(
		t.Context(), "task-gemini-image", "images.generations", result,
		defaultGeneratedAssetUploadPolicy(), channels, true, 0,
	)
	if err != nil {
		t.Fatal(err)
	}
	if TaskResultHasInlineBinary(finalized) {
		t.Fatalf("Gemini image result still contains inline binary: %+v", taskResultInlineBinaryDiagnostics(finalized))
	}
	data := finalized["data"].([]any)
	item := data[0].(map[string]any)
	if _, exists := item["b64_json"]; exists {
		t.Fatalf("Gemini b64_json was retained: %#v", item)
	}
	if stringFromAny(item["url"]) == "" || item["upload"] == nil || item["assetRef"] != nil {
		t.Fatalf("Gemini b64_json was not replaced by a URL result: %#v", item)
	}
}

func TestResolvedGeneratedAssetContentTypePrefersDetectedMedia(t *testing.T) {
	pngPayload := []byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 0}

	contentType := resolvedGeneratedAssetContentType("image/jpeg", "image", pngPayload)
	if contentType != "image/png" {
		t.Fatalf("expected detected PNG content type, got %s", contentType)
	}
	if extension := fileExtensionForContentType(contentType, "image"); extension != ".png" {
		t.Fatalf("expected PNG extension, got %s", extension)
	}
}

func TestResolvedGeneratedAssetContentTypeKeepsDeclaredMediaWhenDetectionIsGeneric(t *testing.T) {
	contentType := resolvedGeneratedAssetContentType("image/webp", "image", []byte("not enough media bytes"))
	if contentType != "image/webp" {
		t.Fatalf("expected declared webp content type, got %s", contentType)
	}
}

func TestGeneratedAssetFileNameIsUniqueAndTyped(t *testing.T) {
	first := generatedAssetFileName("663e19cd4fa9d8078385c7c9", 0, "image/png", "image")
	second := generatedAssetFileName("663e19cd4fa9d8078385c7c9", 0, "image/png", "image")
	if first == second {
		t.Fatalf("expected generated file names to be unique, both were %s", first)
	}
	if !strings.HasPrefix(first, "gateway-result-663e19cd4fa9d8078385c7c9-01-") || !strings.HasSuffix(first, ".png") {
		t.Fatalf("unexpected generated file name: %s", first)
	}
}

func TestUploadGeneratedAssetFailsWithoutObjectStorage(t *testing.T) {
	service := &Service{}
	payload := []byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 0}
	asset := &generatedInlineAsset{
		Bytes:       payload,
		ContentType: "image/jpeg",
		Kind:        "image",
		SourceKey:   "b64_json",
	}

	_, _, _, _, err := service.uploadGeneratedAsset(context.Background(), "task-123", asset, 0, nil)
	if clients.ErrorCode(err) != "storage_write_failed" {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestUploadGeneratedAudioFailsWithoutObjectStorage(t *testing.T) {
	service := &Service{}
	asset := &generatedInlineAsset{
		Bytes:       []byte("inline audio payload"),
		ContentType: "audio/mpeg",
		Kind:        "audio",
		SourceKey:   "content",
	}

	_, _, _, _, err := service.uploadGeneratedAsset(context.Background(), "task-tts", asset, 0, nil)
	if clients.ErrorCode(err) != "storage_write_failed" {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestUploadGeneratedRawMediaValueReplacesGeminiInlineDataWithURL(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }))
	defer server.Close()
	service := &Service{}
	channels := []store.FileStorageChannel{testObjectStorageChannel("s3", server.URL, "s3-result")}
	payload := append([]byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a}, bytes.Repeat([]byte{0}, 160)...)
	raw := map[string]any{
		"candidates": []any{
			map[string]any{
				"content": map[string]any{
					"parts": []any{
						map[string]any{
							"inlineData": map[string]any{
								"mimeType": "image/png",
								"data":     base64.StdEncoding.EncodeToString(payload),
							},
						},
					},
				},
			},
		},
	}
	index := 0

	uploaded, changed, err := service.uploadGeneratedRawMediaValue(context.Background(), "task-raw", "chat.completions", raw, "", nil, defaultGeneratedAssetUploadPolicy(), channels, &index)
	if err != nil {
		t.Fatalf("upload raw media: %v", err)
	}
	if !changed {
		t.Fatal("expected raw inlineData to be replaced")
	}
	uploadedRaw := uploaded.(map[string]any)
	candidates := uploadedRaw["candidates"].([]any)
	candidate := candidates[0].(map[string]any)
	content := candidate["content"].(map[string]any)
	parts := content["parts"].([]any)
	part := parts[0].(map[string]any)
	inlineData := part["inlineData"].(map[string]any)
	data, ok := inlineData["data"].(map[string]any)
	if !ok {
		t.Fatalf("inlineData.data should be an asset reference, got %+v", inlineData["data"])
	}
	upload, _ := data["upload"].(map[string]any)
	if upload["sha256"] == "" || upload["contentType"] != "image/png" || upload["size"] != len(payload) || data["assetRef"] != nil {
		t.Fatalf("unexpected URL storage metadata: %+v", data)
	}
	if urlValue := stringFromAny(data["url"]); !strings.HasPrefix(urlValue, "https://cdn.example.com/media/image_result/") || !strings.HasSuffix(urlValue, ".png") {
		t.Fatalf("unexpected raw media URL: %s", urlValue)
	}
	if inlineData["data"] == base64.StdEncoding.EncodeToString(payload) {
		t.Fatal("raw inlineData still contains base64 payload")
	}
}

func TestUploadGeneratedRawMediaValueReplacesBufferAndBytesWithURLs(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }))
	defer server.Close()
	service := &Service{}
	channels := []store.FileStorageChannel{testObjectStorageChannel("s3", server.URL, "s3-buffer-result")}
	raw := map[string]any{
		"buffer": map[string]any{
			"type": "Buffer", "mimeType": "image/png", "data": []any{float64(0x89), float64('P'), float64('N'), float64('G')},
		},
		"audio_bytes": []any{float64('I'), float64('D'), float64('3')},
		"direct":      []byte("direct bytes"),
	}
	index := 0
	uploaded, changed, err := service.uploadGeneratedRawMediaValue(t.Context(), "task-buffer", "images.generations", raw, "", nil, defaultGeneratedAssetUploadPolicy(), channels, &index)
	if err != nil {
		t.Fatal(err)
	}
	if !changed || index != 3 {
		t.Fatalf("changed=%v uploads=%d", changed, index)
	}
	next := uploaded.(map[string]any)
	for _, key := range []string{"buffer", "audio_bytes", "direct"} {
		item, ok := next[key].(map[string]any)
		if !ok || item["url"] == nil || item["upload"] == nil || item["assetRef"] != nil {
			t.Fatalf("%s was not objectified: %#v", key, next[key])
		}
	}
	if TaskResultHasInlineBinary(next) {
		t.Fatalf("objectified result still contains inline binary: %#v", next)
	}
}

func TestUploadFileFailsWithoutObjectStorageAndCreatesNoLocalFile(t *testing.T) {
	storageDir := t.TempDir()
	service := &Service{cfg: config.Config{
		LocalUploadedStorageDir: storageDir,
		ServerMainBaseURL:       "http://127.0.0.1:1",
		ServerMainInternalToken: "change-me",
	}}
	payload := []byte("%PDF-1.4")

	_, err := service.UploadFile(context.Background(), FileUploadPayload{
		Bytes:       payload,
		ContentType: "application/pdf",
		FileName:    "用户文件.png",
		Source:      "playground",
	})
	if clients.ErrorCode(err) != "storage_write_failed" {
		t.Fatalf("unexpected error: %v", err)
	}
	entries, err := os.ReadDir(storageDir)
	if err != nil {
		t.Fatalf("failed to read uploaded static dir: %v", err)
	}
	if len(entries) != 0 {
		t.Fatalf("unexpected local files: %+v", entries)
	}
}

func TestRedactGeneratedResultRawDataAudioPayload(t *testing.T) {
	audioHex := strings.Repeat("49443304", 40)
	result := map[string]any{
		"raw_data": map[string]any{
			"base_resp": map[string]any{"status_code": float64(0)},
			"data":      map[string]any{"audio": audioHex},
		},
	}

	if !redactGeneratedResultRawData(result) {
		t.Fatalf("expected raw audio payload to be redacted")
	}
	rawData, _ := result["raw_data"].(map[string]any)
	data, _ := rawData["data"].(map[string]any)
	audio, _ := data["audio"].(map[string]any)
	if audio["redacted"] != true || audio["encoding"] != "hex" || audio["contentType"] != "audio/mpeg" {
		t.Fatalf("unexpected redacted audio payload: %+v", audio)
	}
	if _, ok := audio["sha256"].(string); !ok {
		t.Fatalf("expected redacted audio payload to include sha256: %+v", audio)
	}
}
