package loot

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

	"eq2emu/internal/items"

	_ "zombiezen.com/go/sqlite"
)

// Test helper functions and mock implementations

// MockItemMasterList implements items.MasterItemListService for testing
type MockItemMasterList struct {
	items map[int32]*items.Item
}

func NewMockItemMasterList() *MockItemMasterList {
	return &MockItemMasterList{
		items: make(map[int32]*items.Item),
	}
}

func (m *MockItemMasterList) GetItem(itemID int32) *items.Item {
	return m.items[itemID]
}

func (m *MockItemMasterList) AddTestItem(itemID int32, name string, tier int8) {
	item := &items.Item{
		Name: name,
		Details: items.ItemDetails{
			ItemID: itemID,
			Tier:   tier,
		},
		GenericInfo: items.ItemGenericInfo{
			ItemType: items.ItemTypeNormal,
		},
	}
	item.Details.UniqueID = items.NextUniqueItemID()
	m.items[itemID] = item
}

// MockPlayerService implements PlayerService for testing
type MockPlayerService struct {
	playerPositions map[uint32][5]float32 // x, y, z, heading, zoneID
	inventorySpace  map[uint32]int
	combat          map[uint32]bool
	skills          map[uint32]map[string]int32
}

func NewMockPlayerService() *MockPlayerService {
	return &MockPlayerService{
		playerPositions: make(map[uint32][5]float32),
		inventorySpace:  make(map[uint32]int),
		combat:          make(map[uint32]bool),
		skills:          make(map[uint32]map[string]int32),
	}
}

func (m *MockPlayerService) GetPlayerPosition(playerID uint32) (x, y, z, heading float32, zoneID int32, err error) {
	pos := m.playerPositions[playerID]
	return pos[0], pos[1], pos[2], pos[3], int32(pos[4]), nil
}

func (m *MockPlayerService) IsPlayerInCombat(playerID uint32) bool {
	return m.combat[playerID]
}

func (m *MockPlayerService) CanPlayerCarryItems(playerID uint32, itemCount int) bool {
	space := m.inventorySpace[playerID]
	return space >= itemCount
}

func (m *MockPlayerService) AddItemsToPlayer(playerID uint32, items []*items.Item) error {
	return nil
}

func (m *MockPlayerService) AddCoinsToPlayer(playerID uint32, coins int32) error {
	return nil
}

func (m *MockPlayerService) GetPlayerSkillValue(playerID uint32, skillName string) int32 {
	if skills, exists := m.skills[playerID]; exists {
		return skills[skillName]
	}
	return 0
}

func (m *MockPlayerService) AddPlayerExperience(playerID uint32, experience int32, skillName string) error {
	return nil
}

func (m *MockPlayerService) SendMessageToPlayer(playerID uint32, message string) error {
	return nil
}

func (m *MockPlayerService) SetPlayerPosition(playerID uint32, x, y, z, heading float32, zoneID int32) {
	m.playerPositions[playerID] = [5]float32{x, y, z, heading, float32(zoneID)}
}

func (m *MockPlayerService) SetInventorySpace(playerID uint32, space int) {
	m.inventorySpace[playerID] = space
}

// MockZoneService implements ZoneService for testing
type MockZoneService struct {
	rules   map[int32]map[string]interface{}
	objects map[int32]map[int32]interface{} // zoneID -> objectID
}

func NewMockZoneService() *MockZoneService {
	return &MockZoneService{
		rules:   make(map[int32]map[string]interface{}),
		objects: make(map[int32]map[int32]interface{}),
	}
}

func (m *MockZoneService) GetZoneRule(zoneID int32, ruleName string) (interface{}, error) {
	if rules, exists := m.rules[zoneID]; exists {
		return rules[ruleName], nil
	}
	return true, nil // Default to enabled
}

func (m *MockZoneService) SpawnObjectInZone(zoneID int32, appearanceID int32, x, y, z, heading float32, name string, commands []string) (int32, error) {
	objectID := int32(len(m.objects[zoneID]) + 1)
	if m.objects[zoneID] == nil {
		m.objects[zoneID] = make(map[int32]interface{})
	}
	m.objects[zoneID][objectID] = struct{}{}
	return objectID, nil
}

func (m *MockZoneService) RemoveObjectFromZone(zoneID int32, objectID int32) error {
	if objects, exists := m.objects[zoneID]; exists {
		delete(objects, objectID)
	}
	return nil
}

func (m *MockZoneService) GetDistanceBetweenPoints(x1, y1, z1, x2, y2, z2 float32) float32 {
	dx := x1 - x2
	dy := y1 - y2
	dz := z1 - z2
	return float32(dx*dx + dy*dy + dz*dz) // Simplified distance calculation
}

// Test database setup
func setupTestDatabase(t *testing.T) *sql.DB {
	db, err := sql.Open("sqlite", ":memory:")
	if err != nil {
		t.Fatalf("Failed to open test database: %v", err)
	}

	schema := `
	CREATE TABLE loottable (
		id INTEGER PRIMARY KEY,
		name TEXT,
		mincoin INTEGER DEFAULT 0,
		maxcoin INTEGER DEFAULT 0,
		maxlootitems INTEGER DEFAULT 6,
		lootdrop_probability REAL DEFAULT 100.0,
		coin_probability REAL DEFAULT 50.0
	);

	CREATE TABLE lootdrop (
		loot_table_id INTEGER,
		item_id INTEGER,
		item_charges INTEGER DEFAULT 1,
		equip_item INTEGER DEFAULT 0,
		probability REAL DEFAULT 100.0,
		no_drop_quest_completed_id INTEGER DEFAULT 0
	);

	CREATE TABLE spawn_loot (
		spawn_id INTEGER,
		loottable_id INTEGER
	);

	CREATE TABLE loot_global (
		type TEXT,
		loot_table INTEGER,
		value1 INTEGER,
		value2 INTEGER,
		value3 INTEGER,
		value4 INTEGER
	);
	`

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

	return db
}

// Insert test data
func insertTestLootData(t *testing.T, db *sql.DB) {
	// Test loot table
	_, err := db.Exec(`
		INSERT INTO loottable (id, name, mincoin, maxcoin, maxlootitems, lootdrop_probability, coin_probability)
		VALUES (1, 'Test Loot Table', 10, 50, 3, 100.0, 75.0)
	`)
	if err != nil {
		t.Fatalf("Failed to insert test loot table: %v", err)
	}

	// Test loot drops
	lootDrops := []struct {
		tableID     int32
		itemID      int32
		charges     int16
		probability float32
	}{
		{1, 101, 1, 100.0}, // Always drops
		{1, 102, 5, 50.0},  // 50% chance
		{1, 103, 1, 25.0},  // 25% chance
	}

	for _, drop := range lootDrops {
		_, err := db.Exec(`
			INSERT INTO lootdrop (loot_table_id, item_id, item_charges, probability)
			VALUES (?, ?, ?, ?)
		`, drop.tableID, drop.itemID, drop.charges, drop.probability)
		if err != nil {
			t.Fatalf("Failed to insert loot drop: %v", err)
		}
	}

	// Test spawn loot assignment
	_, err = db.Exec(`
		INSERT INTO spawn_loot (spawn_id, loottable_id)
		VALUES (1001, 1)
	`)
	if err != nil {
		t.Fatalf("Failed to insert spawn loot: %v", err)
	}

	// Test global loot
	_, err = db.Exec(`
		INSERT INTO loot_global (type, loot_table, value1, value2, value3, value4)
		VALUES ('level', 1, 10, 20, 1, 0)
	`)
	if err != nil {
		t.Fatalf("Failed to insert global loot: %v", err)
	}
}

// Test Functions

func TestNewLootDatabase(t *testing.T) {
	db := setupTestDatabase(t)
	defer db.Close()

	lootDB := NewLootDatabase(db)
	if lootDB == nil {
		t.Fatal("Expected non-nil LootDatabase")
	}

	if lootDB.db != db {
		t.Error("Expected database connection to be set")
	}

	if len(lootDB.queries) == 0 {
		t.Error("Expected queries to be prepared")
	}
}

func TestLoadLootData(t *testing.T) {
	db := setupTestDatabase(t)
	defer db.Close()

	insertTestLootData(t, db)

	lootDB := NewLootDatabase(db)
	err := lootDB.LoadAllLootData()
	if err != nil {
		t.Fatalf("Failed to load loot data: %v", err)
	}

	// Test loot table loaded
	table := lootDB.GetLootTable(1)
	if table == nil {
		t.Fatal("Expected to find loot table 1")
	}

	if table.Name != "Test Loot Table" {
		t.Errorf("Expected table name 'Test Loot Table', got '%s'", table.Name)
	}

	if len(table.Drops) != 3 {
		t.Errorf("Expected 3 loot drops, got %d", len(table.Drops))
	}

	// Test spawn loot assignment
	tables := lootDB.GetSpawnLootTables(1001)
	if len(tables) != 1 || tables[0] != 1 {
		t.Errorf("Expected spawn 1001 to have loot table 1, got %v", tables)
	}

	// Test global loot
	globalLoot := lootDB.GetGlobalLootTables(15, 0, 0)
	if len(globalLoot) != 1 {
		t.Errorf("Expected 1 global loot entry for level 15, got %d", len(globalLoot))
	}
}

func TestLootManager(t *testing.T) {
	db := setupTestDatabase(t)
	defer db.Close()

	insertTestLootData(t, db)

	lootDB := NewLootDatabase(db)
	err := lootDB.LoadAllLootData()
	if err != nil {
		t.Fatalf("Failed to load loot data: %v", err)
	}

	// Create mock item master list
	itemList := NewMockItemMasterList()
	itemList.AddTestItem(101, "Test Sword", LootTierCommon)
	itemList.AddTestItem(102, "Test Potion", LootTierCommon)
	itemList.AddTestItem(103, "Test Shield", LootTierTreasured)

	lootManager := NewLootManager(lootDB, itemList)

	// Test loot generation
	context := &LootContext{
		PlayerLevel:     15,
		PlayerRace:      1,
		ZoneID:          100,
		KillerID:        1,
		GroupMembers:    []uint32{1},
		CompletedQuests: make(map[int32]bool),
		LootMethod:      GroupLootMethodFreeForAll,
	}

	result, err := lootManager.GenerateLoot(1001, context)
	if err != nil {
		t.Fatalf("Failed to generate loot: %v", err)
	}

	if result == nil {
		t.Fatal("Expected non-nil loot result")
	}

	// Should have at least one item (100% drop chance for item 101)
	items := result.GetItems()
	if len(items) == 0 {
		t.Error("Expected at least one item in loot result")
	}

	// Should have coins (75% probability)
	coins := result.GetCoins()
	t.Logf("Generated %d items and %d coins", len(items), coins)
}

func TestTreasureChestCreation(t *testing.T) {
	db := setupTestDatabase(t)
	defer db.Close()

	lootDB := NewLootDatabase(db)
	itemList := NewMockItemMasterList()
	itemList.AddTestItem(101, "Test Item", LootTierLegendary) // High tier for ornate chest

	lootManager := NewLootManager(lootDB, itemList)

	// Create loot result
	item := itemList.GetItem(101)
	lootResult := &LootResult{
		Items: []*items.Item{item},
		Coins: 100,
	}

	// Create treasure chest
	chest, err := lootManager.CreateTreasureChest(1001, 100, 10.0, 20.0, 30.0, 0.0, lootResult, []uint32{1, 2})
	if err != nil {
		t.Fatalf("Failed to create treasure chest: %v", err)
	}

	if chest.AppearanceID != ChestAppearanceOrnate {
		t.Errorf("Expected ornate chest appearance %d for legendary item, got %d", 
			ChestAppearanceOrnate, chest.AppearanceID)
	}

	if len(chest.LootRights) != 2 {
		t.Errorf("Expected 2 players with loot rights, got %d", len(chest.LootRights))
	}

	if !chest.HasLootRights(1) {
		t.Error("Expected player 1 to have loot rights")
	}

	if chest.HasLootRights(3) {
		t.Error("Expected player 3 to not have loot rights")
	}
}

func TestChestService(t *testing.T) {
	db := setupTestDatabase(t)
	defer db.Close()

	lootDB := NewLootDatabase(db)
	itemList := NewMockItemMasterList()
	itemList.AddTestItem(101, "Test Item", LootTierCommon)

	lootManager := NewLootManager(lootDB, itemList)

	// Create mock services
	playerService := NewMockPlayerService()
	zoneService := NewMockZoneService()

	// Set up player near chest
	playerService.SetPlayerPosition(1, 10.0, 20.0, 30.0, 0.0, 100)
	playerService.SetInventorySpace(1, 10)

	chestService := NewChestService(lootManager, playerService, zoneService)

	// Create loot and chest
	item := itemList.GetItem(101)
	lootResult := &LootResult{
		Items: []*items.Item{item},
		Coins: 50,
	}

	chest, err := chestService.CreateTreasureChestFromLoot(1001, 100, 10.0, 20.0, 30.0, 0.0, lootResult, []uint32{1})
	if err != nil {
		t.Fatalf("Failed to create treasure chest: %v", err)
	}

	// Test viewing chest
	result := chestService.HandleChestInteraction(chest.ID, 1, ChestInteractionView, 0)
	if !result.Success {
		t.Errorf("Expected successful chest view, got: %s", result.Message)
	}

	if len(result.Items) != 1 {
		t.Errorf("Expected 1 item in view result, got %d", len(result.Items))
	}

	// Test looting item
	result = chestService.HandleChestInteraction(chest.ID, 1, ChestInteractionLoot, item.Details.UniqueID)
	if !result.Success {
		t.Errorf("Expected successful item loot, got: %s", result.Message)
	}

	if len(result.Items) != 1 {
		t.Errorf("Expected 1 looted item, got %d", len(result.Items))
	}

	// Chest should now be empty of items but still have coins
	if lootManager.IsChestEmpty(chest.ID) {
		t.Error("Expected chest to still have coins")
	}

	// Test looting all remaining (coins)
	result = chestService.HandleChestInteraction(chest.ID, 1, ChestInteractionLootAll, 0)
	if !result.Success {
		t.Errorf("Expected successful loot all, got: %s", result.Message)
	}

	if result.Coins != 50 {
		t.Errorf("Expected 50 coins looted, got %d", result.Coins)
	}

	// Chest should now be empty
	if !lootManager.IsChestEmpty(chest.ID) {
		t.Error("Expected chest to be empty after looting all")
	}
}

func TestLootStatistics(t *testing.T) {
	stats := NewLootStatistics()

	// Create test loot result
	item := &items.Item{
		Details: items.ItemDetails{
			ItemID: 101,
			Tier:   LootTierRare,
		},
	}

	lootResult := &LootResult{
		Items: []*items.Item{item},
		Coins: 100,
	}

	// Record loot
	stats.RecordLoot(1, lootResult)
	stats.RecordChest()

	current := stats.GetStatistics()

	if current.TotalLoots != 1 {
		t.Errorf("Expected 1 total loot, got %d", current.TotalLoots)
	}

	if current.TotalItems != 1 {
		t.Errorf("Expected 1 total item, got %d", current.TotalItems)
	}

	if current.TotalCoins != 100 {
		t.Errorf("Expected 100 total coins, got %d", current.TotalCoins)
	}

	if current.TreasureChests != 1 {
		t.Errorf("Expected 1 treasure chest, got %d", current.TreasureChests)
	}

	if current.ItemsByTier[LootTierRare] != 1 {
		t.Errorf("Expected 1 rare item, got %d", current.ItemsByTier[LootTierRare])
	}
}

func TestChestAppearanceSelection(t *testing.T) {
	testCases := []struct {
		tier     int8
		expected int32
	}{
		{LootTierCommon, ChestAppearanceSmall},
		{LootTierTreasured, ChestAppearanceTreasure},
		{LootTierLegendary, ChestAppearanceOrnate},
		{LootTierFabled, ChestAppearanceExquisite},
		{LootTierMythical, ChestAppearanceExquisite},
	}

	for _, tc := range testCases {
		appearance := GetChestAppearance(tc.tier)
		if appearance.AppearanceID != tc.expected {
			t.Errorf("For tier %d, expected appearance %d, got %d", 
				tc.tier, tc.expected, appearance.AppearanceID)
		}
	}
}

func TestLootValidation(t *testing.T) {
	db := setupTestDatabase(t)
	defer db.Close()

	lootDB := NewLootDatabase(db)
	itemList := NewMockItemMasterList()
	lootManager := NewLootManager(lootDB, itemList)

	playerService := NewMockPlayerService()
	zoneService := NewMockZoneService()
	chestService := NewChestService(lootManager, playerService, zoneService)

	// Create a chest with loot rights for player 1
	lootResult := &LootResult{Items: []*items.Item{}, Coins: 100}
	chest, _ := lootManager.CreateTreasureChest(1001, 100, 10.0, 20.0, 30.0, 0.0, lootResult, []uint32{1})

	// Test player without loot rights
	result := chestService.HandleChestInteraction(chest.ID, 2, ChestInteractionView, 0)
	if result.Success {
		t.Error("Expected failure for player without loot rights")
	}
	if result.Result != ChestResultNoRights {
		t.Errorf("Expected no rights result, got %d", result.Result)
	}

	// Test player in combat
	playerService.SetPlayerPosition(1, 10.0, 20.0, 30.0, 0.0, 100)
	playerService.combat[1] = true
	result = chestService.HandleChestInteraction(chest.ID, 1, ChestInteractionView, 0)
	if result.Success {
		t.Error("Expected failure for player in combat")
	}
	if result.Result != ChestResultInCombat {
		t.Errorf("Expected in combat result, got %d", result.Result)
	}

	// Test player too far away
	playerService.combat[1] = false
	playerService.SetPlayerPosition(1, 100.0, 100.0, 100.0, 0.0, 100)
	result = chestService.HandleChestInteraction(chest.ID, 1, ChestInteractionView, 0)
	if result.Success {
		t.Error("Expected failure for player too far away")
	}
	if result.Result != ChestResultTooFar {
		t.Errorf("Expected too far result, got %d", result.Result)
	}
}

func TestCleanupExpiredChests(t *testing.T) {
	db := setupTestDatabase(t)
	defer db.Close()

	lootDB := NewLootDatabase(db)
	itemList := NewMockItemMasterList()
	lootManager := NewLootManager(lootDB, itemList)

	// Create an empty chest (should be cleaned up quickly)
	emptyResult := &LootResult{Items: []*items.Item{}, Coins: 0}
	emptyChest, _ := lootManager.CreateTreasureChest(1001, 100, 10.0, 20.0, 30.0, 0.0, emptyResult, []uint32{1})

	// Modify the created time to make it expired
	emptyChest.Created = time.Now().Add(-time.Duration(ChestDespawnTime+1) * time.Second)

	// Run cleanup
	lootManager.CleanupExpiredChests()

	// Check that empty chest was removed
	if lootManager.GetTreasureChest(emptyChest.ID) != nil {
		t.Error("Expected expired empty chest to be cleaned up")
	}
}

// Benchmark tests
func BenchmarkLootGeneration(b *testing.B) {
	db := setupTestDatabase(b)
	defer db.Close()

	insertTestLootData(b, db)

	lootDB := NewLootDatabase(db)
	lootDB.LoadAllLootData()

	itemList := NewMockItemMasterList()
	itemList.AddTestItem(101, "Test Item", LootTierCommon)
	itemList.AddTestItem(102, "Test Item 2", LootTierCommon)
	itemList.AddTestItem(103, "Test Item 3", LootTierCommon)

	lootManager := NewLootManager(lootDB, itemList)

	context := &LootContext{
		PlayerLevel:     15,
		CompletedQuests: make(map[int32]bool),
	}

	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		_, err := lootManager.GenerateLoot(1001, context)
		if err != nil {
			b.Fatalf("Failed to generate loot: %v", err)
		}
	}
}

func BenchmarkChestInteraction(b *testing.B) {
	db := setupTestDatabase(b)
	defer db.Close()

	lootDB := NewLootDatabase(db)
	itemList := NewMockItemMasterList()
	itemList.AddTestItem(101, "Test Item", LootTierCommon)

	lootManager := NewLootManager(lootDB, itemList)
	playerService := NewMockPlayerService()
	zoneService := NewMockZoneService()
	chestService := NewChestService(lootManager, playerService, zoneService)

	// Set up player
	playerService.SetPlayerPosition(1, 10.0, 20.0, 30.0, 0.0, 100)
	playerService.SetInventorySpace(1, 100)

	// Create chest with loot
	item := itemList.GetItem(101)
	lootResult := &LootResult{Items: []*items.Item{item}, Coins: 100}
	chest, _ := chestService.CreateTreasureChestFromLoot(1001, 100, 10.0, 20.0, 30.0, 0.0, lootResult, []uint32{1})

	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		chestService.HandleChestInteraction(chest.ID, 1, ChestInteractionView, 0)
	}
}