package attachment

import (
	"bytes"
	"image"
	"image/color"
	"image/jpeg"
	"testing"
)

// jpegWithExifOrientation encodes img as JPEG and injects an EXIF APP1 segment
// carrying the given orientation, mimicking a phone camera's output.
func jpegWithExifOrientation(t *testing.T, img image.Image, byteOrder string, orientation uint16) []byte {
	t.Helper()
	var raw bytes.Buffer
	if err := jpeg.Encode(&raw, img, &jpeg.Options{Quality: 95}); err != nil {
		t.Fatalf("encode jpeg: %v", err)
	}
	return appendExifOrientation(raw.Bytes(), byteOrder, orientation)
}

func appendExifOrientation(jpegBytes []byte, byteOrder string, orientation uint16) []byte {
	// TIFF header (8) + IFD count (2) + one entry (12) + next-IFD offset (4).
	tiff := make([]byte, 8+2+12+4)
	if byteOrder == "MM" {
		copy(tiff[0:2], "MM")
		tiff[2], tiff[3] = 0x00, 0x2A
		putUint32BE(tiff[4:8], 8)
		putUint16BE(tiff[8:10], 1)
		putUint16BE(tiff[10:12], 0x0112)
		putUint16BE(tiff[12:14], 3)
		putUint32BE(tiff[14:18], 1)
		putUint16BE(tiff[18:20], orientation)
	} else {
		copy(tiff[0:2], "II")
		tiff[2], tiff[3] = 0x2A, 0x00
		putUint32LE(tiff[4:8], 8)
		putUint16LE(tiff[8:10], 1)
		putUint16LE(tiff[10:12], 0x0112)
		putUint16LE(tiff[12:14], 3)
		putUint32LE(tiff[14:18], 1)
		putUint16LE(tiff[18:20], orientation)
	}
	payload := append([]byte("Exif\x00\x00"), tiff...)
	segmentLength := len(payload) + 2
	app1 := []byte{0xFF, 0xE1, byte(segmentLength >> 8), byte(segmentLength)}
	app1 = append(app1, payload...)
	// APP1 goes right after the SOI marker.
	return append(append(jpegBytes[:2:2], app1...), jpegBytes[2:]...)
}

func putUint16LE(b []byte, v uint16) { b[0], b[1] = byte(v), byte(v>>8) }
func putUint16BE(b []byte, v uint16) { b[0], b[1] = byte(v>>8), byte(v) }
func putUint32LE(b []byte, v uint32) {
	b[0], b[1], b[2], b[3] = byte(v), byte(v>>8), byte(v>>16), byte(v>>24)
}
func putUint32BE(b []byte, v uint32) {
	b[0], b[1], b[2], b[3] = byte(v>>24), byte(v>>16), byte(v>>8), byte(v)
}

func halfSplitImage(width, height int) *image.RGBA {
	img := image.NewRGBA(image.Rect(0, 0, width, height))
	for y := 0; y < height; y++ {
		for x := 0; x < width; x++ {
			if y < height/2 {
				img.Set(x, y, color.RGBA{R: 0xff, A: 0xff})
			} else {
				img.Set(x, y, color.RGBA{B: 0xff, A: 0xff})
			}
		}
	}
	return img
}

func TestReadExifOrientationParsesBothByteOrders(t *testing.T) {
	img := halfSplitImage(40, 30)
	if got := readExifOrientation(jpegWithExifOrientation(t, img, "II", 6)); got != 6 {
		t.Errorf("II order orientation = %d, want 6", got)
	}
	if got := readExifOrientation(jpegWithExifOrientation(t, img, "MM", 8)); got != 8 {
		t.Errorf("MM order orientation = %d, want 8", got)
	}
}

func TestReadExifOrientationSkipsNonAppMetadataBeforeExif(t *testing.T) {
	img := halfSplitImage(40, 30)
	withExif := jpegWithExifOrientation(t, img, "II", 6)
	// A valid COM segment before APP1 used to make the parser stop before it
	// reached EXIF. The segment length includes its own two length bytes.
	comment := []byte{0xFF, 0xFE, 0x00, 0x06, 'n', 'o', 't', 'e'}
	withComment := append(append(withExif[:2:2], comment...), withExif[2:]...)
	if got := readExifOrientation(withComment); got != 6 {
		t.Errorf("orientation after COM = %d, want 6", got)
	}
}

func TestOrientationFromTIFFRejectsMalformedOrientationEntry(t *testing.T) {
	img := halfSplitImage(40, 30)
	validJPEG := jpegWithExifOrientation(t, img, "II", 6)
	// The test helper always writes APP1 immediately after SOI. Strip the
	// marker/length and the six-byte Exif prefix to address its TIFF payload.
	validTIFF := append([]byte(nil), validJPEG[12:38]...)

	wrongMagic := append([]byte(nil), validTIFF...)
	wrongMagic[2], wrongMagic[3] = 0, 0
	if got := orientationFromTIFF(wrongMagic); got != 0 {
		t.Errorf("orientation with wrong TIFF magic = %d, want 0", got)
	}

	wrongCount := append([]byte(nil), validTIFF...)
	putUint32LE(wrongCount[14:18], 2)
	if got := orientationFromTIFF(wrongCount); got != 0 {
		t.Errorf("orientation with count=2 = %d, want 0", got)
	}
}

func TestReadExifOrientationFallsBackSafely(t *testing.T) {
	img := halfSplitImage(40, 30)
	var plain bytes.Buffer
	if err := jpeg.Encode(&plain, img, nil); err != nil {
		t.Fatalf("encode jpeg: %v", err)
	}
	if got := readExifOrientation(plain.Bytes()); got != 0 {
		t.Errorf("EXIF-free JPEG orientation = %d, want 0", got)
	}
	if got := readExifOrientation(encodeTestPNG(t, 8, 8, color.RGBA{A: 0xff})); got != 0 {
		t.Errorf("PNG orientation = %d, want 0", got)
	}
	if got := readExifOrientation(nil); got != 0 {
		t.Errorf("nil orientation = %d, want 0", got)
	}
}

func TestApplyExifOrientationTransformsAllValues(t *testing.T) {
	// 4×2 grid, every pixel unique via its coordinates.
	src := image.NewRGBA(image.Rect(0, 0, 4, 2))
	for y := 0; y < 2; y++ {
		for x := 0; x < 4; x++ {
			src.Set(x, y, color.RGBA{R: uint8(x), G: uint8(y), B: 0, A: 0xff})
		}
	}
	cases := []struct {
		orientation uint16
		w, h        int
	}{
		{1, 4, 2}, {2, 4, 2}, {3, 4, 2}, {4, 4, 2},
		{5, 2, 4}, {6, 2, 4}, {7, 2, 4}, {8, 2, 4},
	}
	for _, tc := range cases {
		dst := applyExifOrientation(src, tc.orientation)
		bounds := dst.Bounds()
		if bounds.Dx() != tc.w || bounds.Dy() != tc.h {
			t.Errorf("orientation %d bounds = %dx%d, want %dx%d", tc.orientation, bounds.Dx(), bounds.Dy(), tc.w, tc.h)
			continue
		}
		// Every destination pixel must invert to the source pixel the
		// transform claims.
		for y := 0; y < tc.h; y++ {
			for x := 0; x < tc.w; x++ {
				wantX, wantY := exifSourceCoordinates(int(tc.orientation), x, y, 4, 2)
				r, g, _, _ := dst.At(x, y).RGBA()
				if uint8(r>>8) != uint8(wantX) || uint8(g>>8) != uint8(wantY) {
					t.Errorf("orientation %d dst(%d,%d) = (%d,%d), want src(%d,%d)",
						tc.orientation, x, y, uint8(r>>8), uint8(g>>8), wantX, wantY)
				}
			}
		}
	}
}

func TestGenerateAvatarThumbnailHonorsExifOrientation(t *testing.T) {
	// 300×200 with red top half / blue bottom half, stored rotated with
	// Orientation=6 (rotate 90 CW): upright display swaps the halves' axis.
	rotated := image.NewRGBA(image.Rect(0, 0, 300, 200))
	for y := 0; y < 200; y++ {
		for x := 0; x < 300; x++ {
			if x < 150 {
				rotated.Set(x, y, color.RGBA{B: 0xff, A: 0xff})
			} else {
				rotated.Set(x, y, color.RGBA{R: 0xff, A: 0xff})
			}
		}
	}
	blob := jpegWithExifOrientation(t, rotated, "II", 6)

	out, err := GenerateAvatarThumbnail(blob, "image/jpeg", AvatarThumbnailSmallEdge, AvatarThumbnailFitCover)
	if err != nil {
		t.Fatalf("GenerateAvatarThumbnail: %v", err)
	}
	if out.Width != 128 || out.Height != 128 {
		t.Errorf("dimensions = %dx%d, want 128x128 upright center-cropped derivative", out.Width, out.Height)
	}
	decoded, _, err := image.Decode(bytes.NewReader(out.Blob))
	if err != nil {
		t.Fatalf("decode thumbnail: %v", err)
	}
	// Orientation 6 rotates 90 CW: the stored left half (blue) becomes the
	// top of the upright render. The square center crop keeps both halves.
	rTop, _, _, _ := decoded.At(64, 10).RGBA()
	rBottom, _, _, _ := decoded.At(64, 118).RGBA()
	if rTop > 10000 || rBottom < 15000 {
		t.Errorf("top red=%d bottom red=%d, want blue top / red bottom after orientation", rTop, rBottom)
	}
}

func TestNormalizeAvatarUploadBakesOrientationWhenReencoding(t *testing.T) {
	// Oversized rotated JPEG: the stored original must be upright and cap-sized.
	rotated := image.NewRGBA(image.Rect(0, 0, 4000, 3000))
	for y := 0; y < 3000; y += 3 {
		for x := 0; x < 4000; x += 3 {
			rotated.Set(x, y, color.RGBA{R: uint8(x % 251), G: uint8(y % 241), B: 0x40, A: 0xff})
		}
	}
	blob := jpegWithExifOrientation(t, rotated, "II", 6)

	contentType, normalized := NormalizeAvatarUpload("image/jpeg", blob)
	if contentType != "image/jpeg" {
		t.Fatalf("contentType = %q, want image/jpeg", contentType)
	}
	config, _, err := image.DecodeConfig(bytes.NewReader(normalized))
	if err != nil {
		t.Fatalf("decode normalized: %v", err)
	}
	if config.Width != 768 || config.Height != 1024 {
		t.Errorf("dimensions = %dx%d, want 768x1024 (upright 3000×4000 capped)", config.Width, config.Height)
	}
}

func TestNormalizeAvatarUploadKeepsInCapJpegWithExifByteIdentical(t *testing.T) {
	small := halfSplitImage(300, 200)
	blob := jpegWithExifOrientation(t, small, "II", 6)

	_, normalized := NormalizeAvatarUpload("image/jpeg", blob)
	if !bytes.Equal(blob, normalized) {
		t.Errorf("in-cap JPEG with EXIF must stay byte-identical (EXIF keeps rendering upright)")
	}
}
