package appearances import ( "sync" "testing" "eq2emu/internal/database" ) func TestNew(t *testing.T) { db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared") if err != nil { t.Fatalf("Failed to create test database: %v", err) } defer db.Close() // Test creating a new appearance appearance := New(db) if appearance == nil { t.Fatal("New returned nil") } if !appearance.IsNew() { t.Error("New appearance should be marked as new") } // Test setting values appearance.ID = 1001 appearance.Name = "Test Appearance" appearance.MinClient = 1096 if appearance.GetID() != 1001 { t.Errorf("Expected GetID() to return 1001, got %d", appearance.GetID()) } if appearance.GetName() != "Test Appearance" { t.Errorf("Expected GetName() to return 'Test Appearance', got %s", appearance.GetName()) } if appearance.GetMinClientVersion() != 1096 { t.Errorf("Expected GetMinClientVersion() to return 1096, got %d", appearance.GetMinClientVersion()) } } func TestNewWithData(t *testing.T) { db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared") if err != nil { t.Fatalf("Failed to create test database: %v", err) } defer db.Close() appearance := NewWithData(100, "Human Male", 1096, db) if appearance == nil { t.Fatal("NewWithData returned nil") } if appearance.GetID() != 100 { t.Errorf("Expected ID 100, got %d", appearance.GetID()) } if appearance.GetName() != "Human Male" { t.Errorf("Expected name 'Human Male', got '%s'", appearance.GetName()) } if appearance.GetMinClientVersion() != 1096 { t.Errorf("Expected min client 1096, got %d", appearance.GetMinClientVersion()) } if !appearance.IsNew() { t.Error("NewWithData should create new appearance") } } func TestAppearanceGetters(t *testing.T) { db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared") defer db.Close() app := NewWithData(123, "Test Appearance", 1096, db) if id := app.GetID(); id != 123 { t.Errorf("GetID() = %v, want 123", id) } if name := app.GetName(); name != "Test Appearance" { t.Errorf("GetName() = %v, want Test Appearance", name) } if nameStr := app.GetNameString(); nameStr != "Test Appearance" { t.Errorf("GetNameString() = %v, want Test Appearance", nameStr) } if minVer := app.GetMinClientVersion(); minVer != 1096 { t.Errorf("GetMinClientVersion() = %v, want 1096", minVer) } } func TestAppearanceSetters(t *testing.T) { db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared") defer db.Close() app := NewWithData(100, "Original", 1000, db) app.SetName("Modified Name") if app.GetName() != "Modified Name" { t.Errorf("SetName failed: got %v, want Modified Name", app.GetName()) } app.SetMinClientVersion(2000) if app.GetMinClientVersion() != 2000 { t.Errorf("SetMinClientVersion failed: got %v, want 2000", app.GetMinClientVersion()) } } func TestIsCompatibleWithClient(t *testing.T) { db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared") defer db.Close() app := NewWithData(100, "Test", 1096, db) tests := []struct { clientVersion int16 want bool }{ {1095, false}, // Below minimum {1096, true}, // Exact minimum {1097, true}, // Above minimum {2000, true}, // Well above minimum {0, false}, // Zero version } for _, tt := range tests { t.Run("", func(t *testing.T) { if got := app.IsCompatibleWithClient(tt.clientVersion); got != tt.want { t.Errorf("IsCompatibleWithClient(%v) = %v, want %v", tt.clientVersion, got, tt.want) } }) } } func TestAppearanceClone(t *testing.T) { db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared") defer db.Close() original := NewWithData(500, "Original Appearance", 1200, db) clone := original.Clone() if clone == nil { t.Fatal("Clone returned nil") } if clone == original { t.Error("Clone returned same pointer as original") } if clone.GetID() != original.GetID() { t.Errorf("Clone ID = %v, want %v", clone.GetID(), original.GetID()) } if clone.GetName() != original.GetName() { t.Errorf("Clone Name = %v, want %v", clone.GetName(), original.GetName()) } if clone.GetMinClientVersion() != original.GetMinClientVersion() { t.Errorf("Clone MinClientVersion = %v, want %v", clone.GetMinClientVersion(), original.GetMinClientVersion()) } if !clone.IsNew() { t.Error("Clone should always be marked as new") } // Verify modification independence clone.SetName("Modified Clone") if original.GetName() == "Modified Clone" { t.Error("Modifying clone affected original") } } // Test appearance type functions func TestGetAppearanceType(t *testing.T) { tests := []struct { typeName string expected int8 }{ {"hair_color1", AppearanceHairColor1}, {"soga_hair_color1", AppearanceSOGAHairColor1}, {"skin_color", AppearanceSkinColor}, {"eye_color", AppearanceEyeColor}, {"unknown_type", -1}, } for _, tt := range tests { t.Run(tt.typeName, func(t *testing.T) { result := GetAppearanceType(tt.typeName) if result != tt.expected { t.Errorf("GetAppearanceType(%q) = %d, want %d", tt.typeName, result, tt.expected) } }) } } func TestGetAppearanceTypeName(t *testing.T) { tests := []struct { typeConst int8 expected string }{ {AppearanceHairColor1, "hair_color1"}, {AppearanceSOGAHairColor1, "soga_hair_color1"}, {AppearanceSkinColor, "skin_color"}, {AppearanceEyeColor, "eye_color"}, {-1, "unknown"}, {100, "unknown"}, } for _, tt := range tests { t.Run("", func(t *testing.T) { result := GetAppearanceTypeName(tt.typeConst) if result != tt.expected { t.Errorf("GetAppearanceTypeName(%d) = %q, want %q", tt.typeConst, result, tt.expected) } }) } } // TestMasterList tests the bespoke master list implementation func TestMasterList(t *testing.T) { masterList := NewMasterList() if masterList == nil { t.Fatal("NewMasterList returned nil") } if masterList.Size() != 0 { t.Errorf("Expected size 0, got %d", masterList.Size()) } // Create test database db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared") if err != nil { t.Fatalf("Failed to create test database: %v", err) } defer db.Close() // Create appearances for testing app1 := NewWithData(1001, "Human Male", 1096, db) app2 := NewWithData(1002, "Elf Female", 1200, db) app3 := NewWithData(1003, "Dwarf Warrior", 1096, db) // Test adding if !masterList.AddAppearance(app1) { t.Error("Should successfully add app1") } if !masterList.AddAppearance(app2) { t.Error("Should successfully add app2") } if !masterList.AddAppearance(app3) { t.Error("Should successfully add app3") } if masterList.Size() != 3 { t.Errorf("Expected size 3, got %d", masterList.Size()) } // Test duplicate add (should fail) if masterList.AddAppearance(app1) { t.Error("Should not add duplicate appearance") } // Test retrieving retrieved := masterList.GetAppearance(1001) if retrieved == nil { t.Error("Should retrieve added appearance") } if retrieved.Name != "Human Male" { t.Errorf("Expected name 'Human Male', got '%s'", retrieved.Name) } // Test safe retrieval retrievedSafe, exists := masterList.GetAppearanceSafe(1001) if !exists { t.Error("Should find existing appearance") } if retrievedSafe.Name != "Human Male" { t.Errorf("Expected safe name 'Human Male', got '%s'", retrievedSafe.Name) } _, notExists := masterList.GetAppearanceSafe(9999) if notExists { t.Error("Should not find non-existent appearance") } // Test client version filtering version1096 := masterList.FindAppearancesByMinClient(1096) if len(version1096) != 2 { t.Errorf("Expected 2 appearances with min client 1096, got %d", len(version1096)) } version1200 := masterList.FindAppearancesByMinClient(1200) if len(version1200) != 1 { t.Errorf("Expected 1 appearance with min client 1200, got %d", len(version1200)) } // Test compatible appearances compatible1200 := masterList.GetCompatibleAppearances(1200) if len(compatible1200) != 3 { t.Errorf("Expected 3 appearances compatible with client 1200, got %d", len(compatible1200)) } compatible1100 := masterList.GetCompatibleAppearances(1100) if len(compatible1100) != 2 { t.Errorf("Expected 2 appearances compatible with client 1100, got %d", len(compatible1100)) } // Test name searching (case insensitive) // Names: "Human Male", "Elf Female", "Dwarf Warrior" humanApps := masterList.FindAppearancesByName("human") if len(humanApps) != 1 { t.Errorf("Expected 1 appearance with 'human' in name, got %d", len(humanApps)) } maleApps := masterList.FindAppearancesByName("male") if len(maleApps) != 1 { // Only "Human Male" contains "male" t.Errorf("Expected 1 appearance with 'male' in name, got %d", len(maleApps)) } // Test exact name match (indexed lookup) humanMaleApps := masterList.FindAppearancesByName("human male") if len(humanMaleApps) != 1 { t.Errorf("Expected 1 appearance with exact name 'human male', got %d", len(humanMaleApps)) } // Test ID range filtering rangeApps := masterList.GetAppearancesByIDRange(1001, 1002) if len(rangeApps) != 2 { t.Errorf("Expected 2 appearances in range 1001-1002, got %d", len(rangeApps)) } // Test client version range filtering clientRangeApps := masterList.GetAppearancesByClientRange(1096, 1096) if len(clientRangeApps) != 2 { t.Errorf("Expected 2 appearances in client range 1096-1096, got %d", len(clientRangeApps)) } // Test metadata caching clientVersions := masterList.GetClientVersions() if len(clientVersions) != 2 { t.Errorf("Expected 2 unique client versions, got %d", len(clientVersions)) } // Test clone clone := masterList.GetAppearanceClone(1001) if clone == nil { t.Error("Should return cloned appearance") } if clone.Name != "Human Male" { t.Errorf("Expected cloned name 'Human Male', got '%s'", clone.Name) } // Test GetAllAppearances allApps := masterList.GetAllAppearances() if len(allApps) != 3 { t.Errorf("Expected 3 appearances in GetAll, got %d", len(allApps)) } // Test GetAllAppearancesList allAppsList := masterList.GetAllAppearancesList() if len(allAppsList) != 3 { t.Errorf("Expected 3 appearances in GetAllList, got %d", len(allAppsList)) } // Test update updatedApp := NewWithData(1001, "Updated Human", 1500, db) if err := masterList.UpdateAppearance(updatedApp); err != nil { t.Errorf("Update should succeed: %v", err) } // Verify update worked retrievedUpdated := masterList.GetAppearance(1001) if retrievedUpdated.Name != "Updated Human" { t.Errorf("Expected updated name 'Updated Human', got '%s'", retrievedUpdated.Name) } // Verify client version index updated version1500 := masterList.FindAppearancesByMinClient(1500) if len(version1500) != 1 { t.Errorf("Expected 1 appearance with min client 1500, got %d", len(version1500)) } // Test removal if !masterList.RemoveAppearance(1001) { t.Error("Should successfully remove appearance") } if masterList.Size() != 2 { t.Errorf("Expected size 2 after removal, got %d", masterList.Size()) } // Test validation issues := masterList.ValidateAppearances() if len(issues) != 0 { t.Errorf("Expected no validation issues, got %d", len(issues)) } // Test statistics stats := masterList.GetStatistics() if stats["total_appearances"] != 2 { t.Errorf("Expected statistics total 2, got %v", stats["total_appearances"]) } // Test clear masterList.Clear() if masterList.Size() != 0 { t.Errorf("Expected size 0 after clear, got %d", masterList.Size()) } } // TestMasterListConcurrency tests thread safety of the master list func TestMasterListConcurrency(t *testing.T) { masterList := NewMasterList() // Create test database db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared") if err != nil { t.Fatalf("Failed to create test database: %v", err) } defer db.Close() const numWorkers = 10 const appsPerWorker = 100 var wg sync.WaitGroup // Concurrently add appearances wg.Add(numWorkers) for i := 0; i < numWorkers; i++ { go func(workerID int) { defer wg.Done() for j := 0; j < appsPerWorker; j++ { app := NewWithData( int32(workerID*appsPerWorker+j+1), "Concurrent Test", int16(1096+(workerID%3)*100), db, ) masterList.AddAppearance(app) } }(i) } // Concurrently read appearances wg.Add(numWorkers) for i := 0; i < numWorkers; i++ { go func() { defer wg.Done() for j := 0; j < appsPerWorker; j++ { // Random reads _ = masterList.GetAppearance(int32(j + 1)) _ = masterList.FindAppearancesByMinClient(1096) _ = masterList.GetCompatibleAppearances(1200) _ = masterList.Size() } }() } wg.Wait() // Verify final state expectedSize := numWorkers * appsPerWorker if masterList.Size() != expectedSize { t.Errorf("Expected size %d, got %d", expectedSize, masterList.Size()) } clientVersions := masterList.GetClientVersions() if len(clientVersions) != 3 { t.Errorf("Expected 3 client versions, got %d", len(clientVersions)) } }