package attachment

import (
	"bytes"
	"context"
	"image"
	"image/color"
	"image/gif"
	"image/jpeg"
	"image/png"
	"testing"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/objectstorage"
)

func encodeTestPNG(t *testing.T, width int, height int, fill color.RGBA) []byte {
	t.Helper()
	src := image.NewRGBA(image.Rect(0, 0, width, height))
	for y := 0; y < height; y++ {
		for x := 0; x < width; x++ {
			src.Set(x, y, fill)
		}
	}
	var raw bytes.Buffer
	if err := png.Encode(&raw, src); err != nil {
		t.Fatalf("encode png source: %v", err)
	}
	return raw.Bytes()
}

func encodeTestJPEG(t *testing.T, width int, height int) []byte {
	t.Helper()
	src := image.NewRGBA(image.Rect(0, 0, width, height))
	for y := 0; y < height; y++ {
		for x := 0; x < width; x++ {
			src.Set(x, y, color.RGBA{R: uint8(x % 256), G: uint8(y % 256), B: 0x40, A: 255})
		}
	}
	var raw bytes.Buffer
	if err := jpeg.Encode(&raw, src, &jpeg.Options{Quality: 95}); err != nil {
		t.Fatalf("encode jpeg source: %v", err)
	}
	return raw.Bytes()
}

func TestGenerateAvatarThumbnailResizesAndKeepsPNGForTransparency(t *testing.T) {
	// Transparent PNG stays PNG so round avatar masks keep their alpha.
	src := image.NewRGBA(image.Rect(0, 0, 300, 200))
	var raw bytes.Buffer
	if err := png.Encode(&raw, src); err != nil {
		t.Fatalf("encode png source: %v", err)
	}

	out, err := GenerateAvatarThumbnail(raw.Bytes(), "image/png", AvatarThumbnailSmallEdge, AvatarThumbnailFitCover)
	if err != nil {
		t.Fatalf("GenerateAvatarThumbnail: %v", err)
	}
	if out.ContentType != "image/png" {
		t.Errorf("ContentType = %q, want image/png", out.ContentType)
	}
	if out.Width != 128 || out.Height != 128 {
		t.Errorf("dimensions = %dx%d, want 128x128 center-cropped cover derivative", out.Width, out.Height)
	}
	decoded, _, err := image.Decode(bytes.NewReader(out.Blob))
	if err != nil {
		t.Fatalf("decode thumbnail: %v", err)
	}
	if _, _, _, alpha := decoded.At(0, 0).RGBA(); alpha != 0 {
		t.Errorf("expected preserved transparency, got alpha %v", alpha)
	}
}

func TestGenerateAvatarThumbnailReEncodesJPEGSources(t *testing.T) {
	out, err := GenerateAvatarThumbnail(encodeTestJPEG(t, 900, 600), "image/jpeg", AvatarThumbnailLargeEdge, AvatarThumbnailFitCover)
	if err != nil {
		t.Fatalf("GenerateAvatarThumbnail: %v", err)
	}
	if out.ContentType != "image/jpeg" {
		t.Errorf("ContentType = %q, want image/jpeg", out.ContentType)
	}
	if out.Width != 512 || out.Height != 512 {
		t.Errorf("dimensions = %dx%d, want 512x512 center-cropped cover derivative", out.Width, out.Height)
	}
}

func TestGenerateAvatarThumbnailDoesNotUpscale(t *testing.T) {
	out, err := GenerateAvatarThumbnail(encodeTestPNG(t, 64, 64, color.RGBA{R: 0xff, A: 0xff}), "image/png", AvatarThumbnailSmallEdge, AvatarThumbnailFitCover)
	if err != nil {
		t.Fatalf("GenerateAvatarThumbnail: %v", err)
	}
	if out.Width != 64 || out.Height != 64 {
		t.Errorf("dimensions = %dx%d, want 64x64 (no upscale)", out.Width, out.Height)
	}
}

// Square object-cover slots must never re-upscale a derivative: the short
// edge has to reach the tier target for common phone aspect ratios.
func TestGenerateAvatarThumbnailGuaranteesShortEdge(t *testing.T) {
	for _, tc := range []struct {
		name          string
		width, height int
		target        int
	}{
		{"4:3 portrait", 300, 400, 128},
		{"4:3 landscape", 400, 300, 128},
		{"16:9 landscape", 640, 360, 128},
		{"3:4 portrait 512 tier", 600, 800, 512},
	} {
		out, err := GenerateAvatarThumbnail(
			encodeTestJPEG(t, tc.width, tc.height),
			"image/jpeg",
			tc.target,
			AvatarThumbnailFitCover,
		)
		if err != nil {
			t.Fatalf("%s: GenerateAvatarThumbnail: %v", tc.name, err)
		}
		if out.Width != tc.target || out.Height != tc.target {
			t.Errorf("%s: dimensions = %dx%d, want %dx%d", tc.name, out.Width, out.Height, tc.target, tc.target)
		}
	}
}

