package heroic_ops import ( "context" "fmt" "path/filepath" "testing" "time" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) // createTestPool creates a temporary test database pool func createTestPool(t *testing.T) *sqlitex.Pool { // Create temporary directory for test database tempDir := t.TempDir() dbPath := filepath.Join(tempDir, "test_heroic_ops.db") // Create and initialize database pool pool, err := sqlitex.NewPool(dbPath, sqlitex.PoolOptions{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, }) if err != nil { t.Fatalf("Failed to create test database pool: %v", err) } // Create heroic ops tables for testing dhom := NewDatabaseHeroicOPManager(pool) err = dhom.EnsureHOTables(context.Background()) if err != nil { t.Fatalf("Failed to create heroic ops tables: %v", err) } return pool } // execSQL is a helper to execute SQL with parameters func execSQL(t *testing.T, pool *sqlitex.Pool, query string, args ...interface{}) { conn, err := pool.Take(context.Background()) if err != nil { t.Fatalf("Failed to get connection: %v", err) } defer pool.Put(conn) err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: args, }) if err != nil { t.Fatalf("Failed to execute SQL: %v", err) } } // TestDatabaseHeroicOPManager_LoadStarters tests loading starters from database func TestDatabaseHeroicOPManager_LoadStarters(t *testing.T) { pool := createTestPool(t) defer pool.Close() dhom := NewDatabaseHeroicOPManager(pool) ctx := context.Background() // Insert test starter data execSQL(t, pool, `INSERT INTO heroic_ops (id, ho_type, starter_class, starter_icon, starter_link_id, chain_order, shift_icon, spell_id, chance, ability1, ability2, ability3, ability4, ability5, ability6, name, description) VALUES (?, ?, ?, ?, 0, 0, 0, 0, 0.0, ?, ?, ?, ?, ?, ?, ?, ?)`, 1, HOTypeStarter, 5, 100, 10, 20, 30, 40, 50, 60, "Test Starter", "A test starter") // Test loading starters starters, err := dhom.LoadStarters(ctx) if err != nil { t.Fatalf("Failed to load starters: %v", err) } if len(starters) != 1 { t.Errorf("Expected 1 starter, got %d", len(starters)) } if len(starters) > 0 { starter := starters[0] if starter.ID != 1 { t.Errorf("Expected starter ID 1, got %d", starter.ID) } if starter.StarterClass != 5 { t.Errorf("Expected starter class 5, got %d", starter.StarterClass) } if starter.Name != "Test Starter" { t.Errorf("Expected name 'Test Starter', got '%s'", starter.Name) } if starter.Ability1 != 10 { t.Errorf("Expected ability1 10, got %d", starter.Ability1) } } } // TestDatabaseHeroicOPManager_LoadStarter tests loading a specific starter func TestDatabaseHeroicOPManager_LoadStarter(t *testing.T) { pool := createTestPool(t) defer pool.Close() dhom := NewDatabaseHeroicOPManager(pool) ctx := context.Background() // Insert test starter data execSQL(t, pool, `INSERT INTO heroic_ops (id, ho_type, starter_class, starter_icon, starter_link_id, chain_order, shift_icon, spell_id, chance, ability1, ability2, ability3, ability4, ability5, ability6, name, description) VALUES (?, ?, ?, ?, 0, 0, 0, 0, 0.0, ?, ?, ?, ?, ?, ?, ?, ?)`, 2, HOTypeStarter, 10, 200, 11, 21, 31, 41, 51, 61, "Specific Starter", "A specific test starter") // Test loading specific starter starter, err := dhom.LoadStarter(ctx, 2) if err != nil { t.Fatalf("Failed to load starter: %v", err) } if starter.ID != 2 { t.Errorf("Expected starter ID 2, got %d", starter.ID) } if starter.StarterClass != 10 { t.Errorf("Expected starter class 10, got %d", starter.StarterClass) } if starter.Name != "Specific Starter" { t.Errorf("Expected name 'Specific Starter', got '%s'", starter.Name) } // Test loading non-existent starter _, err = dhom.LoadStarter(ctx, 999) if err == nil { t.Error("Expected error loading non-existent starter, got nil") } } // TestDatabaseHeroicOPManager_LoadWheels tests loading wheels from database func TestDatabaseHeroicOPManager_LoadWheels(t *testing.T) { pool := createTestPool(t) defer pool.Close() dhom := NewDatabaseHeroicOPManager(pool) ctx := context.Background() // Insert test wheel data execSQL(t, pool, `INSERT INTO heroic_ops (id, ho_type, starter_class, starter_icon, starter_link_id, chain_order, shift_icon, spell_id, chance, ability1, ability2, ability3, ability4, ability5, ability6, name, description) VALUES (?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 10, HOTypeWheel, 1, 1, 300, 1001, 0.75, 15, 25, 35, 45, 55, 65, "Test Wheel", "A test wheel") // Test loading wheels wheels, err := dhom.LoadWheels(ctx) if err != nil { t.Fatalf("Failed to load wheels: %v", err) } if len(wheels) != 1 { t.Errorf("Expected 1 wheel, got %d", len(wheels)) } if len(wheels) > 0 { wheel := wheels[0] if wheel.ID != 10 { t.Errorf("Expected wheel ID 10, got %d", wheel.ID) } if wheel.StarterLinkID != 1 { t.Errorf("Expected starter link ID 1, got %d", wheel.StarterLinkID) } if wheel.SpellID != 1001 { t.Errorf("Expected spell ID 1001, got %d", wheel.SpellID) } if wheel.Chance != 0.75 { t.Errorf("Expected chance 0.75, got %f", wheel.Chance) } } } // TestDatabaseHeroicOPManager_LoadWheelsForStarter tests loading wheels for specific starter func TestDatabaseHeroicOPManager_LoadWheelsForStarter(t *testing.T) { pool := createTestPool(t) defer pool.Close() dhom := NewDatabaseHeroicOPManager(pool) ctx := context.Background() starterID := int32(5) // Insert multiple wheels for the same starter execSQL(t, pool, `INSERT INTO heroic_ops (id, ho_type, starter_class, starter_icon, starter_link_id, chain_order, shift_icon, spell_id, chance, ability1, ability2, ability3, ability4, ability5, ability6, name, description) VALUES (?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 20, HOTypeWheel, starterID, 1, 400, 1002, 0.5, 16, 26, 36, 46, 56, 66, "Wheel 1", "First wheel") execSQL(t, pool, `INSERT INTO heroic_ops (id, ho_type, starter_class, starter_icon, starter_link_id, chain_order, shift_icon, spell_id, chance, ability1, ability2, ability3, ability4, ability5, ability6, name, description) VALUES (?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 21, HOTypeWheel, starterID, 2, 500, 1003, 0.8, 17, 27, 37, 47, 57, 67, "Wheel 2", "Second wheel") // Test loading wheels for specific starter wheels, err := dhom.LoadWheelsForStarter(ctx, starterID) if err != nil { t.Fatalf("Failed to load wheels for starter: %v", err) } if len(wheels) != 2 { t.Errorf("Expected 2 wheels, got %d", len(wheels)) } // Test loading wheels for non-existent starter wheels, err = dhom.LoadWheelsForStarter(ctx, 999) if err != nil { t.Fatalf("Failed to load wheels for non-existent starter: %v", err) } if len(wheels) != 0 { t.Errorf("Expected 0 wheels for non-existent starter, got %d", len(wheels)) } } // TestDatabaseHeroicOPManager_SaveStarter tests saving a heroic op starter func TestDatabaseHeroicOPManager_SaveStarter(t *testing.T) { pool := createTestPool(t) defer pool.Close() dhom := NewDatabaseHeroicOPManager(pool) ctx := context.Background() // Create a test starter starter := &HeroicOPStarter{ ID: 100, StartClass: 15, StarterIcon: 500, Abilities: [6]int16{70, 80, 90, 100, 110, 120}, Name: "Saved Starter", Description: "A saved test starter", } // Save the starter err := dhom.SaveStarter(ctx, starter) if err != nil { t.Fatalf("Failed to save starter: %v", err) } // Load and verify loaded, err := dhom.LoadStarter(ctx, 100) if err != nil { t.Fatalf("Failed to load saved starter: %v", err) } if loaded.StarterClass != 15 { t.Errorf("Expected starter class 15, got %d", loaded.StarterClass) } if loaded.Name != "Saved Starter" { t.Errorf("Expected name 'Saved Starter', got '%s'", loaded.Name) } if loaded.Ability1 != 70 { t.Errorf("Expected ability1 70, got %d", loaded.Ability1) } } // TestDatabaseHeroicOPManager_SaveWheel tests saving a heroic op wheel func TestDatabaseHeroicOPManager_SaveWheel(t *testing.T) { pool := createTestPool(t) defer pool.Close() dhom := NewDatabaseHeroicOPManager(pool) ctx := context.Background() // Create a test wheel wheel := &HeroicOPWheel{ ID: 200, StarterLinkID: 100, Order: 2, ShiftIcon: 600, SpellID: 2001, Chance: 0.9, Abilities: [6]int16{71, 81, 91, 101, 111, 121}, Name: "Saved Wheel", Description: "A saved test wheel", } // Save the wheel err := dhom.SaveWheel(ctx, wheel) if err != nil { t.Fatalf("Failed to save wheel: %v", err) } // Load and verify loaded, err := dhom.LoadWheel(ctx, 200) if err != nil { t.Fatalf("Failed to load saved wheel: %v", err) } if loaded.StarterLinkID != 100 { t.Errorf("Expected starter link ID 100, got %d", loaded.StarterLinkID) } if loaded.SpellID != 2001 { t.Errorf("Expected spell ID 2001, got %d", loaded.SpellID) } if loaded.Chance != 0.9 { t.Errorf("Expected chance 0.9, got %f", loaded.Chance) } } // TestDatabaseHeroicOPManager_DeleteStarter tests deleting a starter and its wheels func TestDatabaseHeroicOPManager_DeleteStarter(t *testing.T) { pool := createTestPool(t) defer pool.Close() dhom := NewDatabaseHeroicOPManager(pool) ctx := context.Background() starterID := int32(300) // Insert starter execSQL(t, pool, `INSERT INTO heroic_ops (id, ho_type, starter_class, starter_icon, starter_link_id, chain_order, shift_icon, spell_id, chance, ability1, ability2, ability3, ability4, ability5, ability6, name, description) VALUES (?, ?, ?, ?, 0, 0, 0, 0, 0.0, ?, ?, ?, ?, ?, ?, ?, ?)`, starterID, HOTypeStarter, 20, 700, 72, 82, 92, 102, 112, 122, "Delete Test Starter", "To be deleted") // Insert associated wheels execSQL(t, pool, `INSERT INTO heroic_ops (id, ho_type, starter_class, starter_icon, starter_link_id, chain_order, shift_icon, spell_id, chance, ability1, ability2, ability3, ability4, ability5, ability6, name, description) VALUES (?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 400, HOTypeWheel, starterID, 1, 800, 3001, 0.6, 73, 83, 93, 103, 113, 123, "Wheel to Delete", "Will be deleted") // Verify they exist _, err := dhom.LoadStarter(ctx, starterID) if err != nil { t.Fatalf("Starter should exist before deletion: %v", err) } wheels, err := dhom.LoadWheelsForStarter(ctx, starterID) if err != nil { t.Fatalf("Failed to load wheels before deletion: %v", err) } if len(wheels) != 1 { t.Errorf("Expected 1 wheel before deletion, got %d", len(wheels)) } // Delete the starter err = dhom.DeleteStarter(ctx, starterID) if err != nil { t.Fatalf("Failed to delete starter: %v", err) } // Verify they're gone _, err = dhom.LoadStarter(ctx, starterID) if err == nil { t.Error("Expected error loading deleted starter, got nil") } wheels, err = dhom.LoadWheelsForStarter(ctx, starterID) if err != nil { t.Fatalf("Failed to load wheels after deletion: %v", err) } if len(wheels) != 0 { t.Errorf("Expected 0 wheels after deletion, got %d", len(wheels)) } } // TestDatabaseHeroicOPManager_HeroicOPInstance tests HO instance operations func TestDatabaseHeroicOPManager_HeroicOPInstance(t *testing.T) { pool := createTestPool(t) defer pool.Close() dhom := NewDatabaseHeroicOPManager(pool) ctx := context.Background() // Create a test HO instance ho := &HeroicOP{ ID: 1000, EncounterID: 500, StarterID: 10, WheelID: 20, State: int8(HOStateWheelPhase), StartTime: time.Now(), WheelStartTime: time.Now().Add(5 * time.Second), TimeRemaining: 8000, TotalTime: 10000, Complete: 0, Countered: [6]int8{1, 1, 0, 0, 0, 0}, ShiftUsed: 0, StarterProgress: 3, CompletedBy: 0, SpellName: "Test Spell", SpellDescription: "A test completion spell", } // Save the HO instance err := dhom.SaveHOInstance(ctx, ho) if err != nil { t.Fatalf("Failed to save HO instance: %v", err) } // Load and verify loaded, err := dhom.LoadHOInstance(ctx, 1000) if err != nil { t.Fatalf("Failed to load HO instance: %v", err) } if loaded.EncounterID != 500 { t.Errorf("Expected encounter ID 500, got %d", loaded.EncounterID) } if loaded.State != int8(HOStateWheelPhase) { t.Errorf("Expected state %d, got %d", HOStateWheelPhase, loaded.State) } if loaded.TimeRemaining != 8000 { t.Errorf("Expected time remaining 8000, got %d", loaded.TimeRemaining) } if loaded.SpellName != "Test Spell" { t.Errorf("Expected spell name 'Test Spell', got '%s'", loaded.SpellName) } // Delete the instance err = dhom.DeleteHOInstance(ctx, 1000) if err != nil { t.Fatalf("Failed to delete HO instance: %v", err) } // Verify it's gone _, err = dhom.LoadHOInstance(ctx, 1000) if err == nil { t.Error("Expected error loading deleted HO instance, got nil") } } // TestDatabaseHeroicOPManager_HeroicOPEvents tests HO event operations func TestDatabaseHeroicOPManager_HeroicOPEvents(t *testing.T) { pool := createTestPool(t) defer pool.Close() dhom := NewDatabaseHeroicOPManager(pool) ctx := context.Background() instanceID := int64(2000) // Create test events events := []*HeroicOPEvent{ { ID: 1, InstanceID: instanceID, EventType: EventHOStarted, CharacterID: 100, AbilityIcon: 50, Timestamp: time.Now(), Data: "started", }, { ID: 2, InstanceID: instanceID, EventType: EventHOAbilityUsed, CharacterID: 101, AbilityIcon: 51, Timestamp: time.Now().Add(1 * time.Second), Data: "ability used", }, } // Save events for _, event := range events { err := dhom.SaveHOEvent(ctx, event) if err != nil { t.Fatalf("Failed to save HO event: %v", err) } } // Load and verify loadedEvents, err := dhom.LoadHOEvents(ctx, instanceID) if err != nil { t.Fatalf("Failed to load HO events: %v", err) } if len(loadedEvents) != 2 { t.Errorf("Expected 2 events, got %d", len(loadedEvents)) } // Events should be ordered by timestamp if len(loadedEvents) >= 2 { if loadedEvents[0].EventType != EventHOStarted { t.Errorf("Expected first event type %d, got %d", EventHOStarted, loadedEvents[0].EventType) } if loadedEvents[1].EventType != EventHOAbilityUsed { t.Errorf("Expected second event type %d, got %d", EventHOAbilityUsed, loadedEvents[1].EventType) } } } // TestDatabaseHeroicOPManager_Statistics tests HO statistics retrieval func TestDatabaseHeroicOPManager_Statistics(t *testing.T) { pool := createTestPool(t) defer pool.Close() dhom := NewDatabaseHeroicOPManager(pool) ctx := context.Background() characterID := int32(1001) // Insert test events for statistics execSQL(t, pool, `INSERT INTO heroic_op_events (id, instance_id, event_type, character_id, ability_icon, timestamp, data) VALUES (?, ?, ?, ?, ?, ?, ?)`, 1, 100, EventHOStarted, characterID, 0, time.Now().Unix(), "") execSQL(t, pool, `INSERT INTO heroic_op_events (id, instance_id, event_type, character_id, ability_icon, timestamp, data) VALUES (?, ?, ?, ?, ?, ?, ?)`, 2, 100, EventHOCompleted, characterID, 0, time.Now().Add(10*time.Second).Unix(), "") execSQL(t, pool, `INSERT INTO heroic_op_events (id, instance_id, event_type, character_id, ability_icon, timestamp, data) VALUES (?, ?, ?, ?, ?, ?, ?)`, 3, 101, EventHOStarted, characterID, 0, time.Now().Add(20*time.Second).Unix(), "") // Get statistics stats, err := dhom.GetHOStatistics(ctx, characterID) if err != nil { t.Fatalf("Failed to get HO statistics: %v", err) } if stats.TotalHOsStarted != 2 { t.Errorf("Expected 2 HOs started, got %d", stats.TotalHOsStarted) } if stats.TotalHOsCompleted != 1 { t.Errorf("Expected 1 HO completed, got %d", stats.TotalHOsCompleted) } if stats.SuccessRate != 50.0 { t.Errorf("Expected success rate 50.0, got %f", stats.SuccessRate) } } // TestDatabaseHeroicOPManager_NextIDs tests ID generation func TestDatabaseHeroicOPManager_NextIDs(t *testing.T) { pool := createTestPool(t) defer pool.Close() dhom := NewDatabaseHeroicOPManager(pool) ctx := context.Background() // Test next starter ID (should be 1 for empty database) starterID, err := dhom.GetNextStarterID(ctx) if err != nil { t.Fatalf("Failed to get next starter ID: %v", err) } if starterID != 1 { t.Errorf("Expected next starter ID 1, got %d", starterID) } // Insert a starter and test again execSQL(t, pool, `INSERT INTO heroic_ops (id, ho_type, starter_class, starter_icon, starter_link_id, chain_order, shift_icon, spell_id, chance, ability1, ability2, ability3, ability4, ability5, ability6, name, description) VALUES (?, ?, ?, ?, 0, 0, 0, 0, 0.0, ?, ?, ?, ?, ?, ?, ?, ?)`, 5, HOTypeStarter, 1, 100, 1, 2, 3, 4, 5, 6, "Test", "Test") starterID, err = dhom.GetNextStarterID(ctx) if err != nil { t.Fatalf("Failed to get next starter ID after insert: %v", err) } if starterID != 6 { t.Errorf("Expected next starter ID 6, got %d", starterID) } // Test next wheel ID wheelID, err := dhom.GetNextWheelID(ctx) if err != nil { t.Fatalf("Failed to get next wheel ID: %v", err) } if wheelID != 1 { t.Errorf("Expected next wheel ID 1, got %d", wheelID) } // Test next instance ID instanceID, err := dhom.GetNextInstanceID(ctx) if err != nil { t.Fatalf("Failed to get next instance ID: %v", err) } if instanceID != 1 { t.Errorf("Expected next instance ID 1, got %d", instanceID) } } // BenchmarkDatabaseHeroicOPManager_LoadStarters benchmarks loading starters func BenchmarkDatabaseHeroicOPManager_LoadStarters(b *testing.B) { pool := createTestPool(&testing.T{}) defer pool.Close() dhom := NewDatabaseHeroicOPManager(pool) ctx := context.Background() // Insert test data for i := 1; i <= 50; i++ { execSQL(&testing.T{}, pool, `INSERT INTO heroic_ops (id, ho_type, starter_class, starter_icon, starter_link_id, chain_order, shift_icon, spell_id, chance, ability1, ability2, ability3, ability4, ability5, ability6, name, description) VALUES (?, ?, ?, ?, 0, 0, 0, 0, 0.0, ?, ?, ?, ?, ?, ?, ?, ?)`, i, HOTypeStarter, i%10+1, i*10, i*1, i*2, i*3, i*4, i*5, i*6, fmt.Sprintf("Starter %d", i), fmt.Sprintf("Description %d", i)) } b.ResetTimer() for i := 0; i < b.N; i++ { starters, err := dhom.LoadStarters(ctx) if err != nil { b.Fatalf("Failed to load starters: %v", err) } if len(starters) != 50 { b.Errorf("Expected 50 starters, got %d", len(starters)) } } } // BenchmarkDatabaseHeroicOPManager_LoadWheels benchmarks loading wheels func BenchmarkDatabaseHeroicOPManager_LoadWheels(b *testing.B) { pool := createTestPool(&testing.T{}) defer pool.Close() dhom := NewDatabaseHeroicOPManager(pool) ctx := context.Background() // Insert test wheel data for i := 1; i <= 100; i++ { execSQL(&testing.T{}, pool, `INSERT INTO heroic_ops (id, ho_type, starter_class, starter_icon, starter_link_id, chain_order, shift_icon, spell_id, chance, ability1, ability2, ability3, ability4, ability5, ability6, name, description) VALUES (?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, i, HOTypeWheel, i%10+1, i%3+1, i*10, i*100+1000, float32(i%100)/100.0, i*1, i*2, i*3, i*4, i*5, i*6, fmt.Sprintf("Wheel %d", i), fmt.Sprintf("Wheel Description %d", i)) } b.ResetTimer() for i := 0; i < b.N; i++ { wheels, err := dhom.LoadWheels(ctx) if err != nil { b.Fatalf("Failed to load wheels: %v", err) } if len(wheels) != 100 { b.Errorf("Expected 100 wheels, got %d", len(wheels)) } } } // BenchmarkDatabaseHeroicOPManager_SaveStarter benchmarks saving starters func BenchmarkDatabaseHeroicOPManager_SaveStarter(b *testing.B) { pool := createTestPool(&testing.T{}) defer pool.Close() dhom := NewDatabaseHeroicOPManager(pool) ctx := context.Background() starter := &HeroicOPStarter{ ID: 1000, StartClass: 5, StarterIcon: 100, Abilities: [6]int16{10, 20, 30, 40, 50, 60}, Name: "Benchmark Starter", Description: "A benchmark test starter", } b.ResetTimer() for i := 0; i < b.N; i++ { err := dhom.SaveStarter(ctx, starter) if err != nil { b.Fatalf("Failed to save starter: %v", err) } } } // BenchmarkDatabaseHeroicOPManager_SaveHOInstance benchmarks saving HO instances func BenchmarkDatabaseHeroicOPManager_SaveHOInstance(b *testing.B) { pool := createTestPool(&testing.T{}) defer pool.Close() dhom := NewDatabaseHeroicOPManager(pool) ctx := context.Background() ho := &HeroicOP{ ID: 5000, EncounterID: 1000, StarterID: 50, WheelID: 100, State: int8(HOStateWheelPhase), StartTime: time.Now(), WheelStartTime: time.Now(), TimeRemaining: 10000, TotalTime: 10000, Complete: 0, Countered: [6]int8{0, 0, 0, 0, 0, 0}, ShiftUsed: 0, StarterProgress: 0, CompletedBy: 0, SpellName: "Benchmark Spell", SpellDescription: "A benchmark spell", } b.ResetTimer() for i := 0; i < b.N; i++ { err := dhom.SaveHOInstance(ctx, ho) if err != nil { b.Fatalf("Failed to save HO instance: %v", err) } } }