package world

import (
	"fmt"
	"github.com/fogleman/gg"
	"github.com/stretchr/testify/require"
	"image"
	"image/color"
	"sync"
	"testing"
)

func hasAnyNonTransparentPixel(img image.Image) bool {
	b := img.Bounds()
	for y := b.Min.Y; y < b.Max.Y; y++ {
		for x := b.Min.X; x < b.Max.X; x++ {
			_, _, _, a := img.At(x, y).RGBA()
			if a != 0 {
				return true
			}
		}
	}
	return false
}

func pixelHasAlpha(img image.Image, x, y int) bool {
	_, _, _, a := img.At(x, y).RGBA()
	return a != 0
}

// TestGGDrawerStrokeSequenceProducesPixels verifies gG Drawer Stroke Sequence Produces Pixels.
func TestGGDrawerStrokeSequenceProducesPixels(t *testing.T) {
	t.Parallel()

	dc := gg.NewContext(32, 32)
	drawer := &GGDrawer{DC: dc}

	drawer.SetStrokeColor(color.RGBA{R: 255, A: 255})
	drawer.SetLineWidth(2)
	drawer.SetDash(4, 2)
	drawer.SetDashOffset(1)
	drawer.AddLine(4, 16, 28, 16)
	drawer.Stroke()

	require.True(t, hasAnyNonTransparentPixel(dc.Image()))
}

// TestGGDrawerFillSequenceProducesPixels verifies gG Drawer Fill Sequence Produces Pixels.
func TestGGDrawerFillSequenceProducesPixels(t *testing.T) {
	t.Parallel()

	dc := gg.NewContext(32, 32)
	drawer := &GGDrawer{DC: dc}

	drawer.SetFillColor(color.RGBA{G: 255, A: 255})
	drawer.AddCircle(16, 16, 6)
	drawer.Fill()

	require.True(t, pixelHasAlpha(dc.Image(), 16, 16))
}

// TestGGDrawerPointSequenceProducesPixels verifies gG Drawer Point Sequence Produces Pixels.
func TestGGDrawerPointSequenceProducesPixels(t *testing.T) {
	t.Parallel()

	dc := gg.NewContext(32, 32)
	drawer := &GGDrawer{DC: dc}

	drawer.SetFillColor(color.RGBA{B: 255, A: 255})
	drawer.AddPoint(16, 16, 3)
	drawer.Fill()

	require.True(t, pixelHasAlpha(dc.Image(), 16, 16))
}

// TestGGDrawerClipRectLimitsDrawing verifies gG Drawer Clip Rect Limits Drawing.
func TestGGDrawerClipRectLimitsDrawing(t *testing.T) {
	t.Parallel()

	dc := gg.NewContext(32, 32)
	drawer := &GGDrawer{DC: dc}

	drawer.Save()
	drawer.ClipRect(0, 0, 10, 32)
	drawer.SetFillColor(color.RGBA{B: 255, A: 255})
	drawer.AddCircle(15, 16, 10)
	drawer.Fill()
	drawer.Restore()

	img := dc.Image()

	require.True(t, pixelHasAlpha(img, 5, 16))
	require.False(t, pixelHasAlpha(img, 15, 16))
}

// TestGGDrawerResetClipClearsClip verifies gG Drawer Reset Clip Clears Clip.
func TestGGDrawerResetClipClearsClip(t *testing.T) {
	t.Parallel()

	dc := gg.NewContext(32, 32)
	drawer := &GGDrawer{DC: dc}

	drawer.ClipRect(0, 0, 10, 32)
	drawer.ResetClip()
	drawer.SetFillColor(color.RGBA{R: 255, G: 255, A: 255})
	drawer.AddCircle(15, 16, 10)
	drawer.Fill()

	require.True(t, pixelHasAlpha(dc.Image(), 15, 16))
}

// TestGGDrawerClearRectTo_FillsBackground verifies gG Drawer Clear Rect To Fills Background.
func TestGGDrawerClearRectTo_FillsBackground(t *testing.T) {
	t.Parallel()

	dc := gg.NewContext(10, 10)
	dr := &GGDrawer{DC: dc}

	// Draw something to ensure we overwrite non-background.
	dr.SetFillColor(color.RGBA{R: 255, A: 255})
	dr.AddCircle(5, 5, 5)
	dr.Fill()

	bg := color.RGBA{A: 255} // black
	dr.ClearRectTo(1, 1, 2, 2, bg)

	img := dc.Image()
	r, g, b, a := img.At(1, 1).RGBA()

	require.Equal(t, uint32(0), r)
	require.Equal(t, uint32(0), g)
	require.Equal(t, uint32(0), b)
	require.Equal(t, uint32(0xffff), a)

	// Pixel outside cleared rect should still have non-zero alpha.
	_, _, _, a2 := img.At(5, 5).RGBA()
	require.NotEqual(t, uint32(0), a2)
}

// TestGGDrawerSaveRestoreRestoresClipState verifies gG Drawer Save Restore Restores Clip State.
func TestGGDrawerSaveRestoreRestoresClipState(t *testing.T) {
	t.Parallel()

	dc := gg.NewContext(32, 32)
	drawer := &GGDrawer{DC: dc}

	drawer.Save()
	drawer.ClipRect(0, 0, 10, 32)
	drawer.Restore()

	drawer.SetFillColor(color.RGBA{R: 255, A: 255})
	drawer.AddCircle(15, 16, 10)
	drawer.Fill()

	require.True(t, pixelHasAlpha(dc.Image(), 15, 16))
}

// TestGGDrawerNestedSaveRestoreRestoresOuterClip verifies gG Drawer Nested Save Restore Restores Outer Clip.
func TestGGDrawerNestedSaveRestoreRestoresOuterClip(t *testing.T) {
	t.Parallel()

	dc := gg.NewContext(32, 32)
	drawer := &GGDrawer{DC: dc}

	drawer.ClipRect(0, 0, 20, 32)

	drawer.Save()
	drawer.ClipRect(0, 0, 10, 32)
	drawer.Restore()

	drawer.SetFillColor(color.RGBA{R: 255, G: 255, A: 255})
	drawer.AddCircle(15, 16, 10)
	drawer.Fill()

	img := dc.Image()

	require.True(t, pixelHasAlpha(img, 15, 16))
	require.False(t, pixelHasAlpha(img, 25, 16))
}

// TestFakePrimitiveDrawerRecordsCommandsAndState verifies fake Primitive Drawer Records Commands And State.
func TestFakePrimitiveDrawerRecordsCommandsAndState(t *testing.T) {
	t.Parallel()

	d := &fakePrimitiveDrawer{}

	d.Save()
	d.ClipRect(1, 2, 30, 40)
	d.SetStrokeColor(color.RGBA{R: 10, G: 20, B: 30, A: 255})
	d.SetFillColor(color.RGBA{R: 40, G: 50, B: 60, A: 255})
	d.SetLineWidth(3)
	d.SetDash(5, 6)
	d.SetDashOffset(7)
	d.AddLine(10, 11, 12, 13)
	d.Stroke()
	d.Restore()

	requireDrawerCommandNames(t, d,
		"Save",
		"ClipRect",
		"SetStrokeColor",
		"SetFillColor",
		"SetLineWidth",
		"SetDash",
		"SetDashOffset",
		"AddLine",
		"Stroke",
		"Restore",
	)

	cmd := requireDrawerSingleCommand(t, d, "AddLine")
	requireCommandArgs(t, cmd, 10, 11, 12, 13)
	requireCommandLineWidth(t, cmd, 3)
	requireCommandDashes(t, cmd, 5, 6)
	requireCommandDashOffset(t, cmd, 7)
	requireCommandClipRects(t, cmd, fakeClipRect{X: 1, Y: 2, W: 30, H: 40})
	require.Equal(t, color.RGBA{R: 10, G: 20, B: 30, A: 255}, cmd.StrokeColor)
	require.Equal(t, color.RGBA{R: 40, G: 50, B: 60, A: 255}, cmd.FillColor)
}

// TestFakePrimitiveDrawerRestoreWithoutSavePanics verifies fake Primitive Drawer Restore Without Save Panics.
func TestFakePrimitiveDrawerRestoreWithoutSavePanics(t *testing.T) {
	t.Parallel()

	d := &fakePrimitiveDrawer{}

	require.Panics(t, func() {
		d.Restore()
	})
}

// TestFakePrimitiveDrawerSaveRestoreRestoresState verifies fake Primitive Drawer Save Restore Restores State.
func TestFakePrimitiveDrawerSaveRestoreRestoresState(t *testing.T) {
	t.Parallel()

	d := &fakePrimitiveDrawer{}

	d.SetLineWidth(1)
	d.Save()
	d.SetLineWidth(9)
	d.ClipRect(1, 2, 3, 4)
	d.Restore()

	state := d.CurrentState()

	require.Equal(t, 1.0, state.LineWidth)
	require.Empty(t, state.Clips)
	require.Equal(t, 0, d.SaveDepth())
}

// TestFakePrimitiveDrawerResetClipClearsOnlyClipState verifies fake Primitive Drawer Reset Clip Clears Only Clip State.
func TestFakePrimitiveDrawerResetClipClearsOnlyClipState(t *testing.T) {
	t.Parallel()

	d := &fakePrimitiveDrawer{}

	d.SetLineWidth(4)
	d.ClipRect(1, 2, 3, 4)
	d.ResetClip()

	state := d.CurrentState()

	require.Equal(t, 4.0, state.LineWidth)
	require.Empty(t, state.Clips)
}

// TestGGDrawerCopyShift_ShiftsPixels verifies gG Drawer Copy Shift Shifts Pixels.
func TestGGDrawerCopyShift_ShiftsPixels(t *testing.T) {
	t.Parallel()

	dc := gg.NewContext(10, 10)
	drawer := &GGDrawer{DC: dc}

	// Draw a single filled point at (1,1).
	drawer.SetFillColor(color.RGBA{R: 255, A: 255})
	drawer.AddPoint(1, 1, 1)
	drawer.Fill()

	// Shift image right by 2 and down by 3.
	drawer.CopyShift(2, 3)

	img := dc.Image()

	// The old pixel near (1,1) should now be present near (3,4).
	// We check alpha only to avoid depending on exact blending.
	_, _, _, a := img.At(3, 4).RGBA()
	require.NotEqual(t, uint32(0), a)

	// A pixel in the newly exposed top-left area should be transparent.
	_, _, _, a2 := img.At(0, 0).RGBA()
	require.Equal(t, uint32(0), a2)
}

// TestGGDrawer_ClearRectTo_DoesNotAffectStrokeState verifies gG Drawer Clear Rect To Does Not Affect Stroke State.
func TestGGDrawer_ClearRectTo_DoesNotAffectStrokeState(t *testing.T) {
	t.Parallel()

	dc := gg.NewContext(40, 20)
	d := &GGDrawer{DC: dc}

	// Fill background to white.
	d.ClearAllTo(color.RGBA{R: 255, G: 255, B: 255, A: 255})

	// Configure stroke to red and draw first line.
	d.SetStrokeColor(color.RGBA{R: 255, A: 255})
	d.SetLineWidth(2)
	d.AddLine(2, 5, 38, 5)
	d.Stroke()

	// Clear a rect in the middle with gray (must not affect stroke state).
	d.ClearRectTo(10, 0, 20, 20, color.RGBA{R: 200, G: 200, B: 200, A: 255})

	// Draw second line WITHOUT reapplying stroke style; it must still be red.
	d.AddLine(2, 15, 38, 15)
	d.Stroke()

	img := dc.Image()

	// Sample a pixel from the second line (y ~15). We expect red channel dominates.
	r, g, b, a := img.At(20, 15).RGBA()
	require.Greater(t, a, uint32(0), "pixel must not be fully transparent")
	require.Greater(t, r, g, "expected red-ish pixel after ClearRectTo")
	require.Greater(t, r, b, "expected red-ish pixel after ClearRectTo")
}

// fakeClipRect describes one clip rectangle in canvas pixel coordinates.
type fakeClipRect struct {
	X, Y float64
	W, H float64
}

// fakeDrawerState stores the active fake drawing state.
// The state is copied on Save and restored on Restore.
type fakeDrawerState struct {
	StrokeColor color.RGBA
	FillColor   color.RGBA
	LineWidth   float64
	Dashes      []float64
	DashOffset  float64
	Clips       []fakeClipRect
}

// clone returns a deep copy of the state.
func (s fakeDrawerState) clone() fakeDrawerState {
	out := s
	out.Dashes = append([]float64(nil), s.Dashes...)
	out.Clips = append([]fakeClipRect(nil), s.Clips...)
	return out
}

// fakeDrawerCommand is one recorded drawer call together with a snapshot
// of the active fake drawing state at the moment of the call.
type fakeDrawerCommand struct {
	Name        string
	Args        []float64
	StrokeColor color.RGBA
	FillColor   color.RGBA
	LineWidth   float64
	Dashes      []float64
	DashOffset  float64
	Clips       []fakeClipRect
}

// String returns a compact debug representation useful in assertion failures.
func (c fakeDrawerCommand) String() string {
	return fmt.Sprintf(
		"%s args=%v stroke=%v fill=%v lineWidth=%v dashes=%v dashOffset=%v clips=%v",
		c.Name,
		c.Args,
		c.StrokeColor,
		c.FillColor,
		c.LineWidth,
		c.Dashes,
		c.DashOffset,
		c.Clips,
	)
}

// fakePrimitiveDrawer is a reusable PrimitiveDrawer test double.
// It records all calls and emulates stateful behavior, including nested
// Save/Restore and clip reset semantics.
type fakePrimitiveDrawer struct {
	commands []fakeDrawerCommand
	state    fakeDrawerState
	stack    []fakeDrawerState
	mu       sync.Mutex
}

// Ensure fakePrimitiveDrawer implements PrimitiveDrawer.
var _ PrimitiveDrawer = (*fakePrimitiveDrawer)(nil)

// rgbaColor converts any color.Color into a comparable RGBA value.
func rgbaColor(c color.Color) color.RGBA {
	if c == nil {
		return color.RGBA{}
	}
	return color.RGBAModel.Convert(c).(color.RGBA)
}

// snapshotCommand records one command together with the current state snapshot.
func (d *fakePrimitiveDrawer) snapshotCommand(name string, args ...float64) {
	cmd := fakeDrawerCommand{
		Name:        name,
		Args:        append([]float64(nil), args...),
		StrokeColor: d.state.StrokeColor,
		FillColor:   d.state.FillColor,
		LineWidth:   d.state.LineWidth,
		Dashes:      append([]float64(nil), d.state.Dashes...),
		DashOffset:  d.state.DashOffset,
		Clips:       append([]fakeClipRect(nil), d.state.Clips...),
	}
	d.commands = append(d.commands, cmd)
}

// Save stores the current fake state.
func (d *fakePrimitiveDrawer) Save() {
	d.stack = append(d.stack, d.state.clone())
	d.snapshotCommand("Save")
}

// Restore restores the most recently saved fake state.
func (d *fakePrimitiveDrawer) Restore() {
	if len(d.stack) == 0 {
		panic("fakePrimitiveDrawer: Restore without matching Save")
	}

	d.state = d.stack[len(d.stack)-1]
	d.stack = d.stack[:len(d.stack)-1]
	d.snapshotCommand("Restore")
}

// ResetClip clears the current fake clip stack.
func (d *fakePrimitiveDrawer) ResetClip() {
	d.state.Clips = nil
	d.snapshotCommand("ResetClip")
}

// ClipRect appends one clip rectangle to the current fake state.
func (d *fakePrimitiveDrawer) ClipRect(x, y, w, h float64) {
	d.state.Clips = append(d.state.Clips, fakeClipRect{X: x, Y: y, W: w, H: h})
	d.snapshotCommand("ClipRect", x, y, w, h)
}

// SetStrokeColor sets the current fake stroke color.
func (d *fakePrimitiveDrawer) SetStrokeColor(c color.Color) {
	d.state.StrokeColor = rgbaColor(c)
	d.snapshotCommand("SetStrokeColor")
}

// SetFillColor sets the current fake fill color.
func (d *fakePrimitiveDrawer) SetFillColor(c color.Color) {
	d.state.FillColor = rgbaColor(c)
	d.snapshotCommand("SetFillColor")
}

// SetLineWidth sets the current fake line width.
func (d *fakePrimitiveDrawer) SetLineWidth(width float64) {
	d.state.LineWidth = width
	d.snapshotCommand("SetLineWidth", width)
}

// SetDash sets the current fake dash pattern.
func (d *fakePrimitiveDrawer) SetDash(dashes ...float64) {
	d.state.Dashes = append([]float64(nil), dashes...)
	d.snapshotCommand("SetDash", dashes...)
}

// SetDashOffset sets the current fake dash offset.
func (d *fakePrimitiveDrawer) SetDashOffset(offset float64) {
	d.state.DashOffset = offset
	d.snapshotCommand("SetDashOffset", offset)
}

// AddPoint records a point path append command.
func (d *fakePrimitiveDrawer) AddPoint(x, y, r float64) {
	d.snapshotCommand("AddPoint", x, y, r)
}

// AddLine records a line path append command.
func (d *fakePrimitiveDrawer) AddLine(x1, y1, x2, y2 float64) {
	d.snapshotCommand("AddLine", x1, y1, x2, y2)
}

// AddCircle records a circle path append command.
func (d *fakePrimitiveDrawer) AddCircle(cx, cy, r float64) {
	d.snapshotCommand("AddCircle", cx, cy, r)
}

// Stroke records a stroke finalization command.
func (d *fakePrimitiveDrawer) Stroke() {
	d.snapshotCommand("Stroke")
}

// Fill records a fill finalization command.
func (d *fakePrimitiveDrawer) Fill() {
	d.snapshotCommand("Fill")
}

// Commands returns a defensive copy of the recorded command log.
func (d *fakePrimitiveDrawer) Commands() []fakeDrawerCommand {
	out := make([]fakeDrawerCommand, len(d.commands))
	copy(out, d.commands)
	return out
}

// CommandNames returns only command names in call order.
func (d *fakePrimitiveDrawer) CommandNames() []string {
	out := make([]string, 0, len(d.commands))
	for _, cmd := range d.commands {
		out = append(out, cmd.Name)
	}
	return out
}

// CommandsByName returns all commands with the given name.
func (d *fakePrimitiveDrawer) CommandsByName(name string) []fakeDrawerCommand {
	var out []fakeDrawerCommand
	for _, cmd := range d.commands {
		if cmd.Name == name {
			out = append(out, cmd)
		}
	}
	return out
}

// LastCommand returns the last recorded command and whether it exists.
func (d *fakePrimitiveDrawer) LastCommand() (fakeDrawerCommand, bool) {
	if len(d.commands) == 0 {
		return fakeDrawerCommand{}, false
	}
	return d.commands[len(d.commands)-1], true
}

// CurrentState returns a defensive copy of the current fake state.
func (d *fakePrimitiveDrawer) CurrentState() fakeDrawerState {
	return d.state.clone()
}

// SaveDepth returns the current Save/Restore nesting depth.
func (d *fakePrimitiveDrawer) SaveDepth() int {
	return len(d.stack)
}

// ResetLog clears only the command log and keeps the current state intact.
func (d *fakePrimitiveDrawer) ResetLog() {
	d.commands = nil
}

func (d *fakePrimitiveDrawer) CopyShift(dx, dy int) {
	d.snapshotCommand("CopyShift", float64(dx), float64(dy))
}

func (d *fakePrimitiveDrawer) ClearAllTo(_ color.Color) {
	// Store as a command; tests usually only care that it was called.
	d.snapshotCommand("ClearAllTo")
}

func (d *fakePrimitiveDrawer) ClearRectTo(x, y, w, h int, _ color.Color) {
	d.snapshotCommand("ClearRectTo", float64(x), float64(y), float64(w), float64(h))
}

func (d *fakePrimitiveDrawer) DrawImage(_ image.Image, x, y int) {
	d.snapshotCommand("DrawImage", float64(x), float64(y))
}

func (d *fakePrimitiveDrawer) DrawImageScaled(_ image.Image, x, y, w, h int) {
	d.snapshotCommand("DrawImageScaled", float64(x), float64(y), float64(w), float64(h))
}
func (d *fakePrimitiveDrawer) Reset() {
	d.mu.Lock()
	defer d.mu.Unlock()
	d.commands = d.commands[:0]
}

// requireDrawerCommandNames asserts the exact command sequence recorded
// by fakePrimitiveDrawer.
func requireDrawerCommandNames(t *testing.T, d *fakePrimitiveDrawer, want ...string) {
	t.Helper()

	require.Equal(t, want, d.CommandNames())
}

// requireDrawerCommandCount asserts the number of recorded commands.
func requireDrawerCommandCount(t *testing.T, d *fakePrimitiveDrawer, want int) {
	t.Helper()

	require.Len(t, d.Commands(), want)
}

// requireDrawerCommandAt returns the command at the specified index.
func requireDrawerCommandAt(t *testing.T, d *fakePrimitiveDrawer, index int) fakeDrawerCommand {
	t.Helper()

	cmds := d.Commands()
	require.GreaterOrEqual(t, index, 0)
	require.Less(t, index, len(cmds))

	return cmds[index]
}

// requireDrawerSingleCommand returns the only command with the given name.
func requireDrawerSingleCommand(t *testing.T, d *fakePrimitiveDrawer, name string) fakeDrawerCommand {
	t.Helper()

	cmds := d.CommandsByName(name)
	require.Len(t, cmds, 1)

	return cmds[0]
}

// requireCommandName asserts the command name.
func requireCommandName(t *testing.T, cmd fakeDrawerCommand, want string) {
	t.Helper()

	require.Equal(t, want, cmd.Name)
}

// requireCommandArgs asserts the exact float arguments.
func requireCommandArgs(t *testing.T, cmd fakeDrawerCommand, want ...float64) {
	t.Helper()

	require.Equal(t, want, cmd.Args)
}

// requireCommandArgsInDelta asserts the float arguments with tolerance.
func requireCommandArgsInDelta(t *testing.T, cmd fakeDrawerCommand, delta float64, want ...float64) {
	t.Helper()

	require.Len(t, cmd.Args, len(want))
	for i := range want {
		require.InDelta(t, want[i], cmd.Args[i], delta, "arg index %d", i)
	}
}

// requireCommandClipRects asserts the clip stack snapshot attached to the command.
func requireCommandClipRects(t *testing.T, cmd fakeDrawerCommand, want ...fakeClipRect) {
	t.Helper()

	require.Equal(t, want, cmd.Clips)
}

// requireCommandLineWidth asserts the line width snapshot attached to the command.
func requireCommandLineWidth(t *testing.T, cmd fakeDrawerCommand, want float64) {
	t.Helper()

	require.Equal(t, want, cmd.LineWidth)
}

// requireCommandDashes asserts the dash snapshot attached to the command.
func requireCommandDashes(t *testing.T, cmd fakeDrawerCommand, want ...float64) {
	t.Helper()

	require.Equal(t, want, cmd.Dashes)
}

// requireCommandDashOffset asserts the dash offset snapshot attached to the command.
func requireCommandDashOffset(t *testing.T, cmd fakeDrawerCommand, want float64) {
	t.Helper()

	require.Equal(t, want, cmd.DashOffset)
}