func TestGenerateAvatarThumbnailCoverCropsExtremeAspect(t *testing.T) {
	out, err := GenerateAvatarThumbnail(
		encodeTestJPEG(t, 1024, 100),
		"image/jpeg",
		AvatarThumbnailSmallEdge,
		AvatarThumbnailFitCover,
	)
	if err != nil {
		t.Fatalf("GenerateAvatarThumbnail: %v", err)
	}
	if out.Width != 100 || out.Height != 100 {
		t.Errorf("dimensions = %dx%d, want 100x100 (center crop without upscaling)", out.Width, out.Height)
	}
}

func TestGenerateAvatarThumbnailContainPreservesWholeIcon(t *testing.T) {
	out, err := GenerateAvatarThumbnail(
		encodeTestPNG(t, 400, 200, color.RGBA{R: 0x30, G: 0x90, B: 0xf0, A: 0xff}),
		"image/png",
		AvatarThumbnailSmallEdge,
		AvatarThumbnailFitContain,
	)
	if err != nil {
		t.Fatalf("GenerateAvatarThumbnail: %v", err)
	}
	if out.Width != 128 || out.Height != 64 {
		t.Errorf("dimensions = %dx%d, want 128x64 contain derivative", out.Width, out.Height)
	}
}

// Animated GIF avatars must not be flattened into a static frame.
func TestGenerateAvatarThumbnailRejectsAnimatedGIF(t *testing.T) {
	first := image.NewPaletted(image.Rect(0, 0, 64, 64), color.Palette{color.RGBA{R: 0xff, A: 0xff}})
	second := image.NewPaletted(image.Rect(0, 0, 64, 64), color.Palette{color.RGBA{B: 0xff, A: 0xff}})
	var raw bytes.Buffer
	if err := gif.EncodeAll(&raw, &gif.GIF{Image: []*image.Paletted{first, second}, Delay: []int{10, 10}, LoopCount: 0}); err != nil {
		t.Fatalf("encode gif: %v", err)
	}
	if _, err := GenerateAvatarThumbnail(raw.Bytes(), "image/gif", AvatarThumbnailSmallEdge, AvatarThumbnailFitCover); err != ErrThumbnailNotSupported {
		t.Errorf("animated GIF err = %v, want ErrThumbnailNotSupported (fall back to original)", err)
	}
}

func TestGenerateAvatarThumbnailRejectsUnsupported(t *testing.T) {
	if _, err := GenerateAvatarThumbnail([]byte("<svg></svg>"), "image/svg+xml", AvatarThumbnailSmallEdge, AvatarThumbnailFitCover); err != ErrThumbnailNotSupported {
		t.Errorf("err = %v, want ErrThumbnailNotSupported", err)
	}
	if _, err := GenerateAvatarThumbnail(nil, "image/png", AvatarThumbnailSmallEdge, AvatarThumbnailFitCover); err != ErrThumbnailNotSupported {
		t.Errorf("empty source err = %v, want ErrThumbnailNotSupported", err)
	}
}

func TestAvatarThumbnailSpecMapsSizes(t *testing.T) {
	variant, edge, ok := AvatarThumbnailSpec("", AvatarThumbnailFitCover)
	if !ok || variant != AvatarThumbnailSmallCoverVariant || edge != AvatarThumbnailSmallEdge {
		t.Errorf("default spec = %q/%d/%v, want small variant", variant, edge, ok)
	}
	variant, edge, ok = AvatarThumbnailSpec("128", AvatarThumbnailFitContain)
	if !ok || variant != AvatarThumbnailSmallContainVariant || edge != AvatarThumbnailSmallEdge {
		t.Errorf("128 spec = %q/%d/%v, want small variant", variant, edge, ok)
	}
	variant, edge, ok = AvatarThumbnailSpec("512", AvatarThumbnailFitCover)
	if !ok || variant != AvatarThumbnailLargeCoverVariant || edge != AvatarThumbnailLargeEdge {
		t.Errorf("512 spec = %q/%d/%v, want large variant", variant, edge, ok)
	}
	if _, _, ok := AvatarThumbnailSpec("256", AvatarThumbnailFitCover); ok {
		t.Errorf("256 spec accepted, want rejection")
	}
}

func TestAttachmentThumbnailStorageUsesV2AfterExifRuleChange(t *testing.T) {
	fake := &fakeMediaStorage{
		driver:  objectstorage.DriverOSS,
		getBlob: []byte("cached-v2"),
	}
	store := &Store{sharedStore: sharedStore{mediaObjectStorage: fake}}

	got, err := store.LoadAttachmentThumbnail(context.Background(), "wsp_1", "att_1")
	require.NoError(t, err)
	require.Equal(t, []byte("cached-v2"), got.Blob)
	require.Equal(t, []string{
		"attachment-derivatives/wsp_1/att_1/thumb-v2.jpg",
	}, fake.fetched)

	_, err = store.SaveAttachmentThumbnail(context.Background(), "wsp_1", "att_1", GeneratedThumbnail{
		ContentType: "image/jpeg",
		Blob:        []byte("new-v2"),
		Width:       480,
		Height:      320,
	})
	require.NoError(t, err)
	require.Equal(t, []string{
		"attachment-derivatives/wsp_1/att_1/thumb-v2.jpg",
	}, fake.uploaded)
}

func TestNormalizeAvatarUploadDownscalesOversizedJPEG(t *testing.T) {
	contentType, blob := NormalizeAvatarUpload("image/jpeg", encodeTestJPEG(t, 4000, 3000))
	if contentType != "image/jpeg" {
		t.Errorf("contentType = %q, want image/jpeg", contentType)
	}
	config, _, err := image.DecodeConfig(bytes.NewReader(blob))
	if err != nil {
		t.Fatalf("decode normalized blob: %v", err)
	}
	if config.Width != 1024 || config.Height != 768 {
		t.Errorf("dimensions = %dx%d, want 1024x768", config.Width, config.Height)
	}
}

func TestNormalizeAvatarUploadKeepsSmallJPEGBytes(t *testing.T) {
	original := encodeTestJPEG(t, 512, 512)
	contentType, blob := NormalizeAvatarUpload("image/png", original)
	if contentType != "image/jpeg" {
		t.Errorf("contentType = %q, want image/jpeg", contentType)
	}
	if !bytes.Equal(original, blob) {
		t.Errorf("expected byte-identical storage for an in-cap JPEG")
	}
}

func TestNormalizeAvatarUploadReencodesPNGLosslessly(t *testing.T) {
	// A huge uniform-color PNG shrinks substantially after lossless re-encode.
	big := encodeTestPNG(t, 4096, 4096, color.RGBA{R: 0x30, G: 0x90, B: 0xf0, A: 0xff})
	contentType, blob := NormalizeAvatarUpload("image/png", big)
	if contentType != "image/png" {
		t.Errorf("contentType = %q, want image/png", contentType)
	}
	config, _, err := image.DecodeConfig(bytes.NewReader(blob))
	if err != nil {
		t.Fatalf("decode normalized blob: %v", err)
	}
	if config.Width != 1024 || config.Height != 1024 {
		t.Errorf("dimensions = %dx%d, want 1024x1024", config.Width, config.Height)
	}
	if len(blob) >= len(big) {
		t.Errorf("normalized PNG %d bytes >= original %d bytes", len(blob), len(big))
	}
}

func TestNormalizeAvatarUploadKeepsUndecodableFormats(t *testing.T) {
	svg := []byte("<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>")
	contentType, blob := NormalizeAvatarUpload("image/svg+xml", svg)
	if contentType != "image/svg+xml" || !bytes.Equal(svg, blob) {
		t.Errorf("expected undecodable formats to pass through unchanged")
	}
}

func TestNormalizeAvatarUploadKeepsGIFBytes(t *testing.T) {
	// A decodable GIF must pass through untouched (animation safety).
	src := image.NewPaletted(image.Rect(0, 0, 4, 4), color.Palette{color.RGBA{R: 0xff, A: 0xff}})
	var raw bytes.Buffer
	if err := gif.Encode(&raw, src, nil); err != nil {
		t.Fatalf("encode gif source: %v", err)
	}
	contentType, blob := NormalizeAvatarUpload("image/gif", raw.Bytes())
	if contentType != "image/gif" || !bytes.Equal(raw.Bytes(), blob) {
		t.Errorf("expected GIF bytes to pass through unchanged")
	}
}

func TestNormalizeAvatarUploadKeepsMislabeledOversizedGIFBytes(t *testing.T) {
	// Format decisions must come from decoded bytes. If a client labels an
	// animated GIF as JPEG, crossing the 1024px cap must not flatten it.
	palette := color.Palette{color.Black, color.White}
	first := image.NewPaletted(image.Rect(0, 0, 1200, 2), palette)
	second := image.NewPaletted(image.Rect(0, 0, 1200, 2), palette)
	var raw bytes.Buffer
	require.NoError(t, gif.EncodeAll(&raw, &gif.GIF{
		Image: []*image.Paletted{first, second},
		Delay: []int{10, 10},
	}))

	contentType, blob := NormalizeAvatarUpload("image/jpeg", raw.Bytes())
	require.Equal(t, "image/gif", contentType)
	require.Equal(t, raw.Bytes(), blob)
}
