package acceptanceemulator

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"image"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync"
	"testing"
	"time"
)

func TestGeminiAndVolcesProtocolEmulation(t *testing.T) {
	var mu sync.Mutex
	now := time.Unix(1_800_000_000, 0)
	server := New(Config{
		Now: func() time.Time {
			mu.Lock()
			defer mu.Unlock()
			return now
		},
		Wait: func(context.Context, time.Duration) error { return nil },
	})
	httpServer := httptest.NewServer(server.Handler())
	defer httpServer.Close()

	input := paddedPNG(256 << 10)
	geminiBody, _ := json.Marshal(map[string]any{
		"contents": []any{map[string]any{"parts": []any{
			map[string]any{"text": "edit image"},
			map[string]any{"inlineData": map[string]any{
				"mimeType": "image/png", "data": base64.StdEncoding.EncodeToString(input),
			}},
		}}},
		"generationConfig": map[string]any{"responseModalities": []any{"IMAGE"}},
	})
	response, err := postIdempotent(
		httpServer.URL+"/v1beta/models/gemini-test:generateContent",
		geminiBody,
		"gemini-task",
	)
	if err != nil {
		t.Fatalf("Gemini request: %v", err)
	}
	if response.StatusCode != http.StatusOK {
		payload, _ := io.ReadAll(response.Body)
		t.Fatalf("Gemini status=%d body=%s", response.StatusCode, payload)
	}
	var geminiResult map[string]any
	if err := json.NewDecoder(response.Body).Decode(&geminiResult); err != nil {
		t.Fatalf("decode Gemini response: %v", err)
	}
	_ = response.Body.Close()

	fixtureResponse, err := http.Get(httpServer.URL + "/fixtures/image-12-oversized.jpg")
	if err != nil {
		t.Fatalf("get oversized fixture: %v", err)
	}
	fixturePayload, readErr := io.ReadAll(fixtureResponse.Body)
	_ = fixtureResponse.Body.Close()
	if readErr != nil {
		t.Fatalf("read oversized fixture: %v", readErr)
	}
	config, format, err := image.DecodeConfig(bytes.NewReader(fixturePayload))
	if err != nil || format != "jpeg" || config.Width != 6144 || config.Height != 2160 {
		t.Fatalf("oversized fixture format=%s config=%+v err=%v", format, config, err)
	}
	if _, format, err = image.Decode(bytes.NewReader(fixturePayload)); err != nil || format != "jpeg" {
		t.Fatalf("fully decode oversized fixture format=%s err=%v", format, err)
	}

	content := []any{map[string]any{"type": "text", "text": "video"}}
	for index, name := range []string{"image-00.png", "image-04.jpg", "image-08.webp"} {
		role := "reference_image"
		if index == 0 {
			role = "first_frame"
		}
		content = append(content, map[string]any{
			"type": "image_url", "role": role,
			"image_url": map[string]any{"url": httpServer.URL + "/fixtures/" + name},
		})
	}
	videoBody, _ := json.Marshal(map[string]any{"model": "seedance-test", "content": content, "seed": 1})
	response, err = postIdempotent(
		httpServer.URL+"/contents/generations/tasks",
		videoBody,
		"video-task",
	)
	if err != nil {
		t.Fatalf("submit video: %v", err)
	}
	if response.StatusCode != http.StatusOK {
		t.Fatalf("submit video status=%d", response.StatusCode)
	}
	var submitted map[string]any
	_ = json.NewDecoder(response.Body).Decode(&submitted)
	_ = response.Body.Close()
	taskID := strings.TrimSpace(stringValue(submitted["id"]))
	if taskID == "" {
		t.Fatal("video emulator returned no task ID")
	}
	mu.Lock()
	now = now.Add(20 * time.Second)
	mu.Unlock()
	response, err = http.Get(httpServer.URL + "/contents/generations/tasks/" + taskID)
	if err != nil {
		t.Fatalf("poll video: %v", err)
	}
	var polled map[string]any
	_ = json.NewDecoder(response.Body).Decode(&polled)
	_ = response.Body.Close()
	if polled["status"] != "succeeded" {
		t.Fatalf("video status=%v", polled["status"])
	}

	response, err = http.Get(httpServer.URL + "/report")
	if err != nil {
		t.Fatalf("get report: %v", err)
	}
	var report Report
	_ = json.NewDecoder(response.Body).Decode(&report)
	_ = response.Body.Close()
	if report.GeminiRequests != 1 || report.VideoSubmissions != 1 || report.VideoInvalid != 0 ||
		report.VideoReferenceCounts["3"] != 1 || report.UniqueImageHashes != 3 {
		t.Fatalf("unexpected emulator report: %+v", report)
	}
}

func TestForcedConversionRejectsImagesOutsideOfficialSeedanceRange(t *testing.T) {
	server := New(Config{Wait: func(context.Context, time.Duration) error { return nil }})
	httpServer := httptest.NewServer(server.Handler())
	defer httpServer.Close()

	content := []any{map[string]any{"type": "text", "text": "acceptance-force-conversion"}}
	for _, name := range []string{"image-12-oversized.jpg", "image-00.png", "image-04.jpg"} {
		content = append(content, map[string]any{
			"type": "image_url", "role": "reference_image",
			"image_url": map[string]any{"url": httpServer.URL + "/fixtures/" + name},
		})
	}
	body, _ := json.Marshal(map[string]any{"model": "seedance-test", "content": content})
	response, err := postIdempotent(httpServer.URL+"/contents/generations/tasks", body, "oversized")
	if err != nil {
		t.Fatal(err)
	}
	defer response.Body.Close()
	if response.StatusCode != http.StatusBadRequest {
		t.Fatalf("status=%d, want %d", response.StatusCode, http.StatusBadRequest)
	}
}

func TestGeminiProtocolAcceptsMultipleFileDataImages(t *testing.T) {
	server := New(Config{Wait: func(context.Context, time.Duration) error { return nil }})
	httpServer := httptest.NewServer(server.Handler())
	defer httpServer.Close()
	parts := []any{map[string]any{"text": "combine references"}}
	for index := 0; index < 3; index++ {
		parts = append(parts, map[string]any{"fileData": map[string]any{
			"mimeType": "image/png",
			"fileUri":  fmt.Sprintf("https://fixtures.example/%d.png", index),
		}})
	}
	body, _ := json.Marshal(map[string]any{
		"contents":         []any{map[string]any{"parts": parts}},
		"generationConfig": map[string]any{"responseModalities": []any{"IMAGE"}},
	})
	response, err := postIdempotent(httpServer.URL+"/v1beta/models/gemini-test:generateContent", body, "gemini-multi")
	if err != nil {
		t.Fatalf("Gemini multi-image request: %v", err)
	}
	defer response.Body.Close()
	if response.StatusCode != http.StatusOK {
		payload, _ := io.ReadAll(response.Body)
		t.Fatalf("Gemini multi-image status=%d body=%s", response.StatusCode, payload)
	}
	reportResponse, err := http.Get(httpServer.URL + "/report")
	if err != nil {
		t.Fatalf("get report: %v", err)
	}
	defer reportResponse.Body.Close()
	var report Report
	if err := json.NewDecoder(reportResponse.Body).Decode(&report); err != nil {
		t.Fatalf("decode report: %v", err)
	}
	if report.GeminiRequests != 1 || report.GeminiInputImages != 3 || report.GeminiInvalid != 0 {
		t.Fatalf("unexpected multi-image report: %+v", report)
	}
}

func TestImageFixturesFullyDecode(t *testing.T) {
	for name, fixture := range buildFixtures() {
		if !strings.HasPrefix(fixture.ContentType, "image/") {
			continue
		}
		decoded, format, err := image.Decode(bytes.NewReader(fixture.Payload))
		if err != nil {
			t.Fatalf("fixture %s does not fully decode: %v", name, err)
		}
		bounds := decoded.Bounds()
		if format == "" || bounds.Dx() <= 0 || bounds.Dy() <= 0 {
			t.Fatalf("fixture %s format=%q bounds=%v", name, format, bounds)
		}
	}
}

func TestPaddedPNGIsValidAndExact(t *testing.T) {
	for _, size := range []int{256 << 10, 4 << 20, 8 << 20} {
		payload := paddedPNG(size)
		if len(payload) != size {
			t.Fatalf("PNG bytes=%d, want=%d", len(payload), size)
		}
		if _, _, err := image.DecodeConfig(bytes.NewReader(payload)); err != nil {
			t.Fatalf("decode padded PNG: %v", err)
		}
	}
}

func TestScaledDelayStaysOnWholeSecondsWithinProfile(t *testing.T) {
	delay := scaledDelay(8*time.Second, 15*time.Second, 2<<20)
	if delay < 8*time.Second || delay > 15*time.Second || delay%time.Second != 0 {
		t.Fatalf("scaled delay=%s", delay)
	}
}

func TestVideoCapacityDelay(t *testing.T) {
	body := map[string]any{
		"seed": json.Number("7"),
		"content": []any{map[string]any{
			"type": "text",
			"text": "capacity acceptance-capacity-ladder",
		}},
	}
	delay := videoDelay(body, false)
	if delay < 30*time.Second || delay > 45*time.Second {
		t.Fatalf("video capacity delay=%s, want 30s..45s", delay)
	}
}

func TestVideoProviderQuotaDelayAndUsage(t *testing.T) {
	body := map[string]any{
		"seed": json.Number("5"),
		"content": []any{map[string]any{
			"type": "text",
			"text": "acceptance-provider-quota",
		}},
	}
	delay := videoDelay(body, false)
	if delay < 5*time.Second || delay > 10*time.Second {
		t.Fatalf("video provider quota delay=%s, want 5s..10s", delay)
	}
	if tokens := videoUsageTokens(body); tokens != 7 {
		t.Fatalf("video provider quota usage tokens=%d, want 7", tokens)
	}
}

func TestVolcesProtocolAcceptsThreeSixAndNineReferenceImages(t *testing.T) {
	server := New(Config{Wait: func(context.Context, time.Duration) error { return nil }})
	httpServer := httptest.NewServer(server.Handler())
	defer httpServer.Close()
	dataImage := "data:image/png;base64," + base64.StdEncoding.EncodeToString(paddedPNG(0))

	for _, imageCount := range []int{3, 6, 9} {
		content := []any{map[string]any{"type": "text", "text": "multi-reference video"}}
		for index := 0; index < imageCount; index++ {
			role := "reference_image"
			if index == 0 {
				role = "first_frame"
			}
			if imageCount > 3 && index == imageCount-1 {
				role = "last_frame"
			}
			imageURL := fmt.Sprintf("%s/fixtures/image-%02d.png", httpServer.URL, index%4)
			if index%2 == 0 {
				imageURL = dataImage
			}
			content = append(content, map[string]any{
				"type": "image_url", "role": role,
				"image_url": map[string]any{"url": imageURL},
			})
		}
		body, _ := json.Marshal(map[string]any{"model": "seedance-test", "content": content, "seed": imageCount})
		response, err := postIdempotent(
			httpServer.URL+"/contents/generations/tasks",
			body,
			fmt.Sprintf("video-%d", imageCount),
		)
		if err != nil {
			t.Fatalf("%d-image submit: %v", imageCount, err)
		}
		_ = response.Body.Close()
		if response.StatusCode != http.StatusOK {
			t.Fatalf("%d-image status=%d", imageCount, response.StatusCode)
		}
	}

	response, err := http.Get(httpServer.URL + "/report")
	if err != nil {
		t.Fatalf("get report: %v", err)
	}
	defer response.Body.Close()
	var report Report
	if err := json.NewDecoder(response.Body).Decode(&report); err != nil {
		t.Fatalf("decode report: %v", err)
	}
	for _, imageCount := range []string{"3", "6", "9"} {
		if report.VideoReferenceCounts[imageCount] != 1 {
			t.Fatalf("reference counts=%v", report.VideoReferenceCounts)
		}
	}
}

func TestObjectStorageEmulationSupportsRetryLifecycleAndFaults(t *testing.T) {
	server := httptest.NewServer(New(Config{}).Handler())
	defer server.Close()
	objectURL := server.URL + "/storage/s3-transient/bucket/media/result.png"

	request, _ := http.NewRequest(http.MethodPut, objectURL, strings.NewReader("image-bytes"))
	request.Header.Set("Content-Type", "image/png")
	response, err := http.DefaultClient.Do(request)
	if err != nil {
		t.Fatal(err)
	}
	_ = response.Body.Close()
	if response.StatusCode != http.StatusServiceUnavailable {
		t.Fatalf("first transient PUT status=%d", response.StatusCode)
	}
	request, _ = http.NewRequest(http.MethodPut, objectURL, strings.NewReader("image-bytes"))
	request.Header.Set("Content-Type", "image/png")
	response, err = http.DefaultClient.Do(request)
	if err != nil {
		t.Fatal(err)
	}
	_ = response.Body.Close()
	if response.StatusCode != http.StatusOK {
		t.Fatalf("retried PUT status=%d", response.StatusCode)
	}

	response, err = http.Get(objectURL)
	if err != nil {
		t.Fatal(err)
	}
	payload, _ := io.ReadAll(response.Body)
	_ = response.Body.Close()
	if response.StatusCode != http.StatusOK || string(payload) != "image-bytes" {
		t.Fatalf("GET status=%d payload=%q", response.StatusCode, payload)
	}
	request, _ = http.NewRequest(http.MethodHead, objectURL, nil)
	response, err = http.DefaultClient.Do(request)
	if err != nil {
		t.Fatal(err)
	}
	_ = response.Body.Close()
	if response.StatusCode != http.StatusOK {
		t.Fatalf("HEAD status=%d", response.StatusCode)
	}
	request, _ = http.NewRequest(http.MethodDelete, objectURL, nil)
	response, err = http.DefaultClient.Do(request)
	if err != nil {
		t.Fatal(err)
	}
	_ = response.Body.Close()
	if response.StatusCode != http.StatusNoContent {
		t.Fatalf("DELETE status=%d", response.StatusCode)
	}

	for profile, wantStatus := range map[string]int{"oss-auth": http.StatusForbidden, "s3-fail": http.StatusServiceUnavailable} {
		request, _ = http.NewRequest(http.MethodPut, server.URL+"/storage/"+profile+"/probe.bin", strings.NewReader("probe"))
		response, err = http.DefaultClient.Do(request)
		if err != nil {
			t.Fatal(err)
		}
		_ = response.Body.Close()
		if response.StatusCode != wantStatus {
			t.Fatalf("%s status=%d, want %d", profile, response.StatusCode, wantStatus)
		}
	}

	response, err = http.Get(server.URL + "/report")
	if err != nil {
		t.Fatal(err)
	}
	defer response.Body.Close()
	var report Report
	if err := json.NewDecoder(response.Body).Decode(&report); err != nil {
		t.Fatal(err)
	}
	if report.StoragePuts != 1 || report.StorageGets != 1 || report.StorageHeads != 1 || report.StorageDeletes != 1 || report.StorageFailures != 3 {
		t.Fatalf("unexpected storage report: %+v", report)
	}
}

func postIdempotent(url string, body []byte, key string) (*http.Response, error) {
	request, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
	if err != nil {
		return nil, err
	}
	request.Header.Set("Content-Type", "application/json")
	request.Header.Set("Idempotency-Key", key)
	return http.DefaultClient.Do(request)
}
