package client

import (
	"image"
	"testing"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/test"

	"github.com/stretchr/testify/require"

	"galaxy/client/world"
)

type fakeClient struct {
	scale float32
	p     world.RenderParams

	forced  bool
	updates int
	refresh int
}

func (e *fakeClient) CanvasScale() float32 { return e.scale }

func (e *fakeClient) UpdateParams(fn func(p *world.RenderParams)) {
	fn(&e.p)
	e.updates++
}

func (e *fakeClient) RequestRefresh() { e.refresh++ }

func (e *fakeClient) ForceFullRedraw() { e.forced = true }

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

	fe := &fakeClient{
		scale: 1.0, // 1 fyne unit == 1 px for the test
		p: world.RenderParams{
			CameraZoom:     1.0,
			CameraXWorldFp: 5 * world.SCALE,
			CameraYWorldFp: 5 * world.SCALE,
		},
	}

	pc := NewPanController(fe)

	// Drag right by +3 px and down by +2 px.
	pc.Dragged(&fyne.DragEvent{
		Dragged: fyne.Delta{DX: 3, DY: 2},
	})

	require.Equal(t, 1, fe.updates)

	// Map follows pointer => camera moves opposite to pointer delta.
	require.Equal(t, 5*world.SCALE-3*world.SCALE, fe.p.CameraXWorldFp)
	require.Equal(t, 5*world.SCALE-2*world.SCALE, fe.p.CameraYWorldFp)
}

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

	fe := &fakeClient{
		scale: 2.0, // 2 px per fyne unit
		p: world.RenderParams{
			CameraZoom:     1.0,
			CameraXWorldFp: 0,
			CameraYWorldFp: 0,
		},
	}

	pc := NewPanController(fe)

	// Dragged.DX=1 fyne unit => 2 px after scaling.
	pc.Dragged(&fyne.DragEvent{
		Dragged: fyne.Delta{DX: 1, DY: 0},
	})

	require.Equal(t, -2*world.SCALE, fe.p.CameraXWorldFp)
}

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

	fe := &fakeClient{
		scale: 1.0,
		p: world.RenderParams{
			CameraZoom:     1.0,
			CameraXWorldFp: 0,
			CameraYWorldFp: 0,
		},
	}

	pc := NewPanController(fe)

	// Simulate a drag start.
	pc.Dragged(&fyne.DragEvent{PointEvent: fyne.PointEvent{Position: fyne.Position{X: 1, Y: 1}}})

	pc.DragEnd()
	require.True(t, fe.forced)
	require.Equal(t, 1, fe.refresh)
}

// Optional: demonstrate use of fyne/test package to ensure types are available.
// (Not strictly needed, but keeps fyne dependency "active" in tests.)
func TestFyneTestDriverIsUsable(t *testing.T) {
	t.Parallel()
	_ = test.NewApp()
}

type immediateExecutor struct{}

func (immediateExecutor) Post(fn func()) {
	if fn != nil {
		fn()
	}
}

type noopRefresher struct{}

func (noopRefresher) Refresh() {}

func newZoomSyncTestClient(t *testing.T, worldW, worldH int, cameraZoom float64) *client {
	t.Helper()

	w := world.NewWorld(worldW, worldH)
	e := &client{
		world:  w,
		drawer: &world.GGDrawer{},
		wp: &world.RenderParams{
			CameraZoom:     cameraZoom,
			CameraXWorldFp: w.W / 2,
			CameraYWorldFp: w.H / 2,
			Options:        &world.RenderOptions{DisableWrapScroll: false},
		},
		hits: make([]world.Hit, 5),
	}

	e.co = NewRasterCoalescer(
		immediateExecutor{},
		noopRefresher{},
		func(wPx, hPx int, _ world.RenderParams) image.Image {
			return image.NewRGBA(image.Rect(0, 0, wPx, hPx))
		},
	)

	return e
}

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

	e := newZoomSyncTestClient(t, 10, 10, 1.0)
	p := *e.wp

	correctedZoom := e.world.CorrectCameraZoom(p.CameraZoom, 100, 100)
	require.NotEqual(t, p.CameraZoom, correctedZoom)

	_ = e.renderRasterImage(100, 100, p)

	require.Equal(t, correctedZoom, e.wp.CameraZoom)
}

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

	e := newZoomSyncTestClient(t, 10, 10, 1.0)
	p := *e.wp

	// Simulate a newer UI update that happened after this render snapshot was taken.
	e.wp.CameraZoom = 3.0

	_ = e.renderRasterImage(100, 100, p)

	require.Equal(t, 3.0, e.wp.CameraZoom)
}

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

	e := newZoomSyncTestClient(t, 10, 10, 1.0)

	// Initial render corrects zoom and syncs it into base params.
	_ = e.renderRasterImage(100, 100, *e.wp)

	syncedZoom := e.wp.CameraZoom
	require.NotEqual(t, 1.0, syncedZoom)

	zoomFp, err := world.CameraZoomToWorldFixed(syncedZoom)
	require.NoError(t, err)

	startX := e.wp.CameraXWorldFp
	pan := NewPanController(e)
	pan.Dragged(&fyne.DragEvent{
		Dragged: fyne.Delta{DX: 1, DY: 0},
	})

	expectedShift := world.PixelSpanToWorldFixed(1, zoomFp)
	require.Equal(t, startX-expectedShift, e.wp.CameraXWorldFp)
}
