package client

import (
	"image"
	"sync"
	"testing"

	"github.com/stretchr/testify/require"
)

type testExecutor struct {
	mu    sync.Mutex
	queue []func()
}

func (e *testExecutor) Post(fn func()) {
	e.mu.Lock()
	e.queue = append(e.queue, fn)
	e.mu.Unlock()
}

func (e *testExecutor) FlushAll() {
	for {
		var fn func()
		e.mu.Lock()
		if len(e.queue) > 0 {
			fn = e.queue[0]
			e.queue = e.queue[1:]
		}
		e.mu.Unlock()
		if fn == nil {
			return
		}
		fn()
	}
}

type testRefresher struct {
	mu    sync.Mutex
	count int
}

func (r *testRefresher) Refresh() {
	r.mu.Lock()
	r.count++
	r.mu.Unlock()
}

func (r *testRefresher) Count() int {
	r.mu.Lock()
	defer r.mu.Unlock()
	return r.count
}

func TestRasterCoalescer_RequestBeforeDraw_CoalescesToLatest(t *testing.T) {
	t.Parallel()

	exec := &testExecutor{}
	ref := &testRefresher{}

	var got []int

	co := NewRasterCoalescer(exec, ref, func(w, h int, p int) image.Image {
		got = append(got, p)
		return image.NewRGBA(image.Rect(0, 0, w, h))
	})

	co.Request(1)
	co.Request(2)
	co.Request(3)

	// Only a single refresh should be scheduled before the next Draw().
	exec.FlushAll()
	require.Equal(t, 1, ref.Count())

	_ = co.Draw(10, 10)
	require.Equal(t, []int{3}, got)
}

func TestRasterCoalescer_RequestDuringDraw_SchedulesOneFollowUpRefresh(t *testing.T) {
	t.Parallel()

	exec := &testExecutor{}
	ref := &testRefresher{}
	var got []int

	var co *RasterCoalescer[int]
	co = NewRasterCoalescer(exec, ref, func(w, h int, p int) image.Image {
		got = append(got, p)
		if p == 1 {
			co.Request(2)
			co.Request(3)
		}
		return image.NewRGBA(image.Rect(0, 0, w, h))
	})

	co.Request(1)

	exec.FlushAll()
	require.Equal(t, 1, ref.Count())

	// First draw renders 1 and schedules exactly one additional refresh.
	_ = co.Draw(10, 10)
	exec.FlushAll()
	require.Equal(t, 2, ref.Count())

	// Second draw renders latest (3).
	_ = co.Draw(10, 10)
	require.Equal(t, []int{1, 3}, got)
}

func TestRasterCoalescer_ManyRequestsWhileDrawing_StillOnlyOneExtraRefresh(t *testing.T) {
	t.Parallel()

	exec := &testExecutor{}
	ref := &testRefresher{}
	var got []int

	var co *RasterCoalescer[int]
	co = NewRasterCoalescer(exec, ref, func(w, h int, p int) image.Image {
		got = append(got, p)
		if p == 1 {
			for i := 2; i <= 50; i++ {
				co.Request(i)
			}
		}
		return image.NewRGBA(image.Rect(0, 0, w, h))
	})

	co.Request(1)
	exec.FlushAll()
	require.Equal(t, 1, ref.Count())

	_ = co.Draw(10, 10)
	exec.FlushAll()
	require.Equal(t, 2, ref.Count())

	_ = co.Draw(10, 10)
	require.Equal(t, []int{1, 50}, got)
}

func TestCopyViewportRGBA_CopiesROIAndIsIndependentFromSource(t *testing.T) {
	t.Parallel()

	src := image.NewRGBA(image.Rect(0, 0, 20, 20))
	dst := image.NewRGBA(image.Rect(0, 0, 5, 6))

	// Fill src with a pattern: pixel (x,y) has RGBA = (x, y, 0, 255).
	for y := 0; y < 20; y++ {
		for x := 0; x < 20; x++ {
			off := y*src.Stride + x*4
			src.Pix[off+0] = byte(x)
			src.Pix[off+1] = byte(y)
			src.Pix[off+2] = 0
			src.Pix[off+3] = 255
		}
	}

	marginX, marginY := 7, 9
	copyViewportRGBA(dst, src, marginX, marginY, 5, 6)

	// Verify a few pixels in dst match the expected source ROI.
	// dst(0,0) == src(marginX, marginY)
	{
		off := 0*dst.Stride + 0*4
		require.Equal(t, byte(marginX), dst.Pix[off+0])
		require.Equal(t, byte(marginY), dst.Pix[off+1])
		require.Equal(t, byte(255), dst.Pix[off+3])
	}
	// dst(4,5) == src(marginX+4, marginY+5)
	{
		off := 5*dst.Stride + 4*4
		require.Equal(t, byte(marginX+4), dst.Pix[off+0])
		require.Equal(t, byte(marginY+5), dst.Pix[off+1])
		require.Equal(t, byte(255), dst.Pix[off+3])
	}

	// Mutate src ROI after copy and ensure dst is unchanged (no aliasing).
	{
		off := (marginY+0)*src.Stride + (marginX+0)*4
		src.Pix[off+0] = 200
		src.Pix[off+1] = 201
		src.Pix[off+3] = 123
	}

	offDst := 0*dst.Stride + 0*4
	require.Equal(t, byte(marginX), dst.Pix[offDst+0])
	require.Equal(t, byte(marginY), dst.Pix[offDst+1])
	require.Equal(t, byte(255), dst.Pix[offDst+3])
}

func TestEventPosToPixel_FloorMapping(t *testing.T) {
	t.Parallel()

	e := &client{}

	// Pretend raster logical is 100x50, pixel is 1000x500.
	e.metaMu.Lock()
	e.lastRasterLogicW = 100
	e.lastRasterLogicH = 50
	e.lastRasterPxW = 1000
	e.lastRasterPxH = 500
	e.metaMu.Unlock()

	x, y, ok := e.eventPosToPixel(0, 0)
	require.True(t, ok)
	require.Equal(t, 0, x)
	require.Equal(t, 0, y)

	// Middle
	x, y, ok = e.eventPosToPixel(50, 25)
	require.True(t, ok)
	require.Equal(t, 500, x)
	require.Equal(t, 250, y)

	// Near max logical should map near max pixel with floor.
	x, y, ok = e.eventPosToPixel(99.9, 49.9)
	require.True(t, ok)
	require.GreaterOrEqual(t, x, 998)
	require.GreaterOrEqual(t, y, 498)

	// Clamp
	x, y, ok = e.eventPosToPixel(-10, 999)
	require.True(t, ok)
	require.Equal(t, 0, x)
	require.Equal(t, 500, y)
}
