package world

import (
	"testing"

	"a301_game_server/internal/entity"
	"a301_game_server/pkg/mathutil"
	pb "a301_game_server/proto/gen/pb"
)

// mockEntity is a minimal entity for testing.
type mockEntity struct {
	id  uint64
	pos mathutil.Vec3
}

func (m *mockEntity) EntityID() uint64           { return m.id }
func (m *mockEntity) EntityType() entity.Type    { return entity.TypePlayer }
func (m *mockEntity) Position() mathutil.Vec3    { return m.pos }
func (m *mockEntity) SetPosition(p mathutil.Vec3) { m.pos = p }
func (m *mockEntity) Rotation() float32          { return 0 }
func (m *mockEntity) SetRotation(float32)        {}
func (m *mockEntity) ToProto() *pb.EntityState   { return &pb.EntityState{EntityId: m.id} }

func TestBroadcastAllAOI_GetNearby(t *testing.T) {
	aoi := NewBroadcastAllAOI()

	e1 := &mockEntity{id: 1, pos: mathutil.NewVec3(0, 0, 0)}
	e2 := &mockEntity{id: 2, pos: mathutil.NewVec3(100, 0, 100)}
	e3 := &mockEntity{id: 3, pos: mathutil.NewVec3(999, 0, 999)}

	aoi.Add(e1)
	aoi.Add(e2)
	aoi.Add(e3)

	// With broadcast-all, everyone sees everyone.
	nearby := aoi.GetNearby(e1)
	if len(nearby) != 2 {
		t.Errorf("expected 2 nearby, got %d", len(nearby))
	}
}

func TestBroadcastAllAOI_Remove(t *testing.T) {
	aoi := NewBroadcastAllAOI()

	e1 := &mockEntity{id: 1}
	e2 := &mockEntity{id: 2}
	aoi.Add(e1)
	aoi.Add(e2)

	events := aoi.Remove(e1)
	if len(events) != 1 {
		t.Errorf("expected 1 leave event, got %d", len(events))
	}
	if events[0].Type != AOILeave {
		t.Errorf("expected AOILeave event")
	}

	nearby := aoi.GetNearby(e2)
	if len(nearby) != 0 {
		t.Errorf("expected 0 nearby after removal, got %d", len(nearby))
	}
}

func TestGridAOI_NearbyInSameCell(t *testing.T) {
	aoi := NewGridAOI(50, 2)

	e1 := &mockEntity{id: 1, pos: mathutil.NewVec3(10, 0, 10)}
	e2 := &mockEntity{id: 2, pos: mathutil.NewVec3(20, 0, 20)}

	aoi.Add(e1)
	aoi.Add(e2)

	nearby := aoi.GetNearby(e1)
	if len(nearby) != 1 {
		t.Errorf("expected 1 nearby, got %d", len(nearby))
	}
	if nearby[0].EntityID() != 2 {
		t.Errorf("expected entity 2, got %d", nearby[0].EntityID())
	}
}

func TestGridAOI_FarAwayNotVisible(t *testing.T) {
	aoi := NewGridAOI(50, 1) // viewRange=1 means 3x3 grid = 150 units visibility

	e1 := &mockEntity{id: 1, pos: mathutil.NewVec3(0, 0, 0)}
	e2 := &mockEntity{id: 2, pos: mathutil.NewVec3(500, 0, 500)} // far away

	aoi.Add(e1)
	aoi.Add(e2)

	nearby := aoi.GetNearby(e1)
	if len(nearby) != 0 {
		t.Errorf("expected 0 nearby for far entity, got %d", len(nearby))
	}
}

func TestGridAOI_MoveGeneratesEvents(t *testing.T) {
	aoi := NewGridAOI(50, 1)

	e1 := &mockEntity{id: 1, pos: mathutil.NewVec3(0, 0, 0)}
	e2 := &mockEntity{id: 2, pos: mathutil.NewVec3(200, 0, 200)}

	aoi.Add(e1)
	aoi.Add(e2)

	// Initially not visible to each other.
	nearby := aoi.GetNearby(e1)
	if len(nearby) != 0 {
		t.Fatalf("expected not visible initially, got %d", len(nearby))
	}

	// Move e2 close to e1.
	oldPos := e2.pos
	e2.pos = mathutil.NewVec3(10, 0, 10)
	events := aoi.UpdatePosition(e2, oldPos, e2.pos)

	// Should generate enter events (e1 sees e2, e2 sees e1).
	enterCount := 0
	for _, evt := range events {
		if evt.Type == AOIEnter {
			enterCount++
		}
	}
	if enterCount != 2 {
		t.Errorf("expected 2 enter events, got %d", enterCount)
	}
}

func TestGridAOI_ToggleComparison(t *testing.T) {
	// Demonstrates the difference between BroadcastAll and Grid AOI.
	e1 := &mockEntity{id: 1, pos: mathutil.NewVec3(0, 0, 0)}
	e2 := &mockEntity{id: 2, pos: mathutil.NewVec3(500, 0, 500)}

	// BroadcastAll: both visible
	broadcast := NewBroadcastAllAOI()
	broadcast.Add(e1)
	broadcast.Add(e2)
	if len(broadcast.GetNearby(e1)) != 1 {
		t.Error("broadcast-all should see all entities")
	}

	// Grid: e2 not visible from e1 (too far)
	grid := NewGridAOI(50, 1)
	grid.Add(e1)
	grid.Add(e2)
	if len(grid.GetNearby(e1)) != 0 {
		t.Error("grid AOI should NOT see distant entities")
	}
}
