package template import ( "context" "database/sql" "os" "path/filepath" "testing" _ "github.com/tursodatabase/go-libsql" ) // setupTestDB creates a temporary test database with required schema and seed data. 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() }) 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 payout_structures (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 payout_brackets (id INTEGER PRIMARY KEY AUTOINCREMENT, structure_id INTEGER NOT NULL, min_entries INTEGER NOT NULL, max_entries INTEGER NOT NULL)`, `CREATE TABLE payout_tiers (id INTEGER PRIMARY KEY AUTOINCREMENT, bracket_id INTEGER NOT NULL, position INTEGER NOT NULL, percentage_basis_points INTEGER NOT NULL)`, `CREATE TABLE buyin_configs (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, buyin_amount INTEGER DEFAULT 0, starting_chips INTEGER DEFAULT 0, rake_total INTEGER DEFAULT 0, bounty_amount INTEGER DEFAULT 0, bounty_chip INTEGER DEFAULT 0, rebuy_allowed INTEGER DEFAULT 0, rebuy_cost INTEGER DEFAULT 0, rebuy_chips INTEGER DEFAULT 0, rebuy_rake INTEGER DEFAULT 0, rebuy_limit INTEGER DEFAULT 0, rebuy_level_cutoff INTEGER, rebuy_time_cutoff_seconds INTEGER, rebuy_chip_threshold INTEGER, addon_allowed INTEGER DEFAULT 0, addon_cost INTEGER DEFAULT 0, addon_chips INTEGER DEFAULT 0, addon_rake INTEGER DEFAULT 0, addon_level_start INTEGER, addon_level_end INTEGER, reentry_allowed INTEGER DEFAULT 0, reentry_limit INTEGER DEFAULT 0, late_reg_level_cutoff INTEGER, late_reg_time_cutoff_seconds INTEGER, created_at INTEGER DEFAULT 0, updated_at INTEGER DEFAULT 0)`, `CREATE TABLE rake_splits (id INTEGER PRIMARY KEY AUTOINCREMENT, buyin_config_id INTEGER NOT NULL, category TEXT NOT NULL, amount INTEGER NOT NULL DEFAULT 0)`, `CREATE TABLE points_formulas (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, expression TEXT DEFAULT '', variables TEXT DEFAULT '{}', is_builtin INTEGER DEFAULT 0, created_at INTEGER DEFAULT 0, updated_at INTEGER DEFAULT 0)`, `CREATE TABLE tournament_templates (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT DEFAULT '', chip_set_id INTEGER NOT NULL, blind_structure_id INTEGER NOT NULL, payout_structure_id INTEGER NOT NULL, buyin_config_id INTEGER NOT NULL, points_formula_id INTEGER, 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, is_builtin INTEGER DEFAULT 0, created_at INTEGER DEFAULT 0, updated_at INTEGER DEFAULT 0)`, `CREATE TABLE tournaments (id TEXT PRIMARY KEY, name TEXT, template_id INTEGER, chip_set_id INTEGER NOT NULL, blind_structure_id INTEGER NOT NULL, payout_structure_id INTEGER NOT NULL, buyin_config_id INTEGER NOT NULL, 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\nSQL: %s", err, stmt) } } // Seed building blocks db.Exec(`INSERT INTO chip_sets (id, name) VALUES (1, 'Standard')`) db.Exec(`INSERT INTO chip_denominations (chip_set_id, value, color_hex) VALUES (1, 25, '#FFF'), (1, 100, '#F00')`) db.Exec(`INSERT INTO blind_structures (id, name) VALUES (1, 'Test Structure')`) db.Exec(`INSERT INTO blind_levels (structure_id, position, level_type, small_blind, big_blind, duration_seconds) VALUES (1, 0, 'round', 25, 50, 900)`) db.Exec(`INSERT INTO payout_structures (id, name) VALUES (1, 'Test Payout')`) db.Exec(`INSERT INTO payout_brackets (id, structure_id, min_entries, max_entries) VALUES (1, 1, 2, 20)`) db.Exec(`INSERT INTO payout_tiers (bracket_id, position, percentage_basis_points) VALUES (1, 1, 6000), (1, 2, 4000)`) db.Exec(`INSERT INTO buyin_configs (id, name, buyin_amount, starting_chips, rake_total) VALUES (1, 'Test Buy-in', 20000, 10000, 2000)`) return db } func TestCreateTemplate_ValidReferences(t *testing.T) { if os.Getenv("CGO_ENABLED") == "0" { t.Skip("requires CGO for libsql") } db := setupTestDB(t) svc := NewTournamentTemplateService(db) ctx := context.Background() tmpl, err := svc.CreateTemplate(ctx, &TournamentTemplate{ Name: "My Tournament", Description: "Test template", ChipSetID: 1, BlindStructureID: 1, PayoutStructureID: 1, BuyinConfigID: 1, MinPlayers: 6, }) if err != nil { t.Fatalf("create template: %v", err) } if tmpl.ID == 0 { t.Error("expected non-zero ID") } if tmpl.Name != "My Tournament" { t.Errorf("expected name 'My Tournament', got %q", tmpl.Name) } if tmpl.ChipSetName != "Standard" { t.Errorf("expected chip set name 'Standard', got %q", tmpl.ChipSetName) } if tmpl.BlindStructureName != "Test Structure" { t.Errorf("expected blind structure name 'Test Structure', got %q", tmpl.BlindStructureName) } } func TestCreateTemplate_InvalidReference(t *testing.T) { if os.Getenv("CGO_ENABLED") == "0" { t.Skip("requires CGO for libsql") } db := setupTestDB(t) svc := NewTournamentTemplateService(db) ctx := context.Background() _, err := svc.CreateTemplate(ctx, &TournamentTemplate{ Name: "Bad Template", ChipSetID: 999, // doesn't exist BlindStructureID: 1, PayoutStructureID: 1, BuyinConfigID: 1, }) if err == nil { t.Fatal("expected error for invalid chip set reference") } } func TestGetTemplateExpanded(t *testing.T) { if os.Getenv("CGO_ENABLED") == "0" { t.Skip("requires CGO for libsql") } db := setupTestDB(t) svc := NewTournamentTemplateService(db) ctx := context.Background() tmpl, err := svc.CreateTemplate(ctx, &TournamentTemplate{ Name: "Expandable", ChipSetID: 1, BlindStructureID: 1, PayoutStructureID: 1, BuyinConfigID: 1, MinPlayers: 6, }) if err != nil { t.Fatalf("create template: %v", err) } expanded, err := svc.GetTemplateExpanded(ctx, tmpl.ID) if err != nil { t.Fatalf("get expanded: %v", err) } if expanded.ChipSet == nil { t.Fatal("expected chip set to be populated") } if expanded.ChipSet.Name != "Standard" { t.Errorf("expected chip set name 'Standard', got %q", expanded.ChipSet.Name) } if len(expanded.ChipSet.Denominations) != 2 { t.Errorf("expected 2 denominations, got %d", len(expanded.ChipSet.Denominations)) } if expanded.BlindStructure == nil { t.Fatal("expected blind structure to be populated") } if len(expanded.BlindStructure.Levels) != 1 { t.Errorf("expected 1 level, got %d", len(expanded.BlindStructure.Levels)) } if expanded.PayoutStructure == nil { t.Fatal("expected payout structure to be populated") } if len(expanded.PayoutStructure.Brackets) != 1 { t.Errorf("expected 1 bracket, got %d", len(expanded.PayoutStructure.Brackets)) } if expanded.BuyinConfig == nil { t.Fatal("expected buyin config to be populated") } if expanded.BuyinConfig.BuyinAmount != 20000 { t.Errorf("expected buyin amount 20000, got %d", expanded.BuyinConfig.BuyinAmount) } } func TestSaveAsTemplate(t *testing.T) { if os.Getenv("CGO_ENABLED") == "0" { t.Skip("requires CGO for libsql") } db := setupTestDB(t) svc := NewTournamentTemplateService(db) ctx := context.Background() // Create a tournament db.Exec(`INSERT INTO tournaments (id, name, chip_set_id, blind_structure_id, payout_structure_id, buyin_config_id, min_players, is_pko) VALUES ('t1', 'Live Tournament', 1, 1, 1, 1, 8, 0)`) tmpl, err := svc.SaveAsTemplate(ctx, "t1", "Saved From Live") if err != nil { t.Fatalf("save as template: %v", err) } if tmpl.Name != "Saved From Live" { t.Errorf("expected name 'Saved From Live', got %q", tmpl.Name) } if tmpl.ChipSetID != 1 { t.Errorf("expected chip_set_id 1, got %d", tmpl.ChipSetID) } if tmpl.MinPlayers != 8 { t.Errorf("expected min_players 8, got %d", tmpl.MinPlayers) } } func TestDuplicateTemplate(t *testing.T) { if os.Getenv("CGO_ENABLED") == "0" { t.Skip("requires CGO for libsql") } db := setupTestDB(t) svc := NewTournamentTemplateService(db) ctx := context.Background() original, err := svc.CreateTemplate(ctx, &TournamentTemplate{ Name: "Original", ChipSetID: 1, BlindStructureID: 1, PayoutStructureID: 1, BuyinConfigID: 1, MinPlayers: 6, }) if err != nil { t.Fatalf("create template: %v", err) } dup, err := svc.DuplicateTemplate(ctx, original.ID, "Duplicate") if err != nil { t.Fatalf("duplicate: %v", err) } if dup.ID == original.ID { t.Error("duplicate should have different ID") } if dup.Name != "Duplicate" { t.Errorf("expected name 'Duplicate', got %q", dup.Name) } if dup.ChipSetID != original.ChipSetID { t.Error("duplicate should have same chip set") } } func TestDeleteBuiltinTemplate(t *testing.T) { if os.Getenv("CGO_ENABLED") == "0" { t.Skip("requires CGO for libsql") } db := setupTestDB(t) svc := NewTournamentTemplateService(db) ctx := context.Background() // Insert a builtin template directly db.Exec(`INSERT INTO tournament_templates (id, name, chip_set_id, blind_structure_id, payout_structure_id, buyin_config_id, is_builtin) VALUES (99, 'Built-in', 1, 1, 1, 1, 1)`) err := svc.DeleteTemplate(ctx, 99) if err == nil { t.Fatal("expected error deleting built-in template") } } func TestListTemplates(t *testing.T) { if os.Getenv("CGO_ENABLED") == "0" { t.Skip("requires CGO for libsql") } db := setupTestDB(t) svc := NewTournamentTemplateService(db) ctx := context.Background() // Create two templates svc.CreateTemplate(ctx, &TournamentTemplate{Name: "A", ChipSetID: 1, BlindStructureID: 1, PayoutStructureID: 1, BuyinConfigID: 1}) svc.CreateTemplate(ctx, &TournamentTemplate{Name: "B", ChipSetID: 1, BlindStructureID: 1, PayoutStructureID: 1, BuyinConfigID: 1}) list, err := svc.ListTemplates(ctx) if err != nil { t.Fatalf("list templates: %v", err) } if len(list) < 2 { t.Errorf("expected at least 2 templates, got %d", len(list)) } }