eq2go/internal/trade/trade_test.go
2025-08-06 13:27:01 -05:00

1394 lines
35 KiB
Go

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)
}
}