package zone

import (
	"database/sql"
	"testing"
	"time"

	"eq2emu/internal/database"

	_ "zombiezen.com/go/sqlite"
)

// TestZoneCreation tests basic zone server creation
func TestZoneCreation(t *testing.T) {
	zoneName := "test_zone"
	zoneServer := NewZoneServer(zoneName)

	if zoneServer == nil {
		t.Fatal("Expected non-nil zone server")
	}

	if zoneServer.GetZoneName() != zoneName {
		t.Errorf("Expected zone name '%s', got '%s'", zoneName, zoneServer.GetZoneName())
	}

	if zoneServer.IsInitialized() {
		t.Error("Expected zone to not be initialized")
	}

	if zoneServer.IsShuttingDown() {
		t.Error("Expected zone to not be shutting down")
	}
}

// TestZoneInitialization tests zone server initialization
func TestZoneInitialization(t *testing.T) {
	zoneServer := NewZoneServer("test_zone")

	config := &ZoneServerConfig{
		ZoneName:          "test_zone",
		ZoneFile:          "test.zone",
		ZoneDescription:   "Test Zone",
		ZoneID:            1,
		InstanceID:        0,
		InstanceType:      InstanceTypeNone,
		MaxPlayers:        100,
		MinLevel:          1,
		MaxLevel:          100,
		SafeX:             0.0,
		SafeY:             0.0,
		SafeZ:             0.0,
		SafeHeading:       0.0,
		LoadMaps:          false, // Don't load maps in tests
		EnableWeather:     false, // Don't enable weather in tests
		EnablePathfinding: false, // Don't enable pathfinding in tests
	}

	err := zoneServer.Initialize(config)
	if err != nil {
		t.Fatalf("Failed to initialize zone server: %v", err)
	}

	if !zoneServer.IsInitialized() {
		t.Error("Expected zone to be initialized")
	}

	if zoneServer.GetZoneID() != 1 {
		t.Errorf("Expected zone ID 1, got %d", zoneServer.GetZoneID())
	}

	if zoneServer.GetInstanceID() != 0 {
		t.Errorf("Expected instance ID 0, got %d", zoneServer.GetInstanceID())
	}

	// Test safe position
	x, y, z, heading := zoneServer.GetSafePosition()
	if x != 0.0 || y != 0.0 || z != 0.0 || heading != 0.0 {
		t.Errorf("Expected safe position (0,0,0,0), got (%.2f,%.2f,%.2f,%.2f)", x, y, z, heading)
	}
}

// TestPositionCalculations tests position utility functions
func TestPositionCalculations(t *testing.T) {
	// Test 2D distance
	distance := Distance2D(0, 0, 3, 4)
	if distance != 5.0 {
		t.Errorf("Expected 2D distance 5.0, got %.2f", distance)
	}

	// Test 3D distance
	distance3d := Distance3D(0, 0, 0, 3, 4, 12)
	if distance3d != 13.0 {
		t.Errorf("Expected 3D distance 13.0, got %.2f", distance3d)
	}

	// Test heading calculation
	heading := CalculateHeading(0, 0, 1, 1)
	expected := float32(64.0) // 45 degrees in EQ2 heading units (512/8)
	if abs(heading-expected) > 1.0 {
		t.Errorf("Expected heading %.2f, got %.2f", expected, heading)
	}

	// Test heading normalization
	normalized := NormalizeHeading(600.0)
	expected = 88.0 // 600 - 512
	if normalized != expected {
		t.Errorf("Expected normalized heading %.2f, got %.2f", expected, normalized)
	}
}

// TestPositionStructs tests position data structures
func TestPositionStructs(t *testing.T) {
	pos1 := NewPosition(10.0, 20.0, 30.0, 128.0)
	pos2 := NewPosition(13.0, 24.0, 30.0, 128.0)

	// Test distance calculation
	distance := pos1.DistanceTo3D(pos2)
	expected := float32(5.0) // 3-4-5 triangle
	if distance != expected {
		t.Errorf("Expected distance %.2f, got %.2f", expected, distance)
	}

	// Test position copy
	posCopy := pos1.Copy()
	if !pos1.Equals(posCopy) {
		t.Error("Expected copied position to equal original")
	}

	// Test bounding box
	bbox := NewBoundingBox(0, 0, 0, 10, 10, 10)
	if !bbox.Contains(5, 5, 5) {
		t.Error("Expected bounding box to contain point (5,5,5)")
	}
	if bbox.Contains(15, 5, 5) {
		t.Error("Expected bounding box to not contain point (15,5,5)")
	}
}

// TestMovementManager tests the movement management system
func TestMovementManager(t *testing.T) {
	// Create a test zone
	zoneServer := NewZoneServer("test_zone")
	config := &ZoneServerConfig{
		ZoneName:          "test_zone",
		ZoneID:            1,
		LoadMaps:          false,
		EnableWeather:     false,
		EnablePathfinding: false,
	}

	err := zoneServer.Initialize(config)
	if err != nil {
		t.Fatalf("Failed to initialize zone server: %v", err)
	}

	// Test movement manager creation
	movementMgr := NewMobMovementManager(zoneServer)
	if movementMgr == nil {
		t.Fatal("Expected non-nil movement manager")
	}

	// Test adding a spawn to movement tracking
	spawnID := int32(1001)
	movementMgr.AddMovementSpawn(spawnID)

	if !movementMgr.IsMoving(spawnID) == false {
		// IsMoving should be false initially
	}

	// Test queueing a movement command
	err = movementMgr.MoveTo(spawnID, 10.0, 20.0, 30.0, DefaultRunSpeed)
	if err != nil {
		t.Errorf("Failed to queue movement command: %v", err)
	}

	// Test getting movement state
	state := movementMgr.GetMovementState(spawnID)
	if state == nil {
		t.Error("Expected non-nil movement state")
	} else if state.SpawnID != spawnID {
		t.Errorf("Expected spawn ID %d, got %d", spawnID, state.SpawnID)
	}
}

// TestInstanceTypes tests instance type functionality
func TestInstanceTypes(t *testing.T) {
	testCases := []struct {
		instanceType InstanceType
		expected     string
	}{
		{InstanceTypeNone, "None"},
		{InstanceTypeGroupLockout, "Group Lockout"},
		{InstanceTypeRaidPersist, "Raid Persistent"},
		{InstanceTypePersonalHouse, "Personal House"},
	}

	for _, tc := range testCases {
		result := tc.instanceType.String()
		if result != tc.expected {
			t.Errorf("Expected instance type string '%s', got '%s'", tc.expected, result)
		}
	}
}

// TestZoneManager tests the zone manager functionality
func TestZoneManager(t *testing.T) {
	// Create test database
	db, err := sql.Open("sqlite", ":memory:")
	if err != nil {
		t.Fatalf("Failed to create test database: %v", err)
	}
	defer db.Close()

	// Create test schema
	schema := `
	CREATE TABLE zones (
		id INTEGER PRIMARY KEY,
		name TEXT NOT NULL,
		file TEXT,
		description TEXT,
		safe_x REAL DEFAULT 0,
		safe_y REAL DEFAULT 0,
		safe_z REAL DEFAULT 0,
		safe_heading REAL DEFAULT 0,
		underworld REAL DEFAULT -1000,
		min_level INTEGER DEFAULT 0,
		max_level INTEGER DEFAULT 0,
		min_status INTEGER DEFAULT 0,
		min_version INTEGER DEFAULT 0,
		instance_type INTEGER DEFAULT 0,
		max_players INTEGER DEFAULT 100,
		default_lockout_time INTEGER DEFAULT 18000,
		default_reenter_time INTEGER DEFAULT 3600,
		default_reset_time INTEGER DEFAULT 259200,
		group_zone_option INTEGER DEFAULT 0,
		expansion_flag INTEGER DEFAULT 0,
		holiday_flag INTEGER DEFAULT 0,
		can_bind INTEGER DEFAULT 1,
		can_gate INTEGER DEFAULT 1,
		can_evac INTEGER DEFAULT 1,
		city_zone INTEGER DEFAULT 0,
		always_loaded INTEGER DEFAULT 0,
		weather_allowed INTEGER DEFAULT 1
	);

	INSERT INTO zones (id, name, file, description) VALUES (1, 'test_zone', 'test.zone', 'Test Zone');
	`

	if _, err := db.Exec(schema); err != nil {
		t.Fatalf("Failed to create test schema: %v", err)
	}

	// Create database wrapper
	dbWrapper := &database.Database{DB: db}

	// Create zone manager
	config := &ZoneManagerConfig{
		MaxZones:           10,
		MaxInstanceZones:   50,
		ProcessInterval:    time.Millisecond * 100,
		CleanupInterval:    time.Second * 1,
		EnableWeather:      false,
		EnablePathfinding:  false,
		EnableCombat:       false,
		EnableSpellProcess: false,
	}

	zoneManager := NewZoneManager(config, dbWrapper)
	if zoneManager == nil {
		t.Fatal("Expected non-nil zone manager")
	}

	// Test zone count initially
	if zoneManager.GetZoneCount() != 0 {
		t.Errorf("Expected 0 zones initially, got %d", zoneManager.GetZoneCount())
	}

	// Note: Full zone manager testing would require more complex setup
	// including proper database schema and mock implementations
}

// TestWeatherSystem tests weather functionality
func TestWeatherSystem(t *testing.T) {
	zoneServer := NewZoneServer("test_zone")

	// Initialize with weather enabled
	config := &ZoneServerConfig{
		ZoneName:          "test_zone",
		ZoneID:            1,
		LoadMaps:          false,
		EnableWeather:     true,
		EnablePathfinding: false,
	}

	err := zoneServer.Initialize(config)
	if err != nil {
		t.Fatalf("Failed to initialize zone server: %v", err)
	}

	// Test setting rain level
	zoneServer.SetRain(0.5)

	// Test weather processing (this is mostly internal)
	zoneServer.ProcessWeather()

	// Weather system would need more sophisticated testing with time control
}

// TestConstants tests various constants are properly defined
func TestConstants(t *testing.T) {
	// Test distance constants
	if SendSpawnDistance != 250.0 {
		t.Errorf("Expected SendSpawnDistance 250.0, got %.2f", SendSpawnDistance)
	}

	if MaxChaseDistance != 80.0 {
		t.Errorf("Expected MaxChaseDistance 80.0, got %.2f", MaxChaseDistance)
	}

	// Test expansion constants
	if ExpansionDOF != 1024 {
		t.Errorf("Expected ExpansionDOF 1024, got %d", ExpansionDOF)
	}

	// Test EQ2 heading constant
	if EQ2HeadingMax != 512.0 {
		t.Errorf("Expected EQ2HeadingMax 512.0, got %.2f", EQ2HeadingMax)
	}
}

// BenchmarkDistanceCalculation benchmarks distance calculations
func BenchmarkDistanceCalculation(b *testing.B) {
	x1, y1, z1 := float32(100.0), float32(200.0), float32(300.0)
	x2, y2, z2 := float32(150.0), float32(250.0), float32(350.0)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		Distance3D(x1, y1, z1, x2, y2, z2)
	}
}

// BenchmarkPositionDistance benchmarks position-based distance calculations
func BenchmarkPositionDistance(b *testing.B) {
	pos1 := NewPosition(100.0, 200.0, 300.0, 128.0)
	pos2 := NewPosition(150.0, 250.0, 350.0, 256.0)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		pos1.DistanceTo3D(pos2)
	}
}

// BenchmarkHeadingCalculation benchmarks heading calculations
func BenchmarkHeadingCalculation(b *testing.B) {
	fromX, fromY := float32(0.0), float32(0.0)
	toX, toY := float32(100.0), float32(100.0)

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		CalculateHeading(fromX, fromY, toX, toY)
	}
}

// MockClient implements the Client interface for testing
type MockClient struct {
	id            uint32
	characterID   int32
	playerName    string
	position      *Position
	clientVersion int32
	loadingZone   bool
	inCombat      bool
	spawnRange    float32
	languageID    int32
}

func NewMockClient(id uint32, name string) *MockClient {
	return &MockClient{
		id:            id,
		characterID:   int32(id),
		playerName:    name,
		position:      NewPosition(0, 0, 0, 0),
		clientVersion: DefaultClientVersion,
		loadingZone:   false,
		inCombat:      false,
		spawnRange:    SendSpawnDistance,
		languageID:    0,
	}
}

func (mc *MockClient) GetID() uint32                           { return mc.id }
func (mc *MockClient) GetCharacterID() int32                   { return mc.characterID }
func (mc *MockClient) GetPlayerName() string                   { return mc.playerName }
func (mc *MockClient) GetPlayer() Player                       { return nil } // TODO: Mock player
func (mc *MockClient) GetClientVersion() int32                 { return mc.clientVersion }
func (mc *MockClient) IsLoadingZone() bool                     { return mc.loadingZone }
func (mc *MockClient) SendPacket(data []byte) error            { return nil }
func (mc *MockClient) GetSpawnRange() float32                  { return mc.spawnRange }
func (mc *MockClient) IsInCombat() bool                        { return mc.inCombat }
func (mc *MockClient) GetLanguageID() int32                    { return mc.languageID }
func (mc *MockClient) GetLanguageSkill(languageID int32) int32 { return 100 }

func (mc *MockClient) GetPosition() (x, y, z, heading float32, zoneID int32) {
	return mc.position.X, mc.position.Y, mc.position.Z, mc.position.Heading, 1
}

func (mc *MockClient) SetPosition(x, y, z, heading float32, zoneID int32) {
	mc.position.Set(x, y, z, heading)
}

func (mc *MockClient) CanSeeSpawn(spawn *Spawn) bool {
	// Simple visibility check based on distance
	spawnX, spawnY, spawnZ, _ := spawn.GetPosition()
	distance := Distance3D(mc.position.X, mc.position.Y, mc.position.Z, spawnX, spawnY, spawnZ)
	return distance <= mc.spawnRange
}

// Placeholder import fix
type Spawn = interface {
	GetID() int32
	GetPosition() (x, y, z, heading float32)
}
