package utils

import (
	"bytes"
	"image"
	"image/color"
	"image/png"
	"math/rand"
	"strings"
	"testing"
)

func TestWriteKittyImage_SmallImage(t *testing.T) {
	// 2x2 image produces small base64 payload — single chunk, no m= key
	img := image.NewGray(image.Rect(0, 0, 2, 2))
	img.SetGray(0, 0, color.Gray{Y: 128})

	var buf strings.Builder
	if err := WriteKittyImage(img, &buf); err != nil {
		t.Fatalf("WriteKittyImage: %v", err)
	}

	out := buf.String()
	if !strings.HasPrefix(out, "\x1b_Gf=100,a=T;") {
		t.Error("expected single-chunk header with f=100,a=T")
	}
	if strings.Contains(out, "m=") {
		t.Error("small image should not use chunked m= key")
	}
	if !strings.HasSuffix(out, "\x1b\\") {
		t.Error("expected escape sequence terminator")
	}
}

func TestWriteKittyImage_LargeImage_Chunked(t *testing.T) {
	// 128x128 random noise image is incompressible — produces >4096 bytes of base64 even with proper LZ77
	rng := rand.New(rand.NewSource(42))
	img := image.NewGray(image.Rect(0, 0, 128, 128))
	for y := range 128 {
		for x := range 128 {
			img.SetGray(x, y, color.Gray{Y: uint8(rng.Intn(256))})
		}
	}

	var buf strings.Builder
	if err := WriteKittyImage(img, &buf); err != nil {
		t.Fatalf("WriteKittyImage: %v", err)
	}

	out := buf.String()

	// Should have multiple escape sequences
	chunks := strings.Split(out, "\x1b\\")
	// Last element is empty after final terminator
	chunks = chunks[:len(chunks)-1]

	if len(chunks) < 2 {
		t.Fatalf("expected multiple chunks, got %d", len(chunks))
	}

	// First chunk should have f=100,a=T,m=1
	if !strings.Contains(chunks[0], "f=100,a=T,m=1") {
		t.Errorf("first chunk missing f=100,a=T,m=1: %s", chunks[0][:min(80, len(chunks[0]))])
	}

	// Last chunk should have m=0
	last := chunks[len(chunks)-1]
	if !strings.Contains(last, "\x1b_Gm=0;") {
		t.Errorf("last chunk missing m=0: %s", last[:min(80, len(last))])
	}

	// Middle chunks should have m=1
	for i := 1; i < len(chunks)-1; i++ {
		if !strings.Contains(chunks[i], "\x1b_Gm=1;") {
			t.Errorf("middle chunk %d missing m=1", i)
		}
	}
}

func TestClearKittyImages(t *testing.T) {
	var buf strings.Builder
	ClearKittyImages(&buf)
	expected := "\x1b_Ga=d\x1b\\"
	if buf.String() != expected {
		t.Errorf("got %q, want %q", buf.String(), expected)
	}
}

func TestWriteSixelImage(t *testing.T) {
	img := image.NewGray(image.Rect(0, 0, 4, 6))
	for y := range 6 {
		for x := range 4 {
			img.SetGray(x, y, color.Gray{Y: uint8((x + y) * 40)})
		}
	}

	var buf strings.Builder
	if err := WriteSixelImage(img, &buf); err != nil {
		t.Fatalf("WriteSixelImage: %v", err)
	}

	out := buf.String()

	// Sixel DCS introducer
	if !strings.HasPrefix(out, "\x1bP") {
		t.Error("expected DCS prefix \\x1bP")
	}

	// String terminator
	if !strings.HasSuffix(out, "\x1b\\") {
		t.Error("expected ST suffix \\x1b\\\\")
	}

	// Should contain 'q' after DCS parameters
	if !strings.Contains(out, "q") {
		t.Error("expected 'q' in DCS sequence")
	}
}

func TestClearImages_Kitty(t *testing.T) {
	var buf strings.Builder
	ClearImages(&buf, ProtocolKitty)
	if buf.String() != "\x1b_Ga=d\x1b\\" {
		t.Errorf("got %q, want kitty clear sequence", buf.String())
	}
}

func TestClearImages_Sixel(t *testing.T) {
	var buf strings.Builder
	ClearImages(&buf, ProtocolSixel)
	if buf.String() != "" {
		t.Errorf("expected no output for sixel clear, got %q", buf.String())
	}
}

func TestWriteImage_Kitty(t *testing.T) {
	img := image.NewGray(image.Rect(0, 0, 2, 2))
	var buf strings.Builder
	if err := WriteImage(img, &buf, ProtocolKitty); err != nil {
		t.Fatalf("WriteImage kitty: %v", err)
	}
	if !strings.HasPrefix(buf.String(), "\x1b_G") {
		t.Error("expected kitty escape prefix")
	}
}

func TestWriteImage_Sixel(t *testing.T) {
	img := image.NewGray(image.Rect(0, 0, 4, 6))
	var buf strings.Builder
	if err := WriteImage(img, &buf, ProtocolSixel); err != nil {
		t.Fatalf("WriteImage sixel: %v", err)
	}
	if !strings.HasPrefix(buf.String(), "\x1bP") {
		t.Error("expected sixel DCS prefix")
	}
}

func TestWriteITermImage(t *testing.T) {
	img := image.NewGray(image.Rect(0, 0, 4, 4))
	img.SetGray(0, 0, color.Gray{Y: 128})

	var buf strings.Builder
	if err := WriteITermImage(img, &buf); err != nil {
		t.Fatalf("WriteITermImage: %v", err)
	}

	out := buf.String()
	if !strings.HasPrefix(out, "\x1b]1337;File=") {
		t.Errorf("expected iTerm2 OSC prefix, got %q", out[:min(30, len(out))])
	}
	if !strings.Contains(out, "inline=1") {
		t.Error("expected inline=1 parameter")
	}
	if !strings.HasSuffix(out, "\x07") {
		t.Error("expected BEL terminator")
	}
}

func TestWriteImage_ITerm(t *testing.T) {
	img := image.NewGray(image.Rect(0, 0, 4, 4))
	var buf strings.Builder
	if err := WriteImage(img, &buf, ProtocolITerm); err != nil {
		t.Fatalf("WriteImage iterm: %v", err)
	}
	if !strings.HasPrefix(buf.String(), "\x1b]1337;File=") {
		t.Error("expected iTerm2 OSC prefix")
	}
}

func TestClampImageSize(t *testing.T) {
	tests := []struct {
		input int
		want  int
	}{
		{100, 224},  // below minimum, clamped up
		{224, 224},  // at minimum
		{448, 448},  // in range
		{600, 600},  // in range
		{896, 896},  // at maximum
		{1000, 896}, // above maximum, clamped down
	}
	for _, tt := range tests {
		got := ClampImageSize(tt.input)
		if got != tt.want {
			t.Errorf("ClampImageSize(%d) = %d, want %d", tt.input, got, tt.want)
		}
	}
}

func TestCreateGrayscaleImage_Basic(t *testing.T) {
	data := [][]uint8{
		{0, 128, 255},
		{64, 192, 32},
	}
	img := CreateGrayscaleImage(data)
	if img == nil {
		t.Fatal("expected non-nil image")
	}

	bounds := img.Bounds()
	if bounds.Dx() != 3 {
		t.Errorf("width = %d, want 3", bounds.Dx())
	}
	if bounds.Dy() != 2 {
		t.Errorf("height = %d, want 2", bounds.Dy())
	}

	gray := img.(*image.Gray)
	// Row 0
	if gray.Pix[0] != 0 {
		t.Errorf("pix[0] = %d, want 0", gray.Pix[0])
	}
	if gray.Pix[1] != 128 {
		t.Errorf("pix[1] = %d, want 128", gray.Pix[1])
	}
	if gray.Pix[2] != 255 {
		t.Errorf("pix[2] = %d, want 255", gray.Pix[2])
	}
	// Row 1
	if gray.Pix[gray.Stride+0] != 64 {
		t.Errorf("pix[row1+0] = %d, want 64", gray.Pix[gray.Stride])
	}
	if gray.Pix[gray.Stride+1] != 192 {
		t.Errorf("pix[row1+1] = %d, want 192", gray.Pix[gray.Stride+1])
	}
	if gray.Pix[gray.Stride+2] != 32 {
		t.Errorf("pix[row1+2] = %d, want 32", gray.Pix[gray.Stride+2])
	}
}

func TestCreateGrayscaleImage_Empty(t *testing.T) {
	tests := []struct {
		name string
		data [][]uint8
	}{
		{"nil", nil},
		{"empty outer", [][]uint8{}},
		{"empty inner", [][]uint8{{}}},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			img := CreateGrayscaleImage(tt.data)
			if img != nil {
				t.Errorf("expected nil for %s", tt.name)
			}
		})
	}
}

// assertRGBAPixel checks that the pixel at (x,y) in an RGBA image matches the expected RGBA values.
func assertRGBAPixel(t *testing.T, rgba *image.RGBA, x, y int, wantR, wantG, wantB, wantA uint8) {
	t.Helper()
	off := y*rgba.Stride + x*4
	got := rgba.Pix[off : off+4]
	if got[0] != wantR || got[1] != wantG || got[2] != wantB || got[3] != wantA {
		t.Errorf("pixel (%d,%d) = [%d,%d,%d,%d], want [%d,%d,%d,%d]",
			x, y, got[0], got[1], got[2], got[3], wantR, wantG, wantB, wantA)
	}
}

func TestCreateRGBImage_Basic(t *testing.T) {
	data := [][]RGBPixel{
		{{R: 255, G: 0, B: 0}, {R: 0, G: 255, B: 0}},
		{{R: 0, G: 0, B: 255}, {R: 255, G: 255, B: 255}},
	}
	img := CreateRGBImage(data)
	if img == nil {
		t.Fatal("expected non-nil image")
	}

	bounds := img.Bounds()
	if bounds.Dx() != 2 {
		t.Errorf("width = %d, want 2", bounds.Dx())
	}
	if bounds.Dy() != 2 {
		t.Errorf("height = %d, want 2", bounds.Dy())
	}

	rgba := img.(*image.RGBA)
	assertRGBAPixel(t, rgba, 0, 0, 255, 0, 0, 255) // red
	assertRGBAPixel(t, rgba, 1, 0, 0, 255, 0, 255) // green
	assertRGBAPixel(t, rgba, 0, 1, 0, 0, 255, 255) // blue
}

func TestCreateRGBImage_Empty(t *testing.T) {
	tests := []struct {
		name string
		data [][]RGBPixel
	}{
		{"nil", nil},
		{"empty outer", [][]RGBPixel{}},
		{"empty inner", [][]RGBPixel{{}}},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			img := CreateRGBImage(tt.data)
			if img != nil {
				t.Errorf("expected nil for %s", tt.name)
			}
		})
	}
}

func TestResizeImage_Grayscale(t *testing.T) {
	// 4x4 uniform gray → 2x2 should produce same gray value
	data := make([][]uint8, 4)
	for i := range data {
		data[i] = []uint8{128, 128, 128, 128}
	}
	src := CreateGrayscaleImage(data)

	resized := ResizeImage(src, 2, 2)
	gray := resized.(*image.Gray)
	for y := range 2 {
		for x := range 2 {
			if gray.Pix[y*gray.Stride+x] != 128 {
				t.Errorf("pixel (%d,%d) = %d, want 128", x, y, gray.Pix[y*gray.Stride+x])
			}
		}
	}
}

func TestResizeImage_RGBA(t *testing.T) {
	// 4x4 uniform color → 2x2 should preserve color
	rgbData := make([][]RGBPixel, 4)
	for i := range rgbData {
		rgbData[i] = []RGBPixel{{R: 100, G: 150, B: 200}, {R: 100, G: 150, B: 200}, {R: 100, G: 150, B: 200}, {R: 100, G: 150, B: 200}}
	}
	src := CreateRGBImage(rgbData)

	resized := ResizeImage(src, 2, 2)
	rgba := resized.(*image.RGBA)
	for y := range 2 {
		for x := range 2 {
			off := y*rgba.Stride + x*4
			if rgba.Pix[off] != 100 || rgba.Pix[off+1] != 150 || rgba.Pix[off+2] != 200 {
				t.Errorf("pixel (%d,%d) = (%d,%d,%d), want (100,150,200)",
					x, y, rgba.Pix[off], rgba.Pix[off+1], rgba.Pix[off+2])
			}
		}
	}
}

func TestResizeImage_GenericFallback(t *testing.T) {
	// Create a paletted image (not *image.Gray or *image.RGBA) to exercise fallback path
	palette := color.Palette{color.Black, color.White}
	src := image.NewPaletted(image.Rect(0, 0, 4, 4), palette)
	for y := range 4 {
		for x := range 4 {
			src.SetColorIndex(x, y, 0) // black
		}
	}
	resized := ResizeImage(src, 2, 2)
	bounds := resized.Bounds()
	if bounds.Dx() != 2 || bounds.Dy() != 2 {
		t.Errorf("bounds = %v, want 2x2", bounds)
	}
}

func TestResizeImage_Upscale(t *testing.T) {
	// 2x2 → 4x4 nearest-neighbor upscale
	data := [][]uint8{
		{0, 255},
		{128, 64},
	}
	src := CreateGrayscaleImage(data)
	resized := ResizeImage(src, 4, 4)
	gray := resized.(*image.Gray)

	// Top-left quadrant should be 0
	if gray.Pix[0] != 0 {
		t.Errorf("(0,0) = %d, want 0", gray.Pix[0])
	}
	// Top-right quadrant should be 255
	if gray.Pix[3] != 255 {
		t.Errorf("(3,0) = %d, want 255", gray.Pix[3])
	}
}

func TestWritePNG(t *testing.T) {
	img := image.NewGray(image.Rect(0, 0, 4, 4))
	var buf bytes.Buffer
	if err := WritePNG(img, &buf); err != nil {
		t.Fatalf("WritePNG: %v", err)
	}
	if buf.Len() == 0 {
		t.Error("expected non-empty PNG output")
	}

	// Verify it's a valid PNG by decoding
	decoded, err := png.Decode(&buf)
	if err != nil {
		t.Fatalf("failed to decode PNG: %v", err)
	}
	if decoded.Bounds().Dx() != 4 || decoded.Bounds().Dy() != 4 {
		t.Errorf("decoded bounds = %v, want 4x4", decoded.Bounds())
	}
}

func TestClearImages_ITerm(t *testing.T) {
	var buf strings.Builder
	ClearImages(&buf, ProtocolITerm)
	if buf.String() != "" {
		t.Errorf("expected no output for iTerm2 clear, got %q", buf.String())
	}
}