package webui

import (
	"testing"

	"kjol/wasmruntime"
)

// A 1024x768 viewport with a 100x40 trigger in the middle, and a 200x100 panel.
var (
	vp      = wasmruntime.Size{Width: 1024, Height: 768}
	trigger = wasmruntime.Rect{X: 400, Y: 300, Width: 100, Height: 40}
	panel   = wasmruntime.Rect{X: 0, Y: 0, Width: 200, Height: 100}
)

func opts(placement string) PositionOptions {
	o := DefaultPositionOptions()
	o.Placement = placement
	return o
}

func TestPlacements(t *testing.T) {
	// trigger: left=400 right=500 centerX=450, top=300 bottom=340 centerY=320
	// panel: 200x100. offset 4.
	cases := []struct {
		placement string
		top, left float64
	}{
		{PlacementBottomStart, 344, 400}, // below, left edges aligned
		{PlacementBottom, 344, 350},      // below, centred: 450 - 100
		{PlacementBottomEnd, 344, 300},   // below, right edges aligned: 500 - 200
		{PlacementTopStart, 196, 400},    // above: 300 - 100 - 4
		{PlacementTop, 196, 350},
		{PlacementTopEnd, 196, 300},
		{PlacementRightStart, 300, 504}, // right of: 500 + 4
		{PlacementRight, 270, 504},      // right, centred: 320 - 50
		{PlacementRightEnd, 240, 504},   // right, bottom-aligned: 340 - 100
		{PlacementLeftStart, 300, 196},  // left of: 400 - 200 - 4
		{PlacementLeft, 270, 196},
		{PlacementLeftEnd, 240, 196},
	}
	for _, c := range cases {
		got := ComputePosition(trigger, panel, vp, opts(c.placement))
		if got.Top != c.top || got.Left != c.left {
			t.Errorf("%s: got top=%v left=%v, want top=%v left=%v", c.placement, got.Top, got.Left, c.top, c.left)
		}
		if got.Placement != c.placement {
			t.Errorf("%s: resolved placement changed to %s with room to spare", c.placement, got.Placement)
		}
	}
}

func TestFlipWhenItDoesNotFit(t *testing.T) {
	// Trigger near the bottom: a bottom-placed panel would run off, so flip up.
	low := wasmruntime.Rect{X: 400, Y: 700, Width: 100, Height: 40}
	got := ComputePosition(low, panel, vp, opts(PlacementBottomStart))
	if got.Placement != PlacementTopStart {
		t.Fatalf("placement = %s, want top-start (should have flipped)", got.Placement)
	}
	if got.Top != 596 { // 700 - 100 - 4
		t.Errorf("top = %v, want 596", got.Top)
	}

	// The alignment suffix must survive the flip.
	if p := ComputePosition(low, panel, vp, opts(PlacementBottomEnd)).Placement; p != PlacementTopEnd {
		t.Errorf("bottom-end flipped to %s, want top-end", p)
	}

	// Trigger near the right edge: right-placed flips to left.
	right := wasmruntime.Rect{X: 950, Y: 300, Width: 60, Height: 40}
	if p := ComputePosition(right, panel, vp, opts(PlacementRightStart)).Placement; p != PlacementLeftStart {
		t.Errorf("placement = %s, want left-start", p)
	}
}

// If neither side fits, keep the requested one rather than flipping into an
// equally bad spot.
func TestNoFlipWhenNeitherSideFits(t *testing.T) {
	tall := wasmruntime.Rect{X: 0, Y: 0, Width: 200, Height: 700}
	mid := wasmruntime.Rect{X: 400, Y: 350, Width: 100, Height: 40}
	got := ComputePosition(mid, tall, vp, opts(PlacementBottomStart))
	if got.Placement != PlacementBottomStart {
		t.Errorf("placement = %s, want bottom-start (no better side available)", got.Placement)
	}
}

// The bug we are deliberately fixing: the TSX clamped BOTH axes, so a panel that
// did not fit below got pushed up over the trigger it belonged to. Shift must
// leave the main axis alone.
func TestShiftDoesNotCoverTheTrigger(t *testing.T) {
	tall := wasmruntime.Rect{X: 0, Y: 0, Width: 200, Height: 700}
	low := wasmruntime.Rect{X: 400, Y: 700, Width: 100, Height: 40}
	got := ComputePosition(low, tall, vp, opts(PlacementBottomStart))

	if got.Top < low.Bottom() {
		t.Errorf("panel top %v is above the trigger's bottom %v — it is covering its own trigger", got.Top, low.Bottom())
	}
}

func TestShiftClampsTheCrossAxis(t *testing.T) {
	// Trigger hard against the right edge; a bottom-start panel would overflow.
	edge := wasmruntime.Rect{X: 980, Y: 300, Width: 40, Height: 40}
	got := ComputePosition(edge, panel, vp, opts(PlacementBottomStart))

	wantLeft := vp.Width - panel.Width - 8 // 1024 - 200 - padding
	if got.Left != wantLeft {
		t.Errorf("left = %v, want %v (clamped to the viewport)", got.Left, wantLeft)
	}

	// And against the left edge.
	edge = wasmruntime.Rect{X: 2, Y: 300, Width: 40, Height: 40}
	if got := ComputePosition(edge, panel, vp, opts(PlacementBottomEnd)); got.Left != 8 {
		t.Errorf("left = %v, want 8 (clamped to the padding)", got.Left)
	}
}

// The second flaw we fix: after a shift, the arrow must still point at the trigger.
func TestArrowTracksTheTriggerAfterShift(t *testing.T) {
	o := opts(PlacementBottom)
	o.ArrowSize = 8

	// Unshifted, centred: the arrow sits at the panel's middle.
	got := ComputePosition(trigger, panel, vp, o)
	if got.ArrowOffset != 100 { // panel is 200 wide
		t.Errorf("centred arrow offset = %v, want 100", got.ArrowOffset)
	}

	// Hard against the right edge, the panel gets shifted left but the trigger did
	// not move — so the arrow must slide right to keep pointing at it.
	edge := wasmruntime.Rect{X: 980, Y: 300, Width: 40, Height: 40} // centre x = 1000
	got = ComputePosition(edge, panel, vp, o)
	wantOffset := edge.CenterX() - got.Left // 1000 - 816 = 184
	if got.ArrowOffset != wantOffset {
		t.Errorf("arrow offset = %v, want %v (pointing at the trigger centre)", got.ArrowOffset, wantOffset)
	}
	if got.ArrowOffset <= 100 {
		t.Error("arrow did not move toward the trigger after the panel was shifted")
	}
}

// The arrow must never leave the panel, however far the trigger is.
func TestArrowIsClampedToThePanel(t *testing.T) {
	o := opts(PlacementBottomStart)
	o.ArrowSize = 8
	o.ArrowPadding = 4

	far := wasmruntime.Rect{X: 1000, Y: 300, Width: 20, Height: 40}
	got := ComputePosition(far, panel, vp, o)

	lo, hi := 8.0, panel.Width-8 // padding + half the arrow, at both ends
	if got.ArrowOffset < lo || got.ArrowOffset > hi {
		t.Errorf("arrow offset %v escaped the panel [%v, %v]", got.ArrowOffset, lo, hi)
	}
}

// The third fix: tell the caller how much room there is, so a long menu can scroll
// instead of running off the screen.
func TestAvailableSpace(t *testing.T) {
	got := ComputePosition(trigger, panel, vp, opts(PlacementBottomStart))
	want := vp.Height - trigger.Bottom() - 4 - 8 // viewport - trigger bottom - offset - padding
	if got.MaxHeight != want {
		t.Errorf("MaxHeight = %v, want %v", got.MaxHeight, want)
	}
	if got.MaxWidth != 0 {
		t.Errorf("MaxWidth = %v, want 0 (unconstrained on the cross axis)", got.MaxWidth)
	}

	got = ComputePosition(trigger, panel, vp, opts(PlacementRightStart))
	if got.MaxWidth != vp.Width-trigger.Right()-12 {
		t.Errorf("MaxWidth = %v, want %v", got.MaxWidth, vp.Width-trigger.Right()-12)
	}
	if got.MaxHeight != 0 {
		t.Errorf("MaxHeight = %v, want 0", got.MaxHeight)
	}
}

func TestOppositeSide(t *testing.T) {
	for placement, want := range map[string]string{
		PlacementTopStart: "bottom",
		PlacementBottom:   "top",
		PlacementLeftEnd:  "right",
		PlacementRight:    "left",
	} {
		if got := OppositeSide(placement); got != want {
			t.Errorf("OppositeSide(%s) = %s, want %s", placement, got, want)
		}
	}
}

func TestUnknownPlacementFallsBackToBottom(t *testing.T) {
	got := ComputePosition(trigger, panel, vp, opts("nonsense"))
	if got.Placement != "bottom-center" && got.Placement != "bottom" {
		t.Errorf("placement = %s, want a bottom variant", got.Placement)
	}
	if got.Top != 344 {
		t.Errorf("top = %v, want 344 (positioned below)", got.Top)
	}
}
