From fa47af5ffe091f5e7951e21de6bef0e8ce0d63c9 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Wed, 6 Aug 2025 13:27:01 -0500 Subject: [PATCH] fix trade package --- internal/trade/constants.go | 6 +- internal/trade/trade_test.go | 1394 ++++++++++++++++++++++++++++++++++ internal/trade/types.go | 4 +- 3 files changed, 1399 insertions(+), 5 deletions(-) create mode 100644 internal/trade/trade_test.go diff --git a/internal/trade/constants.go b/internal/trade/constants.go index 94c686b..10680de 100644 --- a/internal/trade/constants.go +++ b/internal/trade/constants.go @@ -21,9 +21,9 @@ const ( // Trade slot configuration const ( - TradeMaxSlotsDefault = 12 // Default max slots for newer clients - TradeMaxSlotsLegacy = 6 // Max slots for older clients (version <= 561) - TradeSlotAutoFind = 255 // Automatically find next free slot + TradeMaxSlotsDefault = 12 // Default max slots for newer clients + TradeMaxSlotsLegacy = 6 // Max slots for older clients (version <= 561) + TradeSlotAutoFind = -1 // Automatically find next free slot ) // Coin conversion constants (from C++ CalculateCoins) diff --git a/internal/trade/trade_test.go b/internal/trade/trade_test.go new file mode 100644 index 0000000..1eb694d --- /dev/null +++ b/internal/trade/trade_test.go @@ -0,0 +1,1394 @@ +package trade + +import ( + "sync" + "testing" + "time" +) + +// Test coin calculation utilities +func TestCalculateCoins(t *testing.T) { + tests := []struct { + name string + totalCopper int64 + expectedPt int32 + expectedGold int32 + expectedSilv int32 + expectedCopp int32 + }{ + {"Zero coins", 0, 0, 0, 0, 0}, + {"Only copper", 50, 0, 0, 0, 50}, + {"Only silver", 500, 0, 0, 5, 0}, + {"Only gold", 50000, 0, 5, 0, 0}, + {"Only platinum", 5000000, 5, 0, 0, 0}, + {"Mixed coins", 1234567, 1, 23, 45, 67}, + {"Large amount", 9999999999, 9999, 99, 99, 99}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := CalculateCoins(tt.totalCopper) + if result.Platinum != tt.expectedPt { + t.Errorf("Expected platinum %d, got %d", tt.expectedPt, result.Platinum) + } + if result.Gold != tt.expectedGold { + t.Errorf("Expected gold %d, got %d", tt.expectedGold, result.Gold) + } + if result.Silver != tt.expectedSilv { + t.Errorf("Expected silver %d, got %d", tt.expectedSilv, result.Silver) + } + if result.Copper != tt.expectedCopp { + t.Errorf("Expected copper %d, got %d", tt.expectedCopp, result.Copper) + } + }) + } +} + +func TestCoinsToCopper(t *testing.T) { + coins := CoinAmounts{ + Platinum: 1, + Gold: 23, + Silver: 45, + Copper: 67, + } + expected := int64(1234567) + result := CoinsToCopper(coins) + if result != expected { + t.Errorf("Expected %d copper, got %d", expected, result) + } +} + +func TestFormatCoins(t *testing.T) { + tests := []struct { + name string + copper int64 + expected string + }{ + {"Zero coins", 0, "0 copper"}, + {"Only copper", 50, "50 copper"}, + {"Only silver", 500, "5 silver"}, + {"Mixed coins", 1234567, "1 platinum, 23 gold, 45 silver, 67 copper"}, + {"No copper", 1230000, "1 platinum, 23 gold"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FormatCoins(tt.copper) + if result != tt.expected { + t.Errorf("Expected '%s', got '%s'", tt.expected, result) + } + }) + } +} + +// Test validation utilities +func TestValidateTradeSlot(t *testing.T) { + tests := []struct { + name string + slot int8 + maxSlots int8 + expected bool + }{ + {"Valid slot 0", 0, 12, true}, + {"Valid slot middle", 5, 12, true}, + {"Valid slot max-1", 11, 12, true}, + {"Invalid negative", -1, 12, false}, + {"Invalid too high", 12, 12, false}, + {"Invalid way too high", 100, 12, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ValidateTradeSlot(tt.slot, tt.maxSlots) + if result != tt.expected { + t.Errorf("Expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestValidateTradeQuantity(t *testing.T) { + tests := []struct { + name string + quantity int32 + available int32 + expected bool + }{ + {"Valid quantity", 5, 10, true}, + {"Valid exact match", 10, 10, true}, + {"Invalid zero", 0, 10, false}, + {"Invalid negative", -1, 10, false}, + {"Invalid too much", 11, 10, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ValidateTradeQuantity(tt.quantity, tt.available) + if result != tt.expected { + t.Errorf("Expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestFormatTradeError(t *testing.T) { + tests := []struct { + name string + code int32 + expected string + }{ + {"Success", TradeResultSuccess, "Success"}, + {"Already in trade", TradeResultAlreadyInTrade, "Item is already in the trade"}, + {"No trade", TradeResultNoTrade, "Item cannot be traded"}, + {"Heirloom", TradeResultHeirloom, "Heirloom item cannot be traded to this player"}, + {"Invalid slot", TradeResultInvalidSlot, "Invalid or occupied trade slot"}, + {"Slot out of range", TradeResultSlotOutOfRange, "Trade slot is out of range"}, + {"Insufficient qty", TradeResultInsufficientQty, "Insufficient quantity to trade"}, + {"Unknown error", 999, "Unknown trade error: 999"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FormatTradeError(tt.code) + if result != tt.expected { + t.Errorf("Expected '%s', got '%s'", tt.expected, result) + } + }) + } +} + +func TestGetClientMaxSlots(t *testing.T) { + tests := []struct { + name string + version int32 + expected int8 + }{ + {"Very old client", 500, TradeMaxSlotsLegacy}, + {"Legacy client", 561, TradeMaxSlotsLegacy}, + {"Modern client", 562, TradeMaxSlotsDefault}, + {"New client", 1000, TradeMaxSlotsDefault}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetClientMaxSlots(tt.version) + if result != tt.expected { + t.Errorf("Expected %d slots, got %d", tt.expected, result) + } + }) + } +} + +func TestIsValidTradeState(t *testing.T) { + tests := []struct { + name string + state TradeState + operation string + expected bool + }{ + {"Add item active", TradeStateActive, "add_item", true}, + {"Add item completed", TradeStateCompleted, "add_item", false}, + {"Cancel active", TradeStateActive, "cancel", true}, + {"Complete accepted", TradeStateAccepted, "complete", true}, + {"Complete active", TradeStateActive, "complete", false}, + {"Invalid operation", TradeStateActive, "invalid", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsValidTradeState(tt.state, tt.operation) + if result != tt.expected { + t.Errorf("Expected %v, got %v", tt.expected, result) + } + }) + } +} + +// Test TradeValidationError +func TestTradeValidationError(t *testing.T) { + err := &TradeValidationError{ + Code: TradeResultNoTrade, + Message: "Test error message", + } + + if err.Error() != "Test error message" { + t.Errorf("Expected 'Test error message', got '%s'", err.Error()) + } +} + +// Test placeholder implementations +func TestPlaceholderEntity(t *testing.T) { + entity := &PlaceholderEntity{ + ID: 123, + Name: "Test Entity", + IsPlayerFlag: true, + IsBotFlag: false, + CoinsAmount: 10000, + ClientVer: 800, + } + + if entity.GetID() != 123 { + t.Errorf("Expected ID 123, got %d", entity.GetID()) + } + + if entity.GetName() != "Test Entity" { + t.Errorf("Expected name 'Test Entity', got '%s'", entity.GetName()) + } + + if !entity.IsPlayer() { + t.Error("Expected entity to be a player") + } + + if entity.IsBot() { + t.Error("Expected entity not to be a bot") + } + + if !entity.HasCoins(5000) { + t.Error("Expected entity to have enough coins") + } + + if entity.HasCoins(15000) { + t.Error("Expected entity not to have enough coins") + } + + if entity.GetClientVersion() != 800 { + t.Errorf("Expected client version 800, got %d", entity.GetClientVersion()) + } + + // Test default name + entityNoName := &PlaceholderEntity{ID: 456} + expectedName := "Entity_456" + if entityNoName.GetName() != expectedName { + t.Errorf("Expected default name '%s', got '%s'", expectedName, entityNoName.GetName()) + } + + // Test default client version + entityNoVersion := &PlaceholderEntity{ID: 789} + if entityNoVersion.GetClientVersion() != 1000 { + t.Errorf("Expected default client version 1000, got %d", entityNoVersion.GetClientVersion()) + } +} + +func TestPlaceholderItem(t *testing.T) { + creationTime := time.Now() + item := &PlaceholderItem{ + ID: 456, + Name: "Test Item", + Quantity: 10, + IconID: 789, + NoTradeFlag: false, + HeirloomFlag: true, + AttunedFlag: false, + CreatedTime: creationTime, + GroupIDs: []int32{1, 2, 3}, + } + + if item.GetID() != 456 { + t.Errorf("Expected ID 456, got %d", item.GetID()) + } + + if item.GetName() != "Test Item" { + t.Errorf("Expected name 'Test Item', got '%s'", item.GetName()) + } + + if item.GetQuantity() != 10 { + t.Errorf("Expected quantity 10, got %d", item.GetQuantity()) + } + + if item.GetIcon(1000) != 789 { + t.Errorf("Expected icon ID 789, got %d", item.GetIcon(1000)) + } + + if item.IsNoTrade() { + t.Error("Expected item not to be no-trade") + } + + if !item.IsHeirloom() { + t.Error("Expected item to be heirloom") + } + + if item.IsAttuned() { + t.Error("Expected item not to be attuned") + } + + if !item.GetCreationTime().Equal(creationTime) { + t.Error("Expected creation time to match") + } + + groupIDs := item.GetGroupCharacterIDs() + if len(groupIDs) != 3 || groupIDs[0] != 1 || groupIDs[1] != 2 || groupIDs[2] != 3 { + t.Errorf("Expected group IDs [1,2,3], got %v", groupIDs) + } + + // Test default name + itemNoName := &PlaceholderItem{ID: 999} + expectedName := "Item_999" + if itemNoName.GetName() != expectedName { + t.Errorf("Expected default name '%s', got '%s'", expectedName, itemNoName.GetName()) + } +} + +// Test TradeParticipant +func TestNewTradeParticipant(t *testing.T) { + // Test legacy client + participant := NewTradeParticipant(123, false, 561) + if participant.EntityID != 123 { + t.Errorf("Expected entity ID 123, got %d", participant.EntityID) + } + if participant.IsBot != false { + t.Error("Expected participant not to be a bot") + } + if participant.MaxSlots != TradeMaxSlotsLegacy { + t.Errorf("Expected %d max slots, got %d", TradeMaxSlotsLegacy, participant.MaxSlots) + } + if participant.ClientVersion != 561 { + t.Errorf("Expected client version 561, got %d", participant.ClientVersion) + } + if participant.HasAccepted { + t.Error("Expected participant not to have accepted initially") + } + if len(participant.Items) != 0 { + t.Error("Expected empty items map initially") + } + if participant.Coins != 0 { + t.Error("Expected zero coins initially") + } + + // Test modern client + participant2 := NewTradeParticipant(456, true, 1000) + if participant2.MaxSlots != TradeMaxSlotsDefault { + t.Errorf("Expected %d max slots, got %d", TradeMaxSlotsDefault, participant2.MaxSlots) + } + if participant2.IsBot != true { + t.Error("Expected participant to be a bot") + } +} + +func TestTradeParticipantMethods(t *testing.T) { + participant := NewTradeParticipant(123, false, 1000) + + // Test GetNextFreeSlot + slot := participant.GetNextFreeSlot() + if slot != 0 { + t.Errorf("Expected first free slot to be 0, got %d", slot) + } + + // Add an item and test slot finding + testItem := &PlaceholderItem{ID: 100, Quantity: 5} + participant.Items[0] = TradeItemInfo{Item: testItem, Quantity: 5} + + slot = participant.GetNextFreeSlot() + if slot != 1 { + t.Errorf("Expected next free slot to be 1, got %d", slot) + } + + // Fill all slots + for i := int8(1); i < participant.MaxSlots; i++ { + participant.Items[i] = TradeItemInfo{Item: testItem, Quantity: 1} + } + + slot = participant.GetNextFreeSlot() + if slot != int8(TradeSlotAutoFind) { + t.Errorf("Expected no free slots (%d), got %d", int8(TradeSlotAutoFind), slot) + } + + // Test HasItem + if !participant.HasItem(100) { + t.Error("Expected participant to have item 100") + } + if participant.HasItem(999) { + t.Error("Expected participant not to have item 999") + } + + // Test GetItemCount + expectedCount := int(participant.MaxSlots) + if participant.GetItemCount() != expectedCount { + t.Errorf("Expected %d items, got %d", expectedCount, participant.GetItemCount()) + } + + // Test ClearItems + participant.ClearItems() + if participant.GetItemCount() != 0 { + t.Error("Expected no items after clear") + } + + // Test GetCoinAmounts + participant.Coins = 1234567 + coinAmounts := participant.GetCoinAmounts() + expected := CalculateCoins(1234567) + if coinAmounts != expected { + t.Errorf("Expected coin amounts %+v, got %+v", expected, coinAmounts) + } +} + +// Test TradeManager +func TestTradeManager(t *testing.T) { + tm := NewTradeManager() + + if tm.GetActiveTradeCount() != 0 { + t.Error("Expected no active trades initially") + } + + // Create test entities + entity1 := &PlaceholderEntity{ID: 100, Name: "Player1"} + entity2 := &PlaceholderEntity{ID: 200, Name: "Player2"} + + // Create a trade + trade := NewTrade(entity1, entity2) + if trade == nil { + t.Fatal("Failed to create trade") + } + + // Add trade to manager + tm.AddTrade(trade) + + if tm.GetActiveTradeCount() != 1 { + t.Error("Expected 1 active trade") + } + + // Test GetTrade for trader1 + retrievedTrade := tm.GetTrade(100) + if retrievedTrade == nil { + t.Error("Expected to find trade for entity 100") + } + if retrievedTrade.GetTrader1ID() != 100 { + t.Error("Expected trade with trader1 ID 100") + } + + // Test GetTrade for trader2 + retrievedTrade = tm.GetTrade(200) + if retrievedTrade == nil { + t.Error("Expected to find trade for entity 200") + } + if retrievedTrade.GetTrader2ID() != 200 { + t.Error("Expected trade with trader2 ID 200") + } + + // Test GetTrade for non-participant + retrievedTrade = tm.GetTrade(999) + if retrievedTrade != nil { + t.Error("Expected no trade for entity 999") + } + + // Remove trade + tm.RemoveTrade(100) + if tm.GetActiveTradeCount() != 0 { + t.Error("Expected no active trades after removal") + } + + retrievedTrade = tm.GetTrade(100) + if retrievedTrade != nil { + t.Error("Expected no trade after removal") + } +} + +// Test Trade creation and basic operations +func TestNewTrade(t *testing.T) { + entity1 := &PlaceholderEntity{ID: 100, Name: "Player1"} + entity2 := &PlaceholderEntity{ID: 200, Name: "Player2"} + + // Valid trade creation + trade := NewTrade(entity1, entity2) + if trade == nil { + t.Fatal("Expected trade to be created") + } + + if trade.GetTrader1ID() != 100 { + t.Errorf("Expected trader1 ID 100, got %d", trade.GetTrader1ID()) + } + + if trade.GetTrader2ID() != 200 { + t.Errorf("Expected trader2 ID 200, got %d", trade.GetTrader2ID()) + } + + if trade.GetState() != TradeStateActive { + t.Errorf("Expected trade state %d, got %d", TradeStateActive, trade.GetState()) + } + + // Test GetTradee + if trade.GetTradee(100) != 200 { + t.Error("Expected tradee of 100 to be 200") + } + if trade.GetTradee(200) != 100 { + t.Error("Expected tradee of 200 to be 100") + } + if trade.GetTradee(999) != 0 { + t.Error("Expected tradee of 999 to be 0") + } + + // Test GetParticipant + participant := trade.GetParticipant(100) + if participant == nil { + t.Error("Expected to find participant 100") + } + if participant.EntityID != 100 { + t.Error("Expected participant entity ID 100") + } + + participant = trade.GetParticipant(999) + if participant != nil { + t.Error("Expected not to find participant 999") + } + + // Invalid trade creation + invalidTrade := NewTrade(nil, entity2) + if invalidTrade != nil { + t.Error("Expected trade creation to fail with nil entity") + } + + invalidTrade = NewTrade(entity1, nil) + if invalidTrade != nil { + t.Error("Expected trade creation to fail with nil entity") + } +} + +func TestTradeAddRemoveItems(t *testing.T) { + entity1 := &PlaceholderEntity{ID: 100} + entity2 := &PlaceholderEntity{ID: 200} + trade := NewTrade(entity1, entity2) + + testItem := &PlaceholderItem{ + ID: 500, + Name: "Test Item", + Quantity: 10, + } + + // Test adding item + err := trade.AddItemToTrade(100, testItem, 5, 0) + if err != nil { + t.Fatalf("Failed to add item to trade: %v", err) + } + + // Verify item was added + retrievedItem := trade.GetTraderSlot(100, 0) + if retrievedItem == nil { + t.Error("Expected to find item in slot 0") + } + if retrievedItem.GetID() != 500 { + t.Error("Expected item ID 500") + } + + // Test auto-slot finding + testItem2 := &PlaceholderItem{ID: 501, Quantity: 3} + err = trade.AddItemToTrade(100, testItem2, 3, TradeSlotAutoFind) + if err != nil { + t.Fatalf("Failed to add item with auto-slot: %v", err) + } + + retrievedItem = trade.GetTraderSlot(100, 1) + if retrievedItem == nil { + t.Error("Expected to find item in slot 1") + } + + // Test adding to occupied slot + err = trade.AddItemToTrade(100, testItem, 1, 0) + if err == nil { + t.Error("Expected error when adding to occupied slot") + } + + // Test adding duplicate item + err = trade.AddItemToTrade(100, testItem, 1, 2) + if err == nil { + t.Error("Expected error when adding duplicate item") + } + + // Test invalid slot + err = trade.AddItemToTrade(100, testItem, 1, 99) + if err == nil { + t.Error("Expected error for invalid slot") + } + + // Test insufficient quantity + err = trade.AddItemToTrade(100, testItem, 20, 3) + if err == nil { + t.Error("Expected error for insufficient quantity") + } + + // Test invalid entity + err = trade.AddItemToTrade(999, testItem, 1, 4) + if err == nil { + t.Error("Expected error for invalid entity") + } + + // Test removing item + err = trade.RemoveItemFromTrade(100, 0) + if err != nil { + t.Fatalf("Failed to remove item: %v", err) + } + + retrievedItem = trade.GetTraderSlot(100, 0) + if retrievedItem != nil { + t.Error("Expected item to be removed from slot 0") + } + + // Test removing from empty slot + err = trade.RemoveItemFromTrade(100, 0) + if err == nil { + t.Error("Expected error when removing from empty slot") + } + + // Test no-trade item + noTradeItem := &PlaceholderItem{ + ID: 600, + Quantity: 5, + NoTradeFlag: true, + } + err = trade.AddItemToTrade(100, noTradeItem, 5, 0) + if err == nil { + t.Error("Expected error for no-trade item") + } + + // Test attuned heirloom item + attunedHeirloom := &PlaceholderItem{ + ID: 700, + Quantity: 1, + HeirloomFlag: true, + AttunedFlag: true, + } + err = trade.AddItemToTrade(100, attunedHeirloom, 1, 0) + if err == nil { + t.Error("Expected error for attuned heirloom item") + } + + // Test expired heirloom item + expiredHeirloom := &PlaceholderItem{ + ID: 701, + Quantity: 1, + HeirloomFlag: true, + AttunedFlag: false, + CreatedTime: time.Now().Add(-72 * time.Hour), // 3 days ago + } + err = trade.AddItemToTrade(100, expiredHeirloom, 1, 0) + if err == nil { + t.Error("Expected error for expired heirloom item") + } + + // Test valid recent heirloom item + recentHeirloom := &PlaceholderItem{ + ID: 702, + Quantity: 1, + HeirloomFlag: true, + AttunedFlag: false, + CreatedTime: time.Now().Add(-1 * time.Hour), // 1 hour ago + } + err = trade.AddItemToTrade(100, recentHeirloom, 1, 0) + if err != nil { + t.Errorf("Expected recent heirloom item to be tradeable: %v", err) + } +} + +func TestTradeCoins(t *testing.T) { + entity1 := &PlaceholderEntity{ID: 100} + entity2 := &PlaceholderEntity{ID: 200} + trade := NewTrade(entity1, entity2) + + // Test adding coins + err := trade.AddCoinsToTrade(100, 5000) + if err != nil { + t.Fatalf("Failed to add coins to trade: %v", err) + } + + participant := trade.GetParticipant(100) + if participant.Coins != 5000 { + t.Errorf("Expected 5000 coins, got %d", participant.Coins) + } + + // Test adding more coins + err = trade.AddCoinsToTrade(100, 2000) + if err != nil { + t.Fatalf("Failed to add more coins: %v", err) + } + + if participant.Coins != 7000 { + t.Errorf("Expected 7000 coins, got %d", participant.Coins) + } + + // Test removing coins + err = trade.RemoveCoinsFromTrade(100, 3000) + if err != nil { + t.Fatalf("Failed to remove coins: %v", err) + } + + if participant.Coins != 4000 { + t.Errorf("Expected 4000 coins, got %d", participant.Coins) + } + + // Test removing more coins than available + err = trade.RemoveCoinsFromTrade(100, 10000) + if err != nil { + t.Fatalf("Failed to remove excess coins: %v", err) + } + + if participant.Coins != 0 { + t.Errorf("Expected 0 coins, got %d", participant.Coins) + } + + // Test invalid coin amount + err = trade.AddCoinsToTrade(100, -100) + if err == nil { + t.Error("Expected error for negative coin amount") + } + + err = trade.AddCoinsToTrade(100, 0) + if err == nil { + t.Error("Expected error for zero coin amount") + } + + // Test invalid entity + err = trade.AddCoinsToTrade(999, 1000) + if err == nil { + t.Error("Expected error for invalid entity") + } +} + +func TestTradeAcceptance(t *testing.T) { + entity1 := &PlaceholderEntity{ID: 100} + entity2 := &PlaceholderEntity{ID: 200} + trade := NewTrade(entity1, entity2) + + // Initially, neither should have accepted + if trade.HasAcceptedTrade(100) { + t.Error("Expected trader1 not to have accepted initially") + } + if trade.HasAcceptedTrade(200) { + t.Error("Expected trader2 not to have accepted initially") + } + if trade.HasAcceptedTrade(999) { + t.Error("Expected invalid entity not to have accepted") + } + + // First trader accepts + completed, err := trade.SetTradeAccepted(100) + if err != nil { + t.Fatalf("Failed to set trade accepted: %v", err) + } + if completed { + t.Error("Expected trade not to be completed after one acceptance") + } + + if !trade.HasAcceptedTrade(100) { + t.Error("Expected trader1 to have accepted") + } + if trade.HasAcceptedTrade(200) { + t.Error("Expected trader2 not to have accepted yet") + } + + // Second trader accepts - should complete + completed, err = trade.SetTradeAccepted(200) + if err != nil { + t.Fatalf("Failed to set second trade accepted: %v", err) + } + if !completed { + t.Error("Expected trade to be completed after both acceptances") + } + + if trade.GetState() != TradeStateCompleted { + t.Errorf("Expected trade state %d, got %d", TradeStateCompleted, trade.GetState()) + } + + // Test accepting invalid entity + _, err = trade.SetTradeAccepted(999) + if err == nil { + t.Error("Expected error for invalid entity acceptance") + } +} + +func TestTradeCancel(t *testing.T) { + entity1 := &PlaceholderEntity{ID: 100} + entity2 := &PlaceholderEntity{ID: 200} + trade := NewTrade(entity1, entity2) + + // Cancel trade + err := trade.CancelTrade(100) + if err != nil { + t.Fatalf("Failed to cancel trade: %v", err) + } + + if trade.GetState() != TradeStateCanceled { + t.Errorf("Expected trade state %d, got %d", TradeStateCanceled, trade.GetState()) + } + + // Test operations on canceled trade + testItem := &PlaceholderItem{ID: 500, Quantity: 5} + err = trade.AddItemToTrade(100, testItem, 5, 0) + if err == nil { + t.Error("Expected error adding item to canceled trade") + } + + err = trade.AddCoinsToTrade(100, 1000) + if err == nil { + t.Error("Expected error adding coins to canceled trade") + } + + _, err = trade.SetTradeAccepted(100) + if err == nil { + t.Error("Expected error accepting canceled trade") + } +} + +func TestTradeStateChangesResetAcceptance(t *testing.T) { + entity1 := &PlaceholderEntity{ID: 100} + entity2 := &PlaceholderEntity{ID: 200} + trade := NewTrade(entity1, entity2) + + // Both traders accept + trade.SetTradeAccepted(100) + trade.SetTradeAccepted(200) // This completes the trade + + // Create new trade for testing reset behavior + trade2 := NewTrade(entity1, entity2) + + // First trader accepts + trade2.SetTradeAccepted(100) + if !trade2.HasAcceptedTrade(100) { + t.Error("Expected trader1 to have accepted") + } + + // Add item - should reset acceptance + testItem := &PlaceholderItem{ID: 500, Quantity: 5} + err := trade2.AddItemToTrade(100, testItem, 5, 0) + if err != nil { + t.Fatalf("Failed to add item: %v", err) + } + + if trade2.HasAcceptedTrade(100) { + t.Error("Expected acceptance to be reset after adding item") + } + + // Accept again and add coins - should reset + trade2.SetTradeAccepted(100) + err = trade2.AddCoinsToTrade(100, 1000) + if err != nil { + t.Fatalf("Failed to add coins: %v", err) + } + + if trade2.HasAcceptedTrade(100) { + t.Error("Expected acceptance to be reset after adding coins") + } + + // Accept again and remove item - should reset + trade2.SetTradeAccepted(100) + err = trade2.RemoveItemFromTrade(100, 0) + if err != nil { + t.Fatalf("Failed to remove item: %v", err) + } + + if trade2.HasAcceptedTrade(100) { + t.Error("Expected acceptance to be reset after removing item") + } + + // Accept again and remove coins - should reset + trade2.SetTradeAccepted(100) + err = trade2.RemoveCoinsFromTrade(100, 500) + if err != nil { + t.Fatalf("Failed to remove coins: %v", err) + } + + if trade2.HasAcceptedTrade(100) { + t.Error("Expected acceptance to be reset after removing coins") + } +} + +func TestGetTradeInfo(t *testing.T) { + entity1 := &PlaceholderEntity{ID: 100} + entity2 := &PlaceholderEntity{ID: 200} + trade := NewTrade(entity1, entity2) + + // Add some items and coins + testItem := &PlaceholderItem{ID: 500, Quantity: 5} + trade.AddItemToTrade(100, testItem, 5, 0) + trade.AddCoinsToTrade(200, 10000) + + info := trade.GetTradeInfo() + + if info["state"] != TradeStateActive { + t.Error("Expected trade state to be active") + } + if info["trader1_id"] != int32(100) { + t.Error("Expected trader1_id to be 100") + } + if info["trader2_id"] != int32(200) { + t.Error("Expected trader2_id to be 200") + } + if info["trader1_items"] != 1 { + t.Error("Expected trader1 to have 1 item") + } + if info["trader2_items"] != 0 { + t.Error("Expected trader2 to have 0 items") + } + if info["trader1_coins"] != int64(0) { + t.Error("Expected trader1 to have 0 coins") + } + if info["trader2_coins"] != int64(10000) { + t.Error("Expected trader2 to have 10000 coins") + } + if info["trader1_accepted"] != false { + t.Error("Expected trader1 not to have accepted") + } + if info["trader2_accepted"] != false { + t.Error("Expected trader2 not to have accepted") + } + + // Test after acceptance + trade.SetTradeAccepted(100) + info = trade.GetTradeInfo() + if info["trader1_accepted"] != true { + t.Error("Expected trader1 to have accepted") + } +} + +// Test TradeService +func TestTradeService(t *testing.T) { + service := NewTradeService() + + if service.GetActiveTradeCount() != 0 { + t.Error("Expected no active trades initially") + } + + // Test InitiateTrade + trade, err := service.InitiateTrade(100, 200) + if err != nil { + t.Fatalf("Failed to initiate trade: %v", err) + } + if trade == nil { + t.Fatal("Expected trade to be created") + } + + if service.GetActiveTradeCount() != 1 { + t.Error("Expected 1 active trade") + } + + // Test duplicate trade initiation + _, err = service.InitiateTrade(100, 300) + if err == nil { + t.Error("Expected error when initiating duplicate trade") + } + + _, err = service.InitiateTrade(300, 200) + if err == nil { + t.Error("Expected error when target is already in trade") + } + + // Test GetTrade + retrievedTrade := service.GetTrade(100) + if retrievedTrade == nil { + t.Error("Expected to find trade") + } + + // Test service operations + testItem := &PlaceholderItem{ID: 500, Quantity: 10} + err = service.AddItemToTrade(100, testItem, 5, 0) + if err != nil { + t.Fatalf("Failed to add item via service: %v", err) + } + + err = service.AddCoinsToTrade(200, 5000) + if err != nil { + t.Fatalf("Failed to add coins via service: %v", err) + } + + // Test GetTradeInfo + info, err := service.GetTradeInfo(100) + if err != nil { + t.Fatalf("Failed to get trade info: %v", err) + } + if info["trader1_items"] != 1 { + t.Error("Expected 1 item in trade info") + } + + // Test AcceptTrade + completed, err := service.AcceptTrade(100) + if err != nil { + t.Fatalf("Failed to accept trade: %v", err) + } + if completed { + t.Error("Expected trade not to be completed yet") + } + + completed, err = service.AcceptTrade(200) + if err != nil { + t.Fatalf("Failed to accept trade for second trader: %v", err) + } + if !completed { + t.Error("Expected trade to be completed") + } + + if service.GetActiveTradeCount() != 0 { + t.Error("Expected no active trades after completion") + } +} + +func TestTradeServiceValidation(t *testing.T) { + service := NewTradeService() + + // Test ValidateTradeRequest + err := service.ValidateTradeRequest(100, 100) + if err == nil { + t.Error("Expected error for self-trade") + } + + err = service.ValidateTradeRequest(0, 100) + if err == nil { + t.Error("Expected error for invalid initiator ID") + } + + err = service.ValidateTradeRequest(100, -1) + if err == nil { + t.Error("Expected error for invalid target ID") + } + + // Valid request + err = service.ValidateTradeRequest(100, 200) + if err != nil { + t.Errorf("Expected valid trade request: %v", err) + } + + // After creating trade, validation should fail + service.InitiateTrade(100, 200) + err = service.ValidateTradeRequest(100, 300) + if err == nil { + t.Error("Expected error for already trading initiator") + } + + err = service.ValidateTradeRequest(300, 200) + if err == nil { + t.Error("Expected error for already trading target") + } +} + +func TestTradeServiceCancel(t *testing.T) { + service := NewTradeService() + + trade, _ := service.InitiateTrade(100, 200) + if trade == nil { + t.Fatal("Failed to create trade") + } + + err := service.CancelTrade(100) + if err != nil { + t.Fatalf("Failed to cancel trade: %v", err) + } + + if service.GetActiveTradeCount() != 0 { + t.Error("Expected no active trades after cancellation") + } + + // Test canceling non-existent trade + err = service.CancelTrade(999) + if err == nil { + t.Error("Expected error canceling non-existent trade") + } +} + +func TestTradeServiceAdminFunctions(t *testing.T) { + service := NewTradeService() + + trade, _ := service.InitiateTrade(100, 200) + if trade == nil { + t.Fatal("Failed to create trade") + } + + // Test ForceCompleteTrade + err := service.ForceCompleteTrade(100) + if err != nil { + t.Fatalf("Failed to force complete trade: %v", err) + } + + if service.GetActiveTradeCount() != 0 { + t.Error("Expected no active trades after forced completion") + } + + // Test ForceCancelTrade + trade2, _ := service.InitiateTrade(300, 400) + if trade2 == nil { + t.Fatal("Failed to create second trade") + } + + err = service.ForceCancelTrade(300, "Admin intervention") + if err != nil { + t.Fatalf("Failed to force cancel trade: %v", err) + } + + if service.GetActiveTradeCount() != 0 { + t.Error("Expected no active trades after forced cancellation") + } +} + +func TestTradeServiceStatistics(t *testing.T) { + service := NewTradeService() + + stats := service.GetTradeStatistics() + if stats["active_trades"] != 0 { + t.Error("Expected 0 active trades in statistics") + } + + if stats["max_trade_duration_minutes"] != float64(30) { + t.Error("Expected 30 minute max duration") + } + + // Create some trades + service.InitiateTrade(100, 200) + service.InitiateTrade(300, 400) + + stats = service.GetTradeStatistics() + if stats["active_trades"] != 2 { + t.Error("Expected 2 active trades in statistics") + } +} + +func TestTradeServiceShutdown(t *testing.T) { + service := NewTradeService() + + service.InitiateTrade(100, 200) + service.InitiateTrade(300, 400) + + if service.GetActiveTradeCount() != 2 { + t.Fatal("Expected 2 active trades before shutdown") + } + + service.Shutdown() + + if service.GetActiveTradeCount() != 0 { + t.Error("Expected no active trades after shutdown") + } +} + +// Test concurrent access +func TestTradeConcurrency(t *testing.T) { + service := NewTradeService() + trade, _ := service.InitiateTrade(100, 200) + + var wg sync.WaitGroup + numGoroutines := 10 + numOperations := 100 + + // Test concurrent coin operations + wg.Add(numGoroutines) + for i := 0; i < numGoroutines; i++ { + go func(id int) { + defer wg.Done() + for j := 0; j < numOperations; j++ { + if j%2 == 0 { + trade.AddCoinsToTrade(100, 100) + } else { + trade.RemoveCoinsFromTrade(100, 50) + } + } + }(i) + } + + wg.Wait() + + // Verify trade is still in valid state + if trade.GetState() != TradeStateActive { + t.Error("Expected trade to remain active after concurrent operations") + } + + info := trade.GetTradeInfo() + if info == nil { + t.Error("Expected to get trade info after concurrent operations") + } +} + +// Test utility functions +func TestCompareTradeItems(t *testing.T) { + item1 := &PlaceholderItem{ID: 100} + item2 := &PlaceholderItem{ID: 200} + + tradeItem1 := TradeItemInfo{Item: item1, Quantity: 5} + tradeItem2 := TradeItemInfo{Item: item1, Quantity: 5} + tradeItem3 := TradeItemInfo{Item: item2, Quantity: 5} + tradeItem4 := TradeItemInfo{Item: item1, Quantity: 10} + + // Test equal items + if !CompareTradeItems(tradeItem1, tradeItem2) { + t.Error("Expected items to be equal") + } + + // Test different items + if CompareTradeItems(tradeItem1, tradeItem3) { + t.Error("Expected items to be different (different item)") + } + + // Test different quantities + if CompareTradeItems(tradeItem1, tradeItem4) { + t.Error("Expected items to be different (different quantity)") + } + + // Test nil items + nilItem1 := TradeItemInfo{Item: nil, Quantity: 5} + nilItem2 := TradeItemInfo{Item: nil, Quantity: 5} + nilItem3 := TradeItemInfo{Item: nil, Quantity: 10} + + if !CompareTradeItems(nilItem1, nilItem2) { + t.Error("Expected nil items with same quantity to be equal") + } + + if CompareTradeItems(nilItem1, nilItem3) { + t.Error("Expected nil items with different quantity to be different") + } + + if CompareTradeItems(nilItem1, tradeItem1) { + t.Error("Expected nil item and real item to be different") + } +} + +func TestCalculateTradeValue(t *testing.T) { + participant := NewTradeParticipant(123, false, 1000) + participant.Coins = 50000 + + item1 := &PlaceholderItem{ID: 100, Name: "Sword"} + item2 := &PlaceholderItem{ID: 200, Name: "Shield"} + + participant.Items[0] = TradeItemInfo{Item: item1, Quantity: 1} + participant.Items[1] = TradeItemInfo{Item: item2, Quantity: 2} + + value := CalculateTradeValue(participant) + + if value["coins"] != int64(50000) { + t.Error("Expected coins value to be 50000") + } + + coinsFormatted, ok := value["coins_formatted"].(string) + if !ok || coinsFormatted != "5 gold" { + t.Errorf("Expected coins_formatted to be '5 gold', got %v", coinsFormatted) + } + + if value["item_count"] != 2 { + t.Error("Expected item count to be 2") + } + + items, ok := value["items"].([]map[string]any) + if !ok || len(items) != 2 { + t.Error("Expected items array with 2 elements") + } + + // Verify item data (order may vary due to map iteration) + foundSword := false + foundShield := false + for _, item := range items { + if item["item_name"] == "Sword" { + foundSword = true + if item["quantity"] != int32(1) { + t.Error("Expected sword quantity to be 1") + } + } + if item["item_name"] == "Shield" { + foundShield = true + if item["quantity"] != int32(2) { + t.Error("Expected shield quantity to be 2") + } + } + } + + if !foundSword || !foundShield { + t.Error("Expected to find both sword and shield in items") + } +} + +func TestValidateTradeCompletion(t *testing.T) { + entity1 := &PlaceholderEntity{ID: 100} + entity2 := &PlaceholderEntity{ID: 200} + trade := NewTrade(entity1, entity2) + + // Test validation with no acceptance + errors := ValidateTradeCompletion(trade) + expectedErrors := 2 // Neither trader has accepted + if len(errors) != expectedErrors { + t.Errorf("Expected %d errors, got %d: %v", expectedErrors, len(errors), errors) + } + + // Test validation with one acceptance + trade.SetTradeAccepted(100) + errors = ValidateTradeCompletion(trade) + expectedErrors = 1 // Only trader2 hasn't accepted + if len(errors) != expectedErrors { + t.Errorf("Expected %d errors, got %d: %v", expectedErrors, len(errors), errors) + } + + // Test validation with both acceptances + trade.SetTradeAccepted(200) // This completes the trade + + // Create new trade for testing completed state validation + completedTrade := NewTrade(entity1, entity2) + completedTrade.SetTradeAccepted(100) + completedTrade.SetTradeAccepted(200) // This marks as completed + + errors = ValidateTradeCompletion(completedTrade) + expectedErrors = 1 // Trade is not in active state + if len(errors) != expectedErrors { + t.Errorf("Expected %d errors for completed trade, got %d: %v", expectedErrors, len(errors), errors) + } +} + +func TestGenerateTradeLogEntry(t *testing.T) { + tradeID := "trade_123" + operation := "add_item" + entityID := int32(456) + details := map[string]any{"item_id": 789, "quantity": 5} + + logEntry := GenerateTradeLogEntry(tradeID, operation, entityID, details) + expected := "[Trade:trade_123] add_item by entity 456: map[item_id:789 quantity:5]" + + if logEntry != expected { + t.Errorf("Expected log entry '%s', got '%s'", expected, logEntry) + } +} + +// Benchmark tests +func BenchmarkCalculateCoins(b *testing.B) { + for i := 0; i < b.N; i++ { + CalculateCoins(1234567) + } +} + +func BenchmarkTradeCreation(b *testing.B) { + entity1 := &PlaceholderEntity{ID: 100} + entity2 := &PlaceholderEntity{ID: 200} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + trade := NewTrade(entity1, entity2) + _ = trade + } +} + +func BenchmarkTradeItemOperations(b *testing.B) { + entity1 := &PlaceholderEntity{ID: 100} + entity2 := &PlaceholderEntity{ID: 200} + trade := NewTrade(entity1, entity2) + item := &PlaceholderItem{ID: 500, Quantity: 100} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + slot := int8(i % int(TradeMaxSlotsDefault)) + trade.AddItemToTrade(100, item, 1, slot) + trade.RemoveItemFromTrade(100, slot) + } +} + +func BenchmarkTradeCoinOperations(b *testing.B) { + entity1 := &PlaceholderEntity{ID: 100} + entity2 := &PlaceholderEntity{ID: 200} + trade := NewTrade(entity1, entity2) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + trade.AddCoinsToTrade(100, 1000) + trade.RemoveCoinsFromTrade(100, 500) + } +} + +func BenchmarkTradeManagerOperations(b *testing.B) { + tm := NewTradeManager() + entity1 := &PlaceholderEntity{ID: 100} + entity2 := &PlaceholderEntity{ID: 200} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + trade := NewTrade(entity1, entity2) + tm.AddTrade(trade) + tm.GetTrade(100) + tm.RemoveTrade(100) + } +} \ No newline at end of file diff --git a/internal/trade/types.go b/internal/trade/types.go index 25b4955..3271ba0 100644 --- a/internal/trade/types.go +++ b/internal/trade/types.go @@ -8,7 +8,7 @@ import ( // TradeItemInfo represents an item in a trade slot // Converted from C++ TradeItemInfo struct type TradeItemInfo struct { - Item *Item // TODO: Replace with actual Item type when available + Item Item // TODO: Replace with actual Item type when available Quantity int32 // Quantity of the item being traded } @@ -77,7 +77,7 @@ func (tp *TradeParticipant) GetNextFreeSlot() int8 { return slot } } - return TradeSlotAutoFind // No free slots available + return int8(TradeSlotAutoFind) // No free slots available } // HasItem checks if participant has a specific item in trade