package blind import ( "context" "database/sql" "os" "path/filepath" "testing" _ "github.com/tursodatabase/go-libsql" ) // setupTestDB creates a temporary test database with the required schema. func setupTestDB(t *testing.T) *sql.DB { t.Helper() tmpDir := t.TempDir() dbPath := filepath.Join(tmpDir, "test.db") db, err := sql.Open("libsql", "file:"+dbPath) if err != nil { t.Fatalf("open test db: %v", err) } t.Cleanup(func() { db.Close() }) // Create required tables stmts := []string{ `CREATE TABLE chip_sets (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, is_builtin INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0)`, `CREATE TABLE chip_denominations (id INTEGER PRIMARY KEY AUTOINCREMENT, chip_set_id INTEGER NOT NULL, value INTEGER NOT NULL, color_hex TEXT NOT NULL DEFAULT '#FFF', label TEXT NOT NULL DEFAULT '', sort_order INTEGER NOT NULL DEFAULT 0)`, `CREATE TABLE blind_structures (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, is_builtin INTEGER NOT NULL DEFAULT 0, game_type_default TEXT NOT NULL DEFAULT 'nlhe', notes TEXT NOT NULL DEFAULT '', created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0)`, `CREATE TABLE blind_levels (id INTEGER PRIMARY KEY AUTOINCREMENT, structure_id INTEGER NOT NULL, position INTEGER NOT NULL, level_type TEXT NOT NULL DEFAULT 'round', game_type TEXT NOT NULL DEFAULT 'nlhe', small_blind INTEGER NOT NULL DEFAULT 0, big_blind INTEGER NOT NULL DEFAULT 0, ante INTEGER NOT NULL DEFAULT 0, bb_ante INTEGER NOT NULL DEFAULT 0, duration_seconds INTEGER NOT NULL DEFAULT 900, chip_up_denomination_value INTEGER, notes TEXT NOT NULL DEFAULT '')`, `CREATE TABLE tournaments (id TEXT PRIMARY KEY, name TEXT, template_id INTEGER, chip_set_id INTEGER, blind_structure_id INTEGER, payout_structure_id INTEGER, buyin_config_id INTEGER, points_formula_id INTEGER, status TEXT DEFAULT 'created', min_players INTEGER DEFAULT 2, max_players INTEGER, early_signup_bonus_chips INTEGER DEFAULT 0, early_signup_cutoff TEXT, punctuality_bonus_chips INTEGER DEFAULT 0, is_pko INTEGER DEFAULT 0, current_level INTEGER DEFAULT 0, clock_state TEXT DEFAULT 'stopped', clock_remaining_ns INTEGER DEFAULT 0, total_elapsed_ns INTEGER DEFAULT 0, hand_for_hand INTEGER DEFAULT 0, started_at INTEGER, ended_at INTEGER, created_at INTEGER DEFAULT 0, updated_at INTEGER DEFAULT 0)`, } for _, stmt := range stmts { if _, err := db.Exec(stmt); err != nil { t.Fatalf("create table: %v", err) } } // Insert standard chip set if _, err := db.Exec(`INSERT INTO chip_sets (id, name) VALUES (1, 'Standard')`); err != nil { t.Fatalf("insert chip set: %v", err) } denominations := []struct { value int64 colorHex string }{ {25, "#FFFFFF"}, {100, "#FF0000"}, {500, "#00AA00"}, {1000, "#000000"}, {5000, "#0000FF"}, } for _, d := range denominations { if _, err := db.Exec(`INSERT INTO chip_denominations (chip_set_id, value, color_hex) VALUES (1, ?, ?)`, d.value, d.colorHex); err != nil { t.Fatalf("insert denomination: %v", err) } } return db } func TestWizardGenerate_StandardTournament(t *testing.T) { if os.Getenv("CGO_ENABLED") == "0" { t.Skip("requires CGO for libsql") } db := setupTestDB(t) ws := NewWizardService(db) ctx := context.Background() levels, err := ws.Generate(ctx, WizardInput{ PlayerCount: 20, StartingChips: 15000, TargetDurationMinutes: 240, ChipSetID: 1, }) if err != nil { t.Fatalf("generate: %v", err) } if len(levels) == 0 { t.Fatal("expected at least 1 level") } // Check all levels have valid types hasRound := false hasBreak := false for _, l := range levels { if l.LevelType == "round" { hasRound = true } if l.LevelType == "break" { hasBreak = true } if l.LevelType != "round" && l.LevelType != "break" { t.Errorf("invalid level type: %s", l.LevelType) } } if !hasRound { t.Error("expected at least one round level") } if !hasBreak { t.Error("expected at least one break") } // Check positions are contiguous for i, l := range levels { if l.Position != i { t.Errorf("level %d: expected position %d, got %d", i, i, l.Position) } } // Check blinds are increasing for round levels prevBB := int64(0) for _, l := range levels { if l.LevelType == "round" { if l.BigBlind <= 0 { t.Errorf("level %d: big blind must be positive, got %d", l.Position, l.BigBlind) } if l.BigBlind < prevBB { t.Errorf("level %d: big blind %d not increasing from previous %d", l.Position, l.BigBlind, prevBB) } if l.SmallBlind >= l.BigBlind { t.Errorf("level %d: small blind %d >= big blind %d", l.Position, l.SmallBlind, l.BigBlind) } prevBB = l.BigBlind } } } func TestWizardGenerate_VariousPlayerCounts(t *testing.T) { if os.Getenv("CGO_ENABLED") == "0" { t.Skip("requires CGO for libsql") } db := setupTestDB(t) ws := NewWizardService(db) ctx := context.Background() for _, pc := range []int{10, 20, 40, 80} { t.Run(stringFromInt(pc)+"players", func(t *testing.T) { levels, err := ws.Generate(ctx, WizardInput{ PlayerCount: pc, StartingChips: 15000, TargetDurationMinutes: 240, ChipSetID: 1, }) if err != nil { t.Fatalf("generate for %d players: %v", pc, err) } if len(levels) < 5 { t.Errorf("expected at least 5 levels for %d players, got %d", pc, len(levels)) } // Verify structure is increasing prevBB := int64(0) for _, l := range levels { if l.LevelType == "round" && l.BigBlind < prevBB { t.Errorf("level %d: big blind %d < previous %d for %d players", l.Position, l.BigBlind, prevBB, pc) } if l.LevelType == "round" { prevBB = l.BigBlind } } }) } } func TestWizardGenerate_BlindsAlignWithDenominations(t *testing.T) { if os.Getenv("CGO_ENABLED") == "0" { t.Skip("requires CGO for libsql") } db := setupTestDB(t) ws := NewWizardService(db) ctx := context.Background() levels, err := ws.Generate(ctx, WizardInput{ PlayerCount: 20, StartingChips: 15000, TargetDurationMinutes: 240, ChipSetID: 1, }) if err != nil { t.Fatalf("generate: %v", err) } denoms := []int64{25, 100, 500, 1000, 5000} for _, l := range levels { if l.LevelType != "round" { continue } if !isDenomAligned(l.SmallBlind, denoms) { t.Errorf("level %d: SB %d not aligned with denominations", l.Position, l.SmallBlind) } if !isDenomAligned(l.BigBlind, denoms) { t.Errorf("level %d: BB %d not aligned with denominations", l.Position, l.BigBlind) } if l.Ante > 0 && !isDenomAligned(l.Ante, denoms) { t.Errorf("level %d: ante %d not aligned with denominations", l.Position, l.Ante) } } } func TestWizardGenerate_ShortTournament(t *testing.T) { if os.Getenv("CGO_ENABLED") == "0" { t.Skip("requires CGO for libsql") } db := setupTestDB(t) ws := NewWizardService(db) ctx := context.Background() levels, err := ws.Generate(ctx, WizardInput{ PlayerCount: 10, StartingChips: 10000, TargetDurationMinutes: 60, ChipSetID: 1, }) if err != nil { t.Fatalf("generate: %v", err) } if len(levels) < 3 { t.Errorf("expected at least 3 levels for 1hr tournament, got %d", len(levels)) } } func TestWizardGenerate_LongTournament(t *testing.T) { if os.Getenv("CGO_ENABLED") == "0" { t.Skip("requires CGO for libsql") } db := setupTestDB(t) ws := NewWizardService(db) ctx := context.Background() levels, err := ws.Generate(ctx, WizardInput{ PlayerCount: 20, StartingChips: 50000, TargetDurationMinutes: 480, ChipSetID: 1, }) if err != nil { t.Fatalf("generate: %v", err) } if len(levels) < 5 { t.Errorf("expected at least 5 levels for 8hr tournament, got %d", len(levels)) } // Check breaks are present breakCount := 0 for _, l := range levels { if l.LevelType == "break" { breakCount++ } } if breakCount == 0 { t.Error("expected at least one break in 8hr tournament") } } func TestWizardGenerate_InvalidInputs(t *testing.T) { if os.Getenv("CGO_ENABLED") == "0" { t.Skip("requires CGO for libsql") } db := setupTestDB(t) ws := NewWizardService(db) ctx := context.Background() tests := []struct { name string input WizardInput }{ {"too few players", WizardInput{PlayerCount: 1, StartingChips: 10000, TargetDurationMinutes: 120, ChipSetID: 1}}, {"zero chips", WizardInput{PlayerCount: 10, StartingChips: 0, TargetDurationMinutes: 120, ChipSetID: 1}}, {"too short", WizardInput{PlayerCount: 10, StartingChips: 10000, TargetDurationMinutes: 10, ChipSetID: 1}}, {"missing chip set", WizardInput{PlayerCount: 10, StartingChips: 10000, TargetDurationMinutes: 120, ChipSetID: 999}}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { _, err := ws.Generate(ctx, tc.input) if err == nil { t.Error("expected error for invalid input") } }) } } // isDenomAligned checks if a value is a multiple of any denomination. func isDenomAligned(value int64, denoms []int64) bool { for _, d := range denoms { if value%d == 0 { return true } } return false } // stringFromInt converts int to string for test names. func stringFromInt(n int) string { s := "" if n == 0 { return "0" } for n > 0 { s = string(rune('0'+n%10)) + s n /= 10 } return s }