From f9fdef946679a4e964fdf04b0840261aa09a93ac Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Fri, 1 Aug 2025 18:56:47 -0500 Subject: [PATCH] Implement collections tests and fix database usage --- internal/collections/collections_test.go | 1242 ++++++++++++++++++++++ internal/collections/database.go | 327 +++--- 2 files changed, 1395 insertions(+), 174 deletions(-) create mode 100644 internal/collections/collections_test.go diff --git a/internal/collections/collections_test.go b/internal/collections/collections_test.go new file mode 100644 index 0000000..4e96bb1 --- /dev/null +++ b/internal/collections/collections_test.go @@ -0,0 +1,1242 @@ +package collections + +import ( + "context" + "fmt" + "sync" + "testing" + + "eq2emu/internal/database" +) + +// Mock implementations for testing + +// MockCollectionDatabase implements CollectionDatabase interface for testing +type MockCollectionDatabase struct { + mu sync.RWMutex + collections []CollectionData + collectionItems map[int32][]CollectionItem + collectionRewards map[int32][]CollectionRewardData + playerCollections map[int32][]PlayerCollectionData + playerCollectionItems map[string][]int32 // key: "charID-collectionID" + failLoad bool + failSave bool +} + +func NewMockCollectionDatabase() *MockCollectionDatabase { + return &MockCollectionDatabase{ + collections: make([]CollectionData, 0), + collectionItems: make(map[int32][]CollectionItem), + collectionRewards: make(map[int32][]CollectionRewardData), + playerCollections: make(map[int32][]PlayerCollectionData), + playerCollectionItems: make(map[string][]int32), + } +} + +func (m *MockCollectionDatabase) LoadCollections(ctx context.Context) ([]CollectionData, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + if m.failLoad { + return nil, fmt.Errorf("mock load error") + } + + return m.collections, nil +} + +func (m *MockCollectionDatabase) LoadCollectionItems(ctx context.Context, collectionID int32) ([]CollectionItem, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + if m.failLoad { + return nil, fmt.Errorf("mock load error") + } + + items, exists := m.collectionItems[collectionID] + if !exists { + return []CollectionItem{}, nil + } + return items, nil +} + +func (m *MockCollectionDatabase) LoadCollectionRewards(ctx context.Context, collectionID int32) ([]CollectionRewardData, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + if m.failLoad { + return nil, fmt.Errorf("mock load error") + } + + rewards, exists := m.collectionRewards[collectionID] + if !exists { + return []CollectionRewardData{}, nil + } + return rewards, nil +} + +func (m *MockCollectionDatabase) LoadPlayerCollections(ctx context.Context, characterID int32) ([]PlayerCollectionData, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + if m.failLoad { + return nil, fmt.Errorf("mock load error") + } + + collections, exists := m.playerCollections[characterID] + if !exists { + return []PlayerCollectionData{}, nil + } + return collections, nil +} + +func (m *MockCollectionDatabase) LoadPlayerCollectionItems(ctx context.Context, characterID, collectionID int32) ([]int32, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + if m.failLoad { + return nil, fmt.Errorf("mock load error") + } + + key := fmt.Sprintf("%d-%d", characterID, collectionID) + items, exists := m.playerCollectionItems[key] + if !exists { + return []int32{}, nil + } + return items, nil +} + +func (m *MockCollectionDatabase) SavePlayerCollection(ctx context.Context, characterID, collectionID int32, completed bool) error { + m.mu.Lock() + defer m.mu.Unlock() + + if m.failSave { + return fmt.Errorf("mock save error") + } + + // Update or add player collection + found := false + collections := m.playerCollections[characterID] + for i := range collections { + if collections[i].CollectionID == collectionID { + collections[i].Completed = completed + found = true + break + } + } + + if !found { + m.playerCollections[characterID] = append(collections, PlayerCollectionData{ + CharacterID: characterID, + CollectionID: collectionID, + Completed: completed, + }) + } + + return nil +} + +func (m *MockCollectionDatabase) SavePlayerCollectionItem(ctx context.Context, characterID, collectionID, itemID int32) error { + m.mu.Lock() + defer m.mu.Unlock() + + if m.failSave { + return fmt.Errorf("mock save error") + } + + key := fmt.Sprintf("%d-%d", characterID, collectionID) + items := m.playerCollectionItems[key] + + // Check if already exists + for _, existingItem := range items { + if existingItem == itemID { + return nil + } + } + + m.playerCollectionItems[key] = append(items, itemID) + return nil +} + +func (m *MockCollectionDatabase) SavePlayerCollections(ctx context.Context, characterID int32, collections []*Collection) error { + if m.failSave { + return fmt.Errorf("mock save error") + } + + for _, collection := range collections { + if collection.GetSaveNeeded() { + err := m.SavePlayerCollection(ctx, characterID, collection.GetID(), collection.GetCompleted()) + if err != nil { + return err + } + + // Save items + items := collection.GetCollectionItems() + for _, item := range items { + if item.Found == ItemFound { + err := m.SavePlayerCollectionItem(ctx, characterID, collection.GetID(), item.ItemID) + if err != nil { + return err + } + } + } + } + } + + return nil +} + +// MockItemLookup implements ItemLookup interface for testing +type MockItemLookup struct { + items map[int32]ItemInfo +} + +func NewMockItemLookup() *MockItemLookup { + return &MockItemLookup{ + items: make(map[int32]ItemInfo), + } +} + +func (m *MockItemLookup) GetItem(itemID int32) (ItemInfo, error) { + if item, exists := m.items[itemID]; exists { + return item, nil + } + return ItemInfo{}, fmt.Errorf("item not found") +} + +func (m *MockItemLookup) ItemExists(itemID int32) bool { + _, exists := m.items[itemID] + return exists +} + +func (m *MockItemLookup) GetItemName(itemID int32) string { + if item, exists := m.items[itemID]; exists { + return item.Name + } + return "Unknown Item" +} + +// MockPlayerManager implements PlayerManager interface for testing +type MockPlayerManager struct { + players map[int32]PlayerInfo +} + +func NewMockPlayerManager() *MockPlayerManager { + return &MockPlayerManager{ + players: make(map[int32]PlayerInfo), + } +} + +func (m *MockPlayerManager) GetPlayerInfo(characterID int32) (PlayerInfo, error) { + if player, exists := m.players[characterID]; exists { + return player, nil + } + return PlayerInfo{}, fmt.Errorf("player not found") +} + +func (m *MockPlayerManager) IsPlayerOnline(characterID int32) bool { + if player, exists := m.players[characterID]; exists { + return player.IsOnline + } + return false +} + +func (m *MockPlayerManager) GetPlayerLevel(characterID int32) int8 { + if player, exists := m.players[characterID]; exists { + return player.Level + } + return 0 +} + +// MockClientManager implements ClientManager interface for testing +type MockClientManager struct { + mu sync.Mutex + sentUpdates []int32 + sentCompletions []int32 + sentLists []int32 + sentProgress []int32 + failSend bool +} + +func NewMockClientManager() *MockClientManager { + return &MockClientManager{ + sentUpdates: make([]int32, 0), + sentCompletions: make([]int32, 0), + sentLists: make([]int32, 0), + sentProgress: make([]int32, 0), + } +} + +func (m *MockClientManager) SendCollectionUpdate(characterID int32, collection *Collection) error { + m.mu.Lock() + defer m.mu.Unlock() + + if m.failSend { + return fmt.Errorf("mock send error") + } + + m.sentUpdates = append(m.sentUpdates, characterID) + return nil +} + +func (m *MockClientManager) SendCollectionComplete(characterID int32, collection *Collection) error { + m.mu.Lock() + defer m.mu.Unlock() + + if m.failSend { + return fmt.Errorf("mock send error") + } + + m.sentCompletions = append(m.sentCompletions, characterID) + return nil +} + +func (m *MockClientManager) SendCollectionList(characterID int32, collections []CollectionInfo) error { + m.mu.Lock() + defer m.mu.Unlock() + + if m.failSend { + return fmt.Errorf("mock send error") + } + + m.sentLists = append(m.sentLists, characterID) + return nil +} + +func (m *MockClientManager) SendCollectionProgress(characterID int32, progress []CollectionProgress) error { + m.mu.Lock() + defer m.mu.Unlock() + + if m.failSend { + return fmt.Errorf("mock send error") + } + + m.sentProgress = append(m.sentProgress, characterID) + return nil +} + +// MockRewardProvider implements RewardProvider interface for testing +type MockRewardProvider struct { + mu sync.Mutex + givenItems []struct { + CharacterID int32 + ItemID int32 + Quantity int8 + } + givenCoin []struct { + CharacterID int32 + Amount int64 + } + givenXP []struct { + CharacterID int32 + Amount int64 + } + failGive bool + failValidate bool +} + +func NewMockRewardProvider() *MockRewardProvider { + return &MockRewardProvider{} +} + +func (m *MockRewardProvider) GiveItem(characterID int32, itemID int32, quantity int8) error { + m.mu.Lock() + defer m.mu.Unlock() + + if m.failGive { + return fmt.Errorf("mock give error") + } + + m.givenItems = append(m.givenItems, struct { + CharacterID int32 + ItemID int32 + Quantity int8 + }{characterID, itemID, quantity}) + return nil +} + +func (m *MockRewardProvider) GiveCoin(characterID int32, amount int64) error { + m.mu.Lock() + defer m.mu.Unlock() + + if m.failGive { + return fmt.Errorf("mock give error") + } + + m.givenCoin = append(m.givenCoin, struct { + CharacterID int32 + Amount int64 + }{characterID, amount}) + return nil +} + +func (m *MockRewardProvider) GiveXP(characterID int32, amount int64) error { + m.mu.Lock() + defer m.mu.Unlock() + + if m.failGive { + return fmt.Errorf("mock give error") + } + + m.givenXP = append(m.givenXP, struct { + CharacterID int32 + Amount int64 + }{characterID, amount}) + return nil +} + +func (m *MockRewardProvider) ValidateRewards(characterID int32, rewards []CollectionRewardItem, coin, xp int64) error { + if m.failValidate { + return fmt.Errorf("mock validate error") + } + return nil +} + +// Tests for Collection + +func TestNewCollection(t *testing.T) { + c := NewCollection() + c.SetID(1) + c.SetName("Test Collection") + c.SetCategory("Test Category") + c.SetLevel(10) + + if c.GetID() != 1 { + t.Errorf("Expected ID 1, got %d", c.GetID()) + } + if c.GetName() != "Test Collection" { + t.Errorf("Expected name 'Test Collection', got %s", c.GetName()) + } + if c.GetCategory() != "Test Category" { + t.Errorf("Expected category 'Test Category', got %s", c.GetCategory()) + } + if c.GetLevel() != 10 { + t.Errorf("Expected level 10, got %d", c.GetLevel()) + } + if c.GetCompleted() { + t.Error("Expected collection to be incomplete") + } + if c.GetSaveNeeded() { + t.Error("Expected saveNeeded to be false") + } +} + +func TestCollectionCopy(t *testing.T) { + c1 := NewCollection() + c1.SetID(1) + c1.SetName("Original") + c1.SetCategory("Category") + c1.SetLevel(5) + c1.SetRewardCoin(1000) + c1.SetRewardXP(5000) + c1.AddCollectionItem(CollectionItem{ItemID: 100, Index: 0, Found: ItemNotFound}) + + c2 := NewCollectionFromData(c1) + + // Verify copy has same values + if c2.GetID() != c1.GetID() { + t.Error("Copy has different ID") + } + if c2.GetName() != c1.GetName() { + t.Error("Copy has different name") + } + if c2.GetRewardCoin() != c1.GetRewardCoin() { + t.Error("Copy has different reward coin") + } + + // Verify deep copy (modifying copy doesn't affect original) + c2.SetRewardCoin(2000) + if c1.GetRewardCoin() == c2.GetRewardCoin() { + t.Error("Modifying copy affected original") + } +} + +func TestCollectionItems(t *testing.T) { + c := NewCollection() + c.SetID(1) + c.SetName("Test") + c.SetCategory("Category") + c.SetLevel(1) + + // Add items + c.AddCollectionItem(CollectionItem{ItemID: 100, Index: 0, Found: ItemNotFound}) + c.AddCollectionItem(CollectionItem{ItemID: 101, Index: 1, Found: ItemNotFound}) + c.AddCollectionItem(CollectionItem{ItemID: 102, Index: 2, Found: ItemNotFound}) + + items := c.GetCollectionItems() + if len(items) != 3 { + t.Errorf("Expected 3 items, got %d", len(items)) + } + + // Test finding item + found := c.MarkItemFound(101) + if !found { + t.Error("Expected to find item 101") + } + if !c.GetSaveNeeded() { + t.Error("Expected saveNeeded to be true after finding item") + } + + // Test finding non-existent item + found = c.MarkItemFound(999) + if found { + t.Error("Should not find non-existent item") + } + + // Check if item is needed + if !c.NeedsItem(100) { + t.Error("Expected NeedsItem to return true for unfound item") + } + if c.NeedsItem(101) { + t.Error("Expected NeedsItem to return false for found item") + } + + // Check ready status + if c.GetIsReadyToTurnIn() { + t.Error("Collection should not be ready with unfound items") + } + + // Find remaining items + c.MarkItemFound(100) + c.MarkItemFound(102) + + if !c.GetIsReadyToTurnIn() { + t.Error("Collection should be ready when all items found") + } +} + +func TestCollectionRewards(t *testing.T) { + c := NewCollection() + c.SetID(1) + c.SetName("Test") + c.SetCategory("Category") + c.SetLevel(1) + + // Set rewards + c.SetRewardCoin(1000) + c.SetRewardXP(5000) + + // Add reward items + c.AddRewardItem(CollectionRewardItem{ItemID: 200, Quantity: 1}) + c.AddRewardItem(CollectionRewardItem{ItemID: 201, Quantity: 5}) + + // Add selectable rewards + c.AddSelectableRewardItem(CollectionRewardItem{ItemID: 300, Quantity: 1}) + c.AddSelectableRewardItem(CollectionRewardItem{ItemID: 301, Quantity: 1}) + + if c.GetRewardCoin() != 1000 { + t.Errorf("Expected reward coin 1000, got %d", c.GetRewardCoin()) + } + if c.GetRewardXP() != 5000 { + t.Errorf("Expected reward XP 5000, got %d", c.GetRewardXP()) + } + + rewards := c.GetRewardItems() + if len(rewards) != 2 { + t.Errorf("Expected 2 reward items, got %d", len(rewards)) + } + + selectable := c.GetSelectableRewardItems() + if len(selectable) != 2 { + t.Errorf("Expected 2 selectable rewards, got %d", len(selectable)) + } +} + +func TestCollectionCompletion(t *testing.T) { + c := NewCollection() + c.SetID(1) + c.SetName("Test") + c.SetCategory("Category") + c.SetLevel(1) + c.AddCollectionItem(CollectionItem{ItemID: 100, Index: 0, Found: ItemNotFound}) + + // Check not ready to complete without all items + if c.GetIsReadyToTurnIn() { + t.Error("Should not be ready to complete without all items") + } + + // Find item + c.MarkItemFound(100) + if !c.GetIsReadyToTurnIn() { + t.Error("Should be ready to complete when all items found") + } + + // Mark as completed + c.SetCompleted(true) + if !c.GetCompleted() { + t.Error("Expected collection to be completed") + } +} + +func TestCollectionProgress(t *testing.T) { + c := NewCollection() + c.SetID(1) + c.SetName("Test") + c.SetCategory("Category") + c.SetLevel(1) + c.AddCollectionItem(CollectionItem{ItemID: 100, Index: 0, Found: ItemNotFound}) + c.AddCollectionItem(CollectionItem{ItemID: 101, Index: 1, Found: ItemNotFound}) + c.AddCollectionItem(CollectionItem{ItemID: 102, Index: 2, Found: ItemNotFound}) + c.AddCollectionItem(CollectionItem{ItemID: 103, Index: 3, Found: ItemNotFound}) + + // Initial progress + progress := c.GetProgress() + if progress != 0.0 { + t.Errorf("Expected 0%% progress, got %.2f%%", progress) + } + + // Find one item + c.MarkItemFound(100) + progress = c.GetProgress() + if progress != 25.0 { + t.Errorf("Expected 25%% progress, got %.2f%%", progress) + } + + // Find more items + c.MarkItemFound(101) + c.MarkItemFound(102) + progress = c.GetProgress() + if progress != 75.0 { + t.Errorf("Expected 75%% progress, got %.2f%%", progress) + } + + // Complete collection + c.MarkItemFound(103) + c.SetCompleted(true) + progress = c.GetProgress() + if progress != 100.0 { + t.Errorf("Expected 100%% progress, got %.2f%%", progress) + } +} + +// Tests for MasterCollectionList + +func TestNewMasterCollectionList(t *testing.T) { + db := NewMockCollectionDatabase() + ml := NewMasterCollectionList(db) + + if ml == nil { + t.Fatal("Failed to create MasterCollectionList") + } +} + +func TestMasterCollectionListLoad(t *testing.T) { + db := NewMockCollectionDatabase() + + // Add test data + db.collections = []CollectionData{ + {ID: 1, Name: "Collection 1", Category: "Category A", Level: 10}, + {ID: 2, Name: "Collection 2", Category: "Category B", Level: 20}, + } + + db.collectionItems[1] = []CollectionItem{ + {ItemID: 100, Index: 0, Found: ItemNotFound}, + {ItemID: 101, Index: 1, Found: ItemNotFound}, + } + + db.collectionItems[2] = []CollectionItem{ + {ItemID: 200, Index: 0, Found: ItemNotFound}, + } + + db.collectionRewards[1] = []CollectionRewardData{ + {CollectionID: 1, RewardType: RewardTypeCoin, RewardValue: "1000", Quantity: 1}, + {CollectionID: 1, RewardType: RewardTypeXP, RewardValue: "5000", Quantity: 1}, + } + + // Set up item lookup + itemLookup := NewMockItemLookup() + itemLookup.items[100] = ItemInfo{ID: 100, Name: "Test Item 1"} + itemLookup.items[101] = ItemInfo{ID: 101, Name: "Test Item 2"} + itemLookup.items[200] = ItemInfo{ID: 200, Name: "Test Item 3"} + + ml := NewMasterCollectionList(db) + err := ml.Initialize(context.Background(), itemLookup) + if err != nil { + t.Fatalf("Failed to load collections: %v", err) + } + + // Verify collections loaded + collection := ml.GetCollection(1) + if collection == nil { + t.Fatal("Collection 1 not found") + } + if collection.GetName() != "Collection 1" { + t.Errorf("Expected name 'Collection 1', got %s", collection.GetName()) + } + + // Verify items loaded + items := collection.GetCollectionItems() + if len(items) != 2 { + t.Errorf("Expected 2 items, got %d", len(items)) + } + + // Verify rewards loaded + if collection.GetRewardCoin() != 1000 { + t.Errorf("Expected reward coin 1000, got %d", collection.GetRewardCoin()) + } +} + +func TestMasterCollectionListSearch(t *testing.T) { + db := NewMockCollectionDatabase() + + db.collections = []CollectionData{ + {ID: 1, Name: "Ancient Artifacts", Category: "Archaeology", Level: 10}, + {ID: 2, Name: "Ancient Weapons", Category: "Combat", Level: 20}, + {ID: 3, Name: "Modern Art", Category: "Art", Level: 15}, + } + + // Add required items for each collection + db.collectionItems[1] = []CollectionItem{ + {ItemID: 100, Index: 0, Found: ItemNotFound}, + } + db.collectionItems[2] = []CollectionItem{ + {ItemID: 200, Index: 0, Found: ItemNotFound}, + } + db.collectionItems[3] = []CollectionItem{ + {ItemID: 300, Index: 0, Found: ItemNotFound}, + } + + // Set up item lookup + itemLookup := NewMockItemLookup() + itemLookup.items[100] = ItemInfo{ID: 100, Name: "Test Item 1"} + itemLookup.items[200] = ItemInfo{ID: 200, Name: "Test Item 2"} + itemLookup.items[300] = ItemInfo{ID: 300, Name: "Test Item 3"} + + ml := NewMasterCollectionList(db) + ml.Initialize(context.Background(), itemLookup) + + // Search by name + results := ml.FindCollectionsByName("Ancient") + if len(results) != 2 { + t.Errorf("Expected 2 results for 'Ancient', got %d", len(results)) + } + + // Search by category + results = ml.GetCollectionsByCategory("Art") + if len(results) != 1 { + t.Errorf("Expected 1 result for category 'Art', got %d", len(results)) + } + + // Search by level range + results = ml.GetCollectionsByLevel(15, 25) + if len(results) != 2 { + t.Errorf("Expected 2 results for level range 15-25, got %d", len(results)) + } +} + +// Tests for PlayerCollectionList + +func TestNewPlayerCollectionList(t *testing.T) { + db := NewMockCollectionDatabase() + pl := NewPlayerCollectionList(1001, db) + + if pl == nil { + t.Fatal("Failed to create PlayerCollectionList") + } + if pl.GetCharacterID() != 1001 { + t.Errorf("Expected character ID 1001, got %d", pl.GetCharacterID()) + } +} + +func TestPlayerCollectionListLoad(t *testing.T) { + db := NewMockCollectionDatabase() + masterList := NewMasterCollectionList(db) + + // Set up master collections + db.collections = []CollectionData{ + {ID: 1, Name: "Collection 1", Category: "Category A", Level: 10}, + {ID: 2, Name: "Collection 2", Category: "Category B", Level: 20}, + } + db.collectionItems[1] = []CollectionItem{ + {ItemID: 100, Index: 0, Found: ItemNotFound}, + {ItemID: 101, Index: 1, Found: ItemNotFound}, + } + db.collectionItems[2] = []CollectionItem{ + {ItemID: 200, Index: 0, Found: ItemNotFound}, + } + + // Set up item lookup + itemLookup := NewMockItemLookup() + itemLookup.items[100] = ItemInfo{ID: 100, Name: "Test Item 1"} + itemLookup.items[101] = ItemInfo{ID: 101, Name: "Test Item 2"} + itemLookup.items[200] = ItemInfo{ID: 200, Name: "Test Item 3"} + + masterList.Initialize(context.Background(), itemLookup) + + // Set up player data + db.playerCollections[1001] = []PlayerCollectionData{ + {CharacterID: 1001, CollectionID: 1, Completed: false}, + {CharacterID: 1001, CollectionID: 2, Completed: true}, + } + db.playerCollectionItems["1001-1"] = []int32{100} // Found item 100 in collection 1 + + pl := NewPlayerCollectionList(1001, db) + err := pl.Initialize(context.Background(), masterList) + if err != nil { + t.Fatalf("Failed to load player collections: %v", err) + } + + // Check loaded collections + collection := pl.GetCollection(1) + if collection == nil { + t.Fatal("Collection 1 not found") + } + if collection.GetCompleted() { + t.Error("Collection 1 should not be completed") + } + + // Check found items + if collection.NeedsItem(100) { + t.Error("Item 100 should be found") + } + if !collection.NeedsItem(101) { + t.Error("Item 101 should not be found") + } + + // Check completed collection + collection2 := pl.GetCollection(2) + if collection2 == nil { + t.Fatal("Collection 2 not found") + } + if !collection2.GetCompleted() { + t.Error("Collection 2 should be completed") + } +} + +func TestPlayerCollectionListAddItem(t *testing.T) { + db := NewMockCollectionDatabase() + masterList := NewMasterCollectionList(db) + + // Set up collections + db.collections = []CollectionData{ + {ID: 1, Name: "Collection 1", Category: "Category A", Level: 10}, + } + db.collectionItems[1] = []CollectionItem{ + {ItemID: 100, Index: 0, Found: ItemNotFound}, + {ItemID: 101, Index: 1, Found: ItemNotFound}, + } + + // Set up item lookup + itemLookup := NewMockItemLookup() + itemLookup.items[100] = ItemInfo{ID: 100, Name: "Test Item 1"} + itemLookup.items[101] = ItemInfo{ID: 101, Name: "Test Item 2"} + + masterList.Initialize(context.Background(), itemLookup) + + pl := NewPlayerCollectionList(1001, db) + + // Try to add collection item directly - this will depend on the actual API + // For now, just test basic functionality + if pl.GetCharacterID() != 1001 { + t.Errorf("Expected character ID 1001, got %d", pl.GetCharacterID()) + } + + // Load collections first + err := pl.Initialize(context.Background(), masterList) + if err != nil { + t.Fatalf("Failed to load player collections: %v", err) + } +} + +// Tests for CollectionManager + +func TestNewCollectionManager(t *testing.T) { + db := NewMockCollectionDatabase() + itemLookup := NewMockItemLookup() + + cm := NewCollectionManager(db, itemLookup) + if cm == nil { + t.Fatal("Failed to create CollectionManager") + } +} + +func TestCollectionManagerInitialize(t *testing.T) { + db := NewMockCollectionDatabase() + itemLookup := NewMockItemLookup() + + // Add test data + db.collections = []CollectionData{ + {ID: 1, Name: "Collection 1", Category: "Category A", Level: 10}, + } + db.collectionItems[1] = []CollectionItem{ + {ItemID: 100, Index: 0, Found: ItemNotFound}, + } + + // Set up item lookup + itemLookup.items[100] = ItemInfo{ID: 100, Name: "Test Item 1"} + + cm := NewCollectionManager(db, itemLookup) + err := cm.Initialize(context.Background()) + if err != nil { + t.Fatalf("Failed to initialize: %v", err) + } + + // Verify master list loaded + stats := cm.GetSystemStatistics() + if stats.TotalCollections != 1 { + t.Errorf("Expected 1 collection, got %d", stats.TotalCollections) + } +} + +func TestCollectionManagerPlayerOperations(t *testing.T) { + db := NewMockCollectionDatabase() + itemLookup := NewMockItemLookup() + + // Add test data + db.collections = []CollectionData{ + {ID: 1, Name: "Collection 1", Category: "Category A", Level: 10}, + } + db.collectionItems[1] = []CollectionItem{ + {ItemID: 100, Index: 0, Found: ItemNotFound}, + {ItemID: 101, Index: 1, Found: ItemNotFound}, + } + + // Set up item lookup + itemLookup.items[100] = ItemInfo{ID: 100, Name: "Test Item 1"} + itemLookup.items[101] = ItemInfo{ID: 101, Name: "Test Item 2"} + + cm := NewCollectionManager(db, itemLookup) + err := cm.Initialize(context.Background()) + if err != nil { + t.Fatalf("Failed to initialize: %v", err) + } + + // Verify statistics + stats := cm.GetSystemStatistics() + if stats.TotalCollections != 1 { + t.Errorf("Expected 1 collection, got %d", stats.TotalCollections) + } + + // Get collection copy + collection := cm.GetCollectionCopy(1) + if collection == nil { + t.Fatal("Failed to get collection copy") + } + if collection.GetName() != "Collection 1" { + t.Errorf("Expected name 'Collection 1', got %s", collection.GetName()) + } +} + +// Tests for DatabaseCollectionManager + +func TestDatabaseCollectionManager(t *testing.T) { + // Create in-memory database + db, err := database.Open(":memory:") + if err != nil { + t.Fatalf("Failed to create database: %v", err) + } + defer db.Close() + + dcm := NewDatabaseCollectionManager(db) + + // Ensure tables exist + err = dcm.EnsureCollectionTables(context.Background()) + if err != nil { + t.Fatalf("Failed to create tables: %v", err) + } + + // Test saving and loading collections + ctx := context.Background() + + // Insert test collection + err = db.Exec(`INSERT INTO collections (id, collection_name, collection_category, level) + VALUES (?, ?, ?, ?)`, 1, "Test Collection", "Test Category", 10) + if err != nil { + t.Fatalf("Failed to insert collection: %v", err) + } + + // Load collections + collections, err := dcm.LoadCollections(ctx) + if err != nil { + t.Fatalf("Failed to load collections: %v", err) + } + if len(collections) != 1 { + t.Errorf("Expected 1 collection, got %d", len(collections)) + } + if collections[0].Name != "Test Collection" { + t.Errorf("Expected name 'Test Collection', got %s", collections[0].Name) + } + + // Test player collection operations + err = dcm.SavePlayerCollection(ctx, 1001, 1, false) + if err != nil { + t.Fatalf("Failed to save player collection: %v", err) + } + + playerCollections, err := dcm.LoadPlayerCollections(ctx, 1001) + if err != nil { + t.Fatalf("Failed to load player collections: %v", err) + } + if len(playerCollections) != 1 { + t.Errorf("Expected 1 player collection, got %d", len(playerCollections)) + } + + // Test statistics + stats, err := dcm.GetCollectionStatistics(ctx) + if err != nil { + t.Fatalf("Failed to get statistics: %v", err) + } + if stats.TotalCollections != 1 { + t.Errorf("Expected 1 total collection, got %d", stats.TotalCollections) + } +} + +// Tests for EntityCollectionAdapter + +// Mock entity for testing +type mockEntity struct { + id int32 +} + +func (m *mockEntity) GetID() int32 { + return m.id +} + +func TestEntityCollectionAdapter(t *testing.T) { + entity := &mockEntity{id: 1001} + + // Mock player manager + pm := NewMockPlayerManager() + pm.players[1001] = PlayerInfo{ + CharacterID: 1001, + Level: 50, + } + + adapter := &EntityCollectionAdapter{ + entity: entity, + playerManager: pm, + } + + if adapter.GetCharacterID() != 1001 { + t.Errorf("Expected character ID 1001, got %d", adapter.GetCharacterID()) + } + + if adapter.GetLevel() != 50 { + t.Errorf("Expected level 50, got %d", adapter.GetLevel()) + } + + // Test HasItem (should return false as placeholder) + if adapter.HasItem(100) { + t.Error("HasItem should return false") + } +} + +// Concurrency tests + +func TestCollectionConcurrency(t *testing.T) { + c := NewCollection() + c.SetID(1) + c.SetName("Test") + c.SetCategory("Category") + c.SetLevel(1) + + // Add items + for i := 0; i < 100; i++ { + c.AddCollectionItem(CollectionItem{ItemID: int32(i), Index: int8(i), Found: ItemNotFound}) + } + + // Concurrent reads and writes + var wg sync.WaitGroup + errors := make(chan error, 100) + + // Writers + for i := 0; i < 50; i++ { + wg.Add(1) + go func(itemID int32) { + defer wg.Done() + c.MarkItemFound(itemID) + }(int32(i)) + } + + // Readers + for i := 0; i < 50; i++ { + wg.Add(1) + go func() { + defer wg.Done() + _ = c.GetProgress() + _ = c.GetCollectionItems() + _ = c.GetIsReadyToTurnIn() + }() + } + + wg.Wait() + close(errors) + + // Check for errors + for err := range errors { + if err != nil { + t.Errorf("Concurrent operation error: %v", err) + } + } +} + +func TestMasterCollectionListConcurrency(t *testing.T) { + db := NewMockCollectionDatabase() + + // Add test data + for i := 1; i <= 100; i++ { + db.collections = append(db.collections, CollectionData{ + ID: int32(i), + Name: fmt.Sprintf("Collection %d", i), + Category: fmt.Sprintf("Category %d", i%10), + Level: int8(i % 50), + }) + + // Add at least one item per collection + db.collectionItems[int32(i)] = []CollectionItem{ + {ItemID: int32(i * 100), Index: 0, Found: ItemNotFound}, + } + } + + // Set up item lookup for all items + itemLookup := NewMockItemLookup() + for i := 1; i <= 100; i++ { + itemLookup.items[int32(i*100)] = ItemInfo{ID: int32(i * 100), Name: fmt.Sprintf("Item %d", i)} + } + + ml := NewMasterCollectionList(db) + ml.Initialize(context.Background(), itemLookup) + + var wg sync.WaitGroup + + // Concurrent reads + for i := 0; i < 100; i++ { + wg.Add(1) + go func(id int32) { + defer wg.Done() + _ = ml.GetCollection(id) + _ = ml.FindCollectionsByName("Collection") + _ = ml.GetCollectionsByCategory("Category 1") + _ = ml.GetAllCollections() + }(int32(i + 1)) + } + + wg.Wait() +} + +// Benchmarks + +func BenchmarkCollectionMarkItemFound(b *testing.B) { + c := NewCollection() + c.SetID(1) + c.SetName("Test") + c.SetCategory("Category") + c.SetLevel(1) + + // Add items + for i := 0; i < 100; i++ { + c.AddCollectionItem(CollectionItem{ItemID: int32(i), Index: int8(i), Found: ItemNotFound}) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + c.MarkItemFound(int32(i % 100)) + } +} + +func BenchmarkMasterCollectionListSearch(b *testing.B) { + db := NewMockCollectionDatabase() + + // Add test data + for i := 1; i <= 1000; i++ { + db.collections = append(db.collections, CollectionData{ + ID: int32(i), + Name: fmt.Sprintf("Collection %d", i), + Category: fmt.Sprintf("Category %d", i%10), + Level: int8(i % 50), + }) + + // Add at least one item per collection + db.collectionItems[int32(i)] = []CollectionItem{ + {ItemID: int32(i * 100), Index: 0, Found: ItemNotFound}, + } + } + + // Set up item lookup for all items + itemLookup := NewMockItemLookup() + for i := 1; i <= 1000; i++ { + itemLookup.items[int32(i*100)] = ItemInfo{ID: int32(i * 100), Name: fmt.Sprintf("Item %d", i)} + } + + ml := NewMasterCollectionList(db) + ml.Initialize(context.Background(), itemLookup) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = ml.FindCollectionsByName("Collection") + } +} + +func BenchmarkPlayerCollectionListLoad(b *testing.B) { + db := NewMockCollectionDatabase() + masterList := NewMasterCollectionList(db) + + // Set up collections + for i := 1; i <= 100; i++ { + db.collections = append(db.collections, CollectionData{ + ID: int32(i), + Name: fmt.Sprintf("Collection %d", i), + Category: "Category", + Level: 10, + }) + + items := make([]CollectionItem, 10) + for j := 0; j < 10; j++ { + items[j] = CollectionItem{ + ItemID: int32(i*100 + j), + Index: int8(j), + Found: ItemNotFound, + } + } + db.collectionItems[int32(i)] = items + } + + // Set up item lookup for all items + itemLookup := NewMockItemLookup() + for i := 1; i <= 100; i++ { + for j := 0; j < 10; j++ { + itemID := int32(i*100 + j) + itemLookup.items[itemID] = ItemInfo{ID: itemID, Name: fmt.Sprintf("Item %d", itemID)} + } + } + + masterList.Initialize(context.Background(), itemLookup) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + pl := NewPlayerCollectionList(1001, db) + pl.Initialize(context.Background(), masterList) + } +} + +// Test constants +func TestConstants(t *testing.T) { + // Test reward types + if RewardTypeItem != "Item" { + t.Errorf("Expected RewardTypeItem to be 'Item', got %s", RewardTypeItem) + } + if RewardTypeSelectable != "Selectable" { + t.Errorf("Expected RewardTypeSelectable to be 'Selectable', got %s", RewardTypeSelectable) + } + if RewardTypeCoin != "Coin" { + t.Errorf("Expected RewardTypeCoin to be 'Coin', got %s", RewardTypeCoin) + } + if RewardTypeXP != "XP" { + t.Errorf("Expected RewardTypeXP to be 'XP', got %s", RewardTypeXP) + } + + // Test item states + if ItemNotFound != 0 { + t.Errorf("Expected ItemNotFound to be 0, got %d", ItemNotFound) + } + if ItemFound != 1 { + t.Errorf("Expected ItemFound to be 1, got %d", ItemFound) + } + + // Test collection states + if CollectionIncomplete != false { + t.Error("Expected CollectionIncomplete to be false") + } + if CollectionCompleted != true { + t.Error("Expected CollectionCompleted to be true") + } +} \ No newline at end of file diff --git a/internal/collections/database.go b/internal/collections/database.go index b16f44a..af7e257 100644 --- a/internal/collections/database.go +++ b/internal/collections/database.go @@ -23,31 +23,20 @@ func NewDatabaseCollectionManager(db *database.DB) *DatabaseCollectionManager { func (dcm *DatabaseCollectionManager) LoadCollections(ctx context.Context) ([]CollectionData, error) { query := "SELECT `id`, `collection_name`, `collection_category`, `level` FROM `collections`" - rows, err := dcm.db.QueryContext(ctx, query) + var collections []CollectionData + err := dcm.db.Query(query, func(row *database.Row) error { + var collection CollectionData + collection.ID = int32(row.Int(0)) + collection.Name = row.Text(1) + collection.Category = row.Text(2) + collection.Level = int8(row.Int(3)) + collections = append(collections, collection) + return nil + }) + if err != nil { return nil, fmt.Errorf("failed to query collections: %w", err) } - defer rows.Close() - - var collections []CollectionData - for rows.Next() { - var collection CollectionData - err := rows.Scan( - &collection.ID, - &collection.Name, - &collection.Category, - &collection.Level, - ) - if err != nil { - return nil, fmt.Errorf("failed to scan collection row: %w", err) - } - - collections = append(collections, collection) - } - - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("error iterating collection rows: %w", err) - } return collections, nil } @@ -59,30 +48,19 @@ func (dcm *DatabaseCollectionManager) LoadCollectionItems(ctx context.Context, c WHERE collection_id = ? ORDER BY item_index ASC` - rows, err := dcm.db.QueryContext(ctx, query, collectionID) - if err != nil { - return nil, fmt.Errorf("failed to query collection items for collection %d: %w", collectionID, err) - } - defer rows.Close() - var items []CollectionItem - for rows.Next() { + err := dcm.db.Query(query, func(row *database.Row) error { var item CollectionItem - err := rows.Scan( - &item.ItemID, - &item.Index, - ) - if err != nil { - return nil, fmt.Errorf("failed to scan collection item row: %w", err) - } - + item.ItemID = int32(row.Int(0)) + item.Index = int8(row.Int(1)) // Items start as not found item.Found = ItemNotFound items = append(items, item) - } + return nil + }, collectionID) - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("error iterating collection item rows: %w", err) + if err != nil { + return nil, fmt.Errorf("failed to query collection items for collection %d: %w", collectionID, err) } return items, nil @@ -94,31 +72,20 @@ func (dcm *DatabaseCollectionManager) LoadCollectionRewards(ctx context.Context, FROM collection_rewards WHERE collection_id = ?` - rows, err := dcm.db.QueryContext(ctx, query, collectionID) + var rewards []CollectionRewardData + err := dcm.db.Query(query, func(row *database.Row) error { + var reward CollectionRewardData + reward.CollectionID = int32(row.Int(0)) + reward.RewardType = row.Text(1) + reward.RewardValue = row.Text(2) + reward.Quantity = int8(row.Int(3)) + rewards = append(rewards, reward) + return nil + }, collectionID) + if err != nil { return nil, fmt.Errorf("failed to query collection rewards for collection %d: %w", collectionID, err) } - defer rows.Close() - - var rewards []CollectionRewardData - for rows.Next() { - var reward CollectionRewardData - err := rows.Scan( - &reward.CollectionID, - &reward.RewardType, - &reward.RewardValue, - &reward.Quantity, - ) - if err != nil { - return nil, fmt.Errorf("failed to scan collection reward row: %w", err) - } - - rewards = append(rewards, reward) - } - - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("error iterating collection reward rows: %w", err) - } return rewards, nil } @@ -129,32 +96,19 @@ func (dcm *DatabaseCollectionManager) LoadPlayerCollections(ctx context.Context, FROM character_collections WHERE char_id = ?` - rows, err := dcm.db.QueryContext(ctx, query, characterID) + var collections []PlayerCollectionData + err := dcm.db.Query(query, func(row *database.Row) error { + var collection PlayerCollectionData + collection.CharacterID = int32(row.Int(0)) + collection.CollectionID = int32(row.Int(1)) + collection.Completed = row.Bool(2) + collections = append(collections, collection) + return nil + }, characterID) + if err != nil { return nil, fmt.Errorf("failed to query player collections for character %d: %w", characterID, err) } - defer rows.Close() - - var collections []PlayerCollectionData - for rows.Next() { - var collection PlayerCollectionData - var completed int - err := rows.Scan( - &collection.CharacterID, - &collection.CollectionID, - &completed, - ) - if err != nil { - return nil, fmt.Errorf("failed to scan player collection row: %w", err) - } - - collection.Completed = completed == 1 - collections = append(collections, collection) - } - - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("error iterating player collection rows: %w", err) - } return collections, nil } @@ -165,26 +119,16 @@ func (dcm *DatabaseCollectionManager) LoadPlayerCollectionItems(ctx context.Cont FROM character_collection_items WHERE char_id = ? AND collection_id = ?` - rows, err := dcm.db.QueryContext(ctx, query, characterID, collectionID) + var itemIDs []int32 + err := dcm.db.Query(query, func(row *database.Row) error { + itemID := int32(row.Int(0)) + itemIDs = append(itemIDs, itemID) + return nil + }, characterID, collectionID) + if err != nil { return nil, fmt.Errorf("failed to query player collection items for character %d, collection %d: %w", characterID, collectionID, err) } - defer rows.Close() - - var itemIDs []int32 - for rows.Next() { - var itemID int32 - err := rows.Scan(&itemID) - if err != nil { - return nil, fmt.Errorf("failed to scan player collection item row: %w", err) - } - - itemIDs = append(itemIDs, itemID) - } - - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("error iterating player collection item rows: %w", err) - } return itemIDs, nil } @@ -201,7 +145,7 @@ func (dcm *DatabaseCollectionManager) SavePlayerCollection(ctx context.Context, ON CONFLICT(char_id, collection_id) DO UPDATE SET completed = ?` - _, err := dcm.db.ExecContext(ctx, query, characterID, collectionID, completedInt, completedInt) + err := dcm.db.Exec(query, characterID, collectionID, completedInt, completedInt) if err != nil { return fmt.Errorf("failed to save player collection for character %d, collection %d: %w", characterID, collectionID, err) } @@ -214,7 +158,7 @@ func (dcm *DatabaseCollectionManager) SavePlayerCollectionItem(ctx context.Conte query := `INSERT OR IGNORE INTO character_collection_items (char_id, collection_id, collection_item_id) VALUES (?, ?, ?)` - _, err := dcm.db.ExecContext(ctx, query, characterID, collectionID, itemID) + err := dcm.db.Exec(query, characterID, collectionID, itemID) if err != nil { return fmt.Errorf("failed to save player collection item for character %d, collection %d, item %d: %w", characterID, collectionID, itemID, err) } @@ -229,37 +173,34 @@ func (dcm *DatabaseCollectionManager) SavePlayerCollections(ctx context.Context, } // Use a transaction for atomic updates - tx, err := dcm.db.BeginTx(ctx, nil) + err := dcm.db.Transaction(func(db *database.DB) error { + for _, collection := range collections { + if !collection.GetSaveNeeded() { + continue + } + + // Save collection completion status + if err := dcm.savePlayerCollectionInTx(db, characterID, collection); err != nil { + return fmt.Errorf("failed to save collection %d: %w", collection.GetID(), err) + } + + // Save found items + if err := dcm.savePlayerCollectionItemsInTx(db, characterID, collection); err != nil { + return fmt.Errorf("failed to save collection items for collection %d: %w", collection.GetID(), err) + } + } + return nil + }) + if err != nil { - return fmt.Errorf("failed to begin transaction: %w", err) - } - defer tx.Rollback() - - for _, collection := range collections { - if !collection.GetSaveNeeded() { - continue - } - - // Save collection completion status - if err := dcm.savePlayerCollectionTx(ctx, tx, characterID, collection); err != nil { - return fmt.Errorf("failed to save collection %d: %w", collection.GetID(), err) - } - - // Save found items - if err := dcm.savePlayerCollectionItemsTx(ctx, tx, characterID, collection); err != nil { - return fmt.Errorf("failed to save collection items for collection %d: %w", collection.GetID(), err) - } - } - - if err := tx.Commit(); err != nil { - return fmt.Errorf("failed to commit transaction: %w", err) + return fmt.Errorf("transaction failed: %w", err) } return nil } -// savePlayerCollectionTx saves a single collection within a transaction -func (dcm *DatabaseCollectionManager) savePlayerCollectionTx(ctx context.Context, tx database.Tx, characterID int32, collection *Collection) error { +// savePlayerCollectionInTx saves a single collection within a transaction +func (dcm *DatabaseCollectionManager) savePlayerCollectionInTx(db *database.DB, characterID int32, collection *Collection) error { completedInt := 0 if collection.GetCompleted() { completedInt = 1 @@ -270,12 +211,12 @@ func (dcm *DatabaseCollectionManager) savePlayerCollectionTx(ctx context.Context ON CONFLICT(char_id, collection_id) DO UPDATE SET completed = ?` - _, err := tx.ExecContext(ctx, query, characterID, collection.GetID(), completedInt, completedInt) + err := db.Exec(query, characterID, collection.GetID(), completedInt, completedInt) return err } -// savePlayerCollectionItemsTx saves collection items within a transaction -func (dcm *DatabaseCollectionManager) savePlayerCollectionItemsTx(ctx context.Context, tx database.Tx, characterID int32, collection *Collection) error { +// savePlayerCollectionItemsInTx saves collection items within a transaction +func (dcm *DatabaseCollectionManager) savePlayerCollectionItemsInTx(db *database.DB, characterID int32, collection *Collection) error { items := collection.GetCollectionItems() for _, item := range items { @@ -283,7 +224,7 @@ func (dcm *DatabaseCollectionManager) savePlayerCollectionItemsTx(ctx context.Co query := `INSERT OR IGNORE INTO character_collection_items (char_id, collection_id, collection_item_id) VALUES (?, ?, ?)` - _, err := tx.ExecContext(ctx, query, characterID, collection.GetID(), item.ItemID) + err := db.Exec(query, characterID, collection.GetID(), item.ItemID) if err != nil { return fmt.Errorf("failed to save item %d: %w", item.ItemID, err) } @@ -339,7 +280,7 @@ func (dcm *DatabaseCollectionManager) EnsureCollectionTables(ctx context.Context } for i, query := range queries { - _, err := dcm.db.ExecContext(ctx, query) + err := dcm.db.Exec(query) if err != nil { return fmt.Errorf("failed to create collection table %d: %w", i+1, err) } @@ -357,7 +298,7 @@ func (dcm *DatabaseCollectionManager) EnsureCollectionTables(ctx context.Context } for i, query := range indexes { - _, err := dcm.db.ExecContext(ctx, query) + err := dcm.db.Exec(query) if err != nil { return fmt.Errorf("failed to create collection index %d: %w", i+1, err) } @@ -370,68 +311,78 @@ func (dcm *DatabaseCollectionManager) EnsureCollectionTables(ctx context.Context func (dcm *DatabaseCollectionManager) GetCollectionCount(ctx context.Context) (int, error) { query := "SELECT COUNT(*) FROM collections" - var count int - err := dcm.db.QueryRowContext(ctx, query).Scan(&count) + row, err := dcm.db.QueryRow(query) if err != nil { return 0, fmt.Errorf("failed to get collection count: %w", err) } + defer row.Close() - return count, nil + if row == nil { + return 0, nil + } + + return row.Int(0), nil } // GetPlayerCollectionCount returns the number of collections a player has func (dcm *DatabaseCollectionManager) GetPlayerCollectionCount(ctx context.Context, characterID int32) (int, error) { query := "SELECT COUNT(*) FROM character_collections WHERE char_id = ?" - var count int - err := dcm.db.QueryRowContext(ctx, query, characterID).Scan(&count) + row, err := dcm.db.QueryRow(query, characterID) if err != nil { return 0, fmt.Errorf("failed to get player collection count for character %d: %w", characterID, err) } + defer row.Close() - return count, nil + if row == nil { + return 0, nil + } + + return row.Int(0), nil } // GetCompletedCollectionCount returns the number of completed collections for a player func (dcm *DatabaseCollectionManager) GetCompletedCollectionCount(ctx context.Context, characterID int32) (int, error) { query := "SELECT COUNT(*) FROM character_collections WHERE char_id = ? AND completed = 1" - var count int - err := dcm.db.QueryRowContext(ctx, query, characterID).Scan(&count) + row, err := dcm.db.QueryRow(query, characterID) if err != nil { return 0, fmt.Errorf("failed to get completed collection count for character %d: %w", characterID, err) } + defer row.Close() - return count, nil + if row == nil { + return 0, nil + } + + return row.Int(0), nil } // DeletePlayerCollection removes a player's collection progress func (dcm *DatabaseCollectionManager) DeletePlayerCollection(ctx context.Context, characterID, collectionID int32) error { // Use a transaction to ensure both tables are updated atomically - tx, err := dcm.db.BeginTx(ctx, nil) - if err != nil { - return fmt.Errorf("failed to begin transaction: %w", err) - } - defer tx.Rollback() + err := dcm.db.Transaction(func(db *database.DB) error { + // Delete collection items first due to foreign key constraint + err := db.Exec( + "DELETE FROM character_collection_items WHERE char_id = ? AND collection_id = ?", + characterID, collectionID) + if err != nil { + return fmt.Errorf("failed to delete player collection items: %w", err) + } - // Delete collection items first due to foreign key constraint - _, err = tx.ExecContext(ctx, - "DELETE FROM character_collection_items WHERE char_id = ? AND collection_id = ?", - characterID, collectionID) - if err != nil { - return fmt.Errorf("failed to delete player collection items: %w", err) - } + // Delete collection + err = db.Exec( + "DELETE FROM character_collections WHERE char_id = ? AND collection_id = ?", + characterID, collectionID) + if err != nil { + return fmt.Errorf("failed to delete player collection: %w", err) + } - // Delete collection - _, err = tx.ExecContext(ctx, - "DELETE FROM character_collections WHERE char_id = ? AND collection_id = ?", - characterID, collectionID) - if err != nil { - return fmt.Errorf("failed to delete player collection: %w", err) - } + return nil + }) - if err := tx.Commit(); err != nil { - return fmt.Errorf("failed to commit transaction: %w", err) + if err != nil { + return fmt.Errorf("transaction failed: %w", err) } return nil @@ -442,50 +393,78 @@ func (dcm *DatabaseCollectionManager) GetCollectionStatistics(ctx context.Contex var stats CollectionStatistics // Total collections - err := dcm.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM collections").Scan(&stats.TotalCollections) + row, err := dcm.db.QueryRow("SELECT COUNT(*) FROM collections") if err != nil { return stats, fmt.Errorf("failed to get total collections: %w", err) } + if row != nil { + stats.TotalCollections = row.Int(0) + row.Close() + } // Total collection items - err = dcm.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM collection_details").Scan(&stats.TotalItems) + row, err = dcm.db.QueryRow("SELECT COUNT(*) FROM collection_details") if err != nil { return stats, fmt.Errorf("failed to get total items: %w", err) } + if row != nil { + stats.TotalItems = row.Int(0) + row.Close() + } // Players with collections - err = dcm.db.QueryRowContext(ctx, "SELECT COUNT(DISTINCT char_id) FROM character_collections").Scan(&stats.PlayersWithCollections) + row, err = dcm.db.QueryRow("SELECT COUNT(DISTINCT char_id) FROM character_collections") if err != nil { return stats, fmt.Errorf("failed to get players with collections: %w", err) } + if row != nil { + stats.PlayersWithCollections = row.Int(0) + row.Close() + } // Completed collections across all players - err = dcm.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM character_collections WHERE completed = 1").Scan(&stats.CompletedCollections) + row, err = dcm.db.QueryRow("SELECT COUNT(*) FROM character_collections WHERE completed = 1") if err != nil { return stats, fmt.Errorf("failed to get completed collections: %w", err) } + if row != nil { + stats.CompletedCollections = row.Int(0) + row.Close() + } // Active collections (incomplete with at least one item found) across all players - query := `SELECT COUNT(DISTINCT cc.char_id, cc.collection_id) + query := `SELECT COUNT(DISTINCT cc.char_id || '-' || cc.collection_id) FROM character_collections cc JOIN character_collection_items cci ON cc.char_id = cci.char_id AND cc.collection_id = cci.collection_id WHERE cc.completed = 0` - err = dcm.db.QueryRowContext(ctx, query).Scan(&stats.ActiveCollections) + row, err = dcm.db.QueryRow(query) if err != nil { return stats, fmt.Errorf("failed to get active collections: %w", err) } + if row != nil { + stats.ActiveCollections = row.Int(0) + row.Close() + } // Found items across all players - err = dcm.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM character_collection_items").Scan(&stats.FoundItems) + row, err = dcm.db.QueryRow("SELECT COUNT(*) FROM character_collection_items") if err != nil { return stats, fmt.Errorf("failed to get found items: %w", err) } + if row != nil { + stats.FoundItems = row.Int(0) + row.Close() + } // Total rewards - err = dcm.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM collection_rewards").Scan(&stats.TotalRewards) + row, err = dcm.db.QueryRow("SELECT COUNT(*) FROM collection_rewards") if err != nil { return stats, fmt.Errorf("failed to get total rewards: %w", err) } + if row != nil { + stats.TotalRewards = row.Int(0) + row.Close() + } return stats, nil -} +} \ No newline at end of file