package tournament import ( "context" "database/sql" "os" "path/filepath" "testing" _ "github.com/tursodatabase/go-libsql" "github.com/felt-app/felt/internal/audit" "github.com/felt-app/felt/internal/clock" "github.com/felt-app/felt/internal/financial" "github.com/felt-app/felt/internal/template" ) // setupIntegrationDB creates a file-based test DB that supports concurrent // access from background goroutines (e.g., clock ticker). In-memory DBs with // libsql don't reliably support cross-goroutine access. func setupIntegrationDB(t *testing.T) *sql.DB { t.Helper() if os.Getenv("CGO_ENABLED") == "0" { t.Skip("requires CGO for libsql") } tmpDir := t.TempDir() dbPath := filepath.Join(tmpDir, "integration.db") db, err := sql.Open("libsql", "file:"+dbPath) if err != nil { t.Fatalf("open test db: %v", err) } t.Cleanup(func() { db.Close() }) // Enable WAL mode for concurrent read/write access from ticker goroutine. // PRAGMA journal_mode returns a row, so use QueryRow instead of Exec. var journalMode string if err := db.QueryRow("PRAGMA journal_mode=WAL").Scan(&journalMode); err != nil { t.Fatalf("enable WAL: %v", err) } // Allow busy waiting for up to 5 seconds to avoid "database is locked". var busyTimeout int if err := db.QueryRow("PRAGMA busy_timeout=5000").Scan(&busyTimeout); err != nil { t.Fatalf("set busy timeout: %v", err) } // Apply the same schema as setupTestDB stmts := []string{ `CREATE TABLE IF NOT EXISTS venue_settings ( id INTEGER PRIMARY KEY CHECK (id = 1), venue_name TEXT NOT NULL DEFAULT '', currency_code TEXT NOT NULL DEFAULT 'DKK', currency_symbol TEXT NOT NULL DEFAULT 'kr', rounding_denomination INTEGER NOT NULL DEFAULT 5000, receipt_mode TEXT NOT NULL DEFAULT 'digital', timezone TEXT NOT NULL DEFAULT 'Europe/Copenhagen', created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0 )`, `CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 0, chip_up_denomination_value INTEGER, notes TEXT, UNIQUE(structure_id, position) )`, `CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS payout_brackets ( id INTEGER PRIMARY KEY AUTOINCREMENT, structure_id INTEGER NOT NULL, min_entries INTEGER NOT NULL, max_entries INTEGER NOT NULL )`, `CREATE TABLE IF NOT EXISTS payout_tiers ( id INTEGER PRIMARY KEY AUTOINCREMENT, bracket_id INTEGER NOT NULL, position INTEGER NOT NULL, percentage_basis_points INTEGER NOT NULL )`, `CREATE TABLE IF NOT EXISTS buyin_configs ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, buyin_amount INTEGER NOT NULL DEFAULT 0, starting_chips INTEGER NOT NULL DEFAULT 0, rake_total INTEGER NOT NULL DEFAULT 0, bounty_amount INTEGER NOT NULL DEFAULT 0, bounty_chip INTEGER NOT NULL DEFAULT 0, rebuy_allowed INTEGER NOT NULL DEFAULT 0, rebuy_cost INTEGER NOT NULL DEFAULT 0, rebuy_chips INTEGER NOT NULL DEFAULT 0, rebuy_rake INTEGER NOT NULL DEFAULT 0, rebuy_limit INTEGER NOT NULL DEFAULT 0, rebuy_level_cutoff INTEGER, rebuy_time_cutoff_seconds INTEGER, rebuy_chip_threshold INTEGER, addon_allowed INTEGER NOT NULL DEFAULT 0, addon_cost INTEGER NOT NULL DEFAULT 0, addon_chips INTEGER NOT NULL DEFAULT 0, addon_rake INTEGER NOT NULL DEFAULT 0, addon_level_start INTEGER, addon_level_end INTEGER, reentry_allowed INTEGER NOT NULL DEFAULT 0, reentry_limit INTEGER NOT NULL DEFAULT 0, late_reg_level_cutoff INTEGER, late_reg_time_cutoff_seconds INTEGER, created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0 )`, `CREATE TABLE IF NOT EXISTS rake_splits ( id INTEGER PRIMARY KEY AUTOINCREMENT, buyin_config_id INTEGER NOT NULL, category TEXT NOT NULL, amount INTEGER NOT NULL DEFAULT 0, UNIQUE(buyin_config_id, category) )`, `CREATE TABLE IF NOT EXISTS tournament_templates ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT NOT NULL 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 NOT NULL DEFAULT 2, max_players INTEGER, early_signup_bonus_chips INTEGER NOT NULL DEFAULT 0, early_signup_cutoff TEXT, punctuality_bonus_chips INTEGER NOT NULL DEFAULT 0, is_pko INTEGER NOT NULL DEFAULT 0, is_builtin INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0 )`, `CREATE TABLE IF NOT EXISTS tournaments ( id TEXT PRIMARY KEY, name TEXT NOT NULL, template_id INTEGER, chip_set_id INTEGER NOT NULL DEFAULT 1, blind_structure_id INTEGER NOT NULL DEFAULT 1, payout_structure_id INTEGER NOT NULL DEFAULT 1, buyin_config_id INTEGER NOT NULL, points_formula_id INTEGER, status TEXT NOT NULL DEFAULT 'created', min_players INTEGER NOT NULL DEFAULT 2, max_players INTEGER, early_signup_bonus_chips INTEGER NOT NULL DEFAULT 0, early_signup_cutoff TEXT, punctuality_bonus_chips INTEGER NOT NULL DEFAULT 0, is_pko INTEGER NOT NULL DEFAULT 0, current_level INTEGER NOT NULL DEFAULT 0, clock_state TEXT NOT NULL DEFAULT 'stopped', clock_remaining_ns INTEGER NOT NULL DEFAULT 0, total_elapsed_ns INTEGER NOT NULL DEFAULT 0, hand_for_hand INTEGER NOT NULL DEFAULT 0, started_at INTEGER, ended_at INTEGER, created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0 )`, `CREATE TABLE IF NOT EXISTS players ( id TEXT PRIMARY KEY, name TEXT NOT NULL, nickname TEXT, email TEXT, phone TEXT, photo_url TEXT, notes TEXT, custom_fields TEXT, created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0 )`, `CREATE TABLE IF NOT EXISTS tournament_players ( id INTEGER PRIMARY KEY AUTOINCREMENT, tournament_id TEXT NOT NULL, player_id TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'registered', seat_table_id INTEGER, seat_position INTEGER, buy_in_at INTEGER, bust_out_at INTEGER, bust_out_order INTEGER, finishing_position INTEGER, current_chips INTEGER NOT NULL DEFAULT 0, rebuys INTEGER NOT NULL DEFAULT 0, addons INTEGER NOT NULL DEFAULT 0, reentries INTEGER NOT NULL DEFAULT 0, bounty_value INTEGER NOT NULL DEFAULT 0, bounties_collected INTEGER NOT NULL DEFAULT 0, prize_amount INTEGER NOT NULL DEFAULT 0, points_awarded INTEGER NOT NULL DEFAULT 0, early_signup_bonus_applied INTEGER NOT NULL DEFAULT 0, punctuality_bonus_applied INTEGER NOT NULL DEFAULT 0, hitman_player_id TEXT, created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0, UNIQUE(tournament_id, player_id) )`, `CREATE TABLE IF NOT EXISTS transactions ( id TEXT PRIMARY KEY, tournament_id TEXT NOT NULL, player_id TEXT NOT NULL, type TEXT NOT NULL, amount INTEGER NOT NULL DEFAULT 0, chips INTEGER NOT NULL DEFAULT 0, operator_id TEXT NOT NULL DEFAULT '', receipt_data TEXT, undone INTEGER NOT NULL DEFAULT 0, undone_by TEXT, metadata TEXT, created_at INTEGER NOT NULL DEFAULT 0 )`, `CREATE TABLE IF NOT EXISTS tables ( id INTEGER PRIMARY KEY AUTOINCREMENT, tournament_id TEXT NOT NULL, name TEXT NOT NULL, seat_count INTEGER NOT NULL DEFAULT 9, dealer_button_position INTEGER, is_active INTEGER NOT NULL DEFAULT 1, hand_completed INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0 )`, `CREATE TABLE IF NOT EXISTS audit_entries ( id TEXT PRIMARY KEY, tournament_id TEXT, timestamp INTEGER NOT NULL, operator_id TEXT NOT NULL DEFAULT 'system', action TEXT NOT NULL, target_type TEXT NOT NULL, target_id TEXT NOT NULL, previous_state TEXT, new_state TEXT, metadata TEXT, undone_by TEXT )`, `CREATE TABLE IF NOT EXISTS operators ( id TEXT PRIMARY KEY, name TEXT NOT NULL, pin_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'floor', active INTEGER NOT NULL DEFAULT 1, created_at INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL DEFAULT 0 )`, `CREATE TABLE IF NOT EXISTS chip_denominations ( id INTEGER PRIMARY KEY AUTOINCREMENT, chip_set_id INTEGER NOT NULL, value INTEGER NOT NULL, color_hex TEXT NOT NULL DEFAULT '#FFFFFF', label TEXT NOT NULL DEFAULT '', sort_order INTEGER NOT NULL DEFAULT 0 )`, // Seed data `INSERT OR IGNORE INTO venue_settings (id, venue_name, currency_code, currency_symbol, rounding_denomination) VALUES (1, 'Test Venue', 'DKK', 'kr', 100)`, `INSERT OR IGNORE INTO chip_sets (id, name) VALUES (1, 'Standard')`, `INSERT OR IGNORE INTO chip_denominations (chip_set_id, value, color_hex, label, sort_order) VALUES (1, 25, '#FFFFFF', '25', 1)`, `INSERT OR IGNORE INTO blind_structures (id, name) VALUES (1, 'Standard')`, `INSERT OR IGNORE INTO blind_levels (structure_id, position, level_type, game_type, small_blind, big_blind, ante, bb_ante, duration_seconds, notes) VALUES (1, 1, 'round', 'nlhe', 100, 200, 0, 0, 600, '')`, `INSERT OR IGNORE INTO blind_levels (structure_id, position, level_type, game_type, small_blind, big_blind, ante, bb_ante, duration_seconds, notes) VALUES (1, 2, 'round', 'nlhe', 200, 400, 0, 0, 600, '')`, `INSERT OR IGNORE INTO payout_structures (id, name) VALUES (1, 'Standard')`, `INSERT OR IGNORE INTO buyin_configs (id, name, buyin_amount, starting_chips, rake_total) VALUES (1, 'Standard', 50000, 10000, 5000)`, `INSERT OR IGNORE INTO tournament_templates (id, name, chip_set_id, blind_structure_id, payout_structure_id, buyin_config_id, min_players, is_pko) VALUES (1, 'Friday Night Turbo', 1, 1, 1, 1, 3, 0)`, } for _, stmt := range stmts { if _, err := db.Exec(stmt); err != nil { t.Fatalf("exec schema stmt: %v\nStmt: %s", err, stmt) } } return db } // TestTournamentLifecycleIntegration tests the full tournament lifecycle: // create from template, add players, start, bust players, check auto-close. func TestTournamentLifecycleIntegration(t *testing.T) { if os.Getenv("CGO_ENABLED") == "0" { t.Skip("requires CGO for libsql") } db := setupIntegrationDB(t) trail := audit.NewTrail(db, nil) registry := clock.NewRegistry(nil) templates := template.NewTournamentTemplateService(db) buyinSvc := template.NewBuyinService(db) fin := financial.NewEngine(db, trail, nil, nil, buyinSvc) chop := financial.NewChopEngine(db, fin, trail, nil) svc := NewService(db, registry, fin, nil, nil, nil, nil, templates, trail, nil) multi := NewMultiManager(svc) ctx := context.Background() // 1. Create tournament from template tournament, err := svc.CreateFromTemplate(ctx, 1, TournamentOverrides{ Name: "Integration Test Tourney", }) if err != nil { t.Fatalf("create tournament: %v", err) } if tournament.Status != StatusCreated { t.Fatalf("expected status 'created', got '%s'", tournament.Status) } // 2. Add a table _, err = db.Exec( `INSERT INTO tables (tournament_id, name, seat_count, is_active) VALUES (?, 'Table 1', 9, 1)`, tournament.ID, ) if err != nil { t.Fatalf("create table: %v", err) } // 3. Register 5 players players := []struct{ id, name string }{ {"p1", "Alice"}, {"p2", "Bob"}, {"p3", "Charlie"}, {"p4", "Diana"}, {"p5", "Eve"}, } for _, p := range players { _, _ = db.Exec(`INSERT OR IGNORE INTO players (id, name) VALUES (?, ?)`, p.id, p.name) _, _ = db.Exec( `INSERT INTO tournament_players (tournament_id, player_id, status, current_chips) VALUES (?, ?, 'active', 10000)`, tournament.ID, p.id, ) // Create buy-in transaction _, _ = db.Exec( `INSERT INTO transactions (id, tournament_id, player_id, type, amount, chips, operator_id, created_at) VALUES (?, ?, ?, 'buyin', 50000, 10000, 'op1', 0)`, "tx-"+p.id, tournament.ID, p.id, ) } // 4. Verify player summary detail, err := svc.GetTournament(ctx, tournament.ID) if err != nil { t.Fatalf("get tournament: %v", err) } if detail.Players.Active != 5 { t.Errorf("expected 5 active players, got %d", detail.Players.Active) } // 5. Start tournament err = svc.StartTournament(ctx, tournament.ID) if err != nil { t.Fatalf("start tournament: %v", err) } // Stop the clock ticker -- in-memory test DBs don't support the background // goroutine's DB access pattern, and the lifecycle test doesn't need real-time // clock ticking. registry.StopTicker(tournament.ID) // Verify status changed detail, _ = svc.GetTournament(ctx, tournament.ID) if detail.Tournament.Status != StatusRunning { t.Errorf("expected status 'running', got '%s'", detail.Tournament.Status) } // 6. Bust 3 players (leaving 2 active) for _, pid := range []string{"p5", "p4", "p3"} { _, _ = db.Exec( `UPDATE tournament_players SET status = 'busted', current_chips = 0, bust_out_at = unixepoch() WHERE tournament_id = ? AND player_id = ?`, tournament.ID, pid, ) } // 7. Prize pool should reflect buy-ins if fin != nil { pool, err := fin.CalculatePrizePool(ctx, tournament.ID) if err != nil { t.Fatalf("calculate prize pool: %v", err) } // 5 buyins * 50000 = 250000 if pool.TotalBuyInAmount != 250000 { t.Errorf("expected total buyin 250000, got %d", pool.TotalBuyInAmount) } } // 8. Check auto-close with 2 remaining (should NOT auto-close) err = svc.CheckAutoClose(ctx, tournament.ID) if err != nil { t.Fatalf("check auto close (2 remaining): %v", err) } detail, _ = svc.GetTournament(ctx, tournament.ID) if detail.Tournament.Status != StatusRunning { t.Errorf("should still be running with 2 players, got '%s'", detail.Tournament.Status) } // 9. Bust another player (leaving 1 active) _, _ = db.Exec( `UPDATE tournament_players SET status = 'busted', current_chips = 0, bust_out_at = unixepoch() WHERE tournament_id = ? AND player_id = 'p2'`, tournament.ID, ) // 10. Check auto-close with 1 remaining (SHOULD auto-close) err = svc.CheckAutoClose(ctx, tournament.ID) if err != nil { t.Fatalf("check auto close (1 remaining): %v", err) } detail, _ = svc.GetTournament(ctx, tournament.ID) if detail.Tournament.Status != StatusCompleted { t.Errorf("should be completed with 1 player, got '%s'", detail.Tournament.Status) } // 11. Verify tournament appears in completed (not active) list active, _ := multi.ListActiveTournaments(ctx) for _, a := range active { if a.ID == tournament.ID { t.Error("completed tournament should not appear in active list") } } // 12. Audit trail should have entries var auditCount int db.QueryRow("SELECT COUNT(*) FROM audit_entries WHERE tournament_id = ?", tournament.ID).Scan(&auditCount) if auditCount == 0 { t.Error("expected audit entries for tournament") } // Suppress unused variable warnings _ = chop } // TestDealIntegration tests the deal workflow from proposal to confirmation. func TestDealIntegration(t *testing.T) { if os.Getenv("CGO_ENABLED") == "0" { t.Skip("requires CGO for libsql") } db := setupTestDB(t) // Add deal_proposals table _, _ = db.Exec(`CREATE TABLE IF NOT EXISTS deal_proposals ( id TEXT PRIMARY KEY, tournament_id TEXT NOT NULL, deal_type TEXT NOT NULL, payouts TEXT NOT NULL DEFAULT '[]', total_amount INTEGER NOT NULL DEFAULT 0, is_partial INTEGER NOT NULL DEFAULT 0, remaining_pool INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'proposed', created_at INTEGER NOT NULL DEFAULT 0 )`) trail := audit.NewTrail(db, nil) buyinSvc := template.NewBuyinService(db) fin := financial.NewEngine(db, trail, nil, nil, buyinSvc) chop := financial.NewChopEngine(db, fin, trail, nil) ctx := context.Background() // Create tournament with 3 active players tournamentID := "deal-test" _, _ = db.Exec( `INSERT INTO tournaments (id, name, buyin_config_id, payout_structure_id, status, min_players) VALUES (?, 'Deal Test', 1, 1, 'running', 2)`, tournamentID, ) for i, name := range []string{"Alice", "Bob", "Charlie"} { pid := "dp" + intToStr(i+1) _, _ = db.Exec(`INSERT OR IGNORE INTO players (id, name) VALUES (?, ?)`, pid, name) _, _ = db.Exec( `INSERT INTO tournament_players (tournament_id, player_id, status, current_chips) VALUES (?, ?, 'active', ?)`, tournamentID, pid, (i+1)*5000, ) _, _ = db.Exec( `INSERT INTO transactions (id, tournament_id, player_id, type, amount, chips, operator_id, created_at) VALUES (?, ?, ?, 'buyin', 50000, ?, 'op1', 0)`, "tx-deal-"+pid, tournamentID, pid, (i+1)*5000, ) } // Propose an even chop proposal, err := chop.ProposeDeal(ctx, tournamentID, "even_chop", financial.DealParams{}) if err != nil { t.Fatalf("propose deal: %v", err) } if proposal.Status != "proposed" { t.Errorf("expected status 'proposed', got '%s'", proposal.Status) } if len(proposal.Payouts) != 3 { t.Errorf("expected 3 payouts, got %d", len(proposal.Payouts)) } // Verify sum equals pool var sum int64 for _, p := range proposal.Payouts { sum += p.Amount } if sum != proposal.TotalAmount { t.Errorf("payout sum %d != total amount %d", sum, proposal.TotalAmount) } // Confirm the deal err = chop.ConfirmDeal(ctx, tournamentID, proposal.ID) if err != nil { t.Fatalf("confirm deal: %v", err) } // Verify tournament is completed (full chop) var status string db.QueryRow("SELECT status FROM tournaments WHERE id = ?", tournamentID).Scan(&status) if status != "completed" { t.Errorf("expected 'completed', got '%s'", status) } // Verify players have 'deal' status var dealCount int db.QueryRow( "SELECT COUNT(*) FROM tournament_players WHERE tournament_id = ? AND status = 'deal'", tournamentID, ).Scan(&dealCount) if dealCount != 3 { t.Errorf("expected 3 players with 'deal' status, got %d", dealCount) } // Verify chop transactions were created var chopTxCount int db.QueryRow( "SELECT COUNT(*) FROM transactions WHERE tournament_id = ? AND type = 'chop'", tournamentID, ).Scan(&chopTxCount) if chopTxCount != 3 { t.Errorf("expected 3 chop transactions, got %d", chopTxCount) } // Verify total payouts equal prize pool var totalPaid int64 db.QueryRow( "SELECT COALESCE(SUM(amount), 0) FROM transactions WHERE tournament_id = ? AND type = 'chop'", tournamentID, ).Scan(&totalPaid) if totalPaid != proposal.TotalAmount { t.Errorf("total paid %d != proposed total %d", totalPaid, proposal.TotalAmount) } } // TestCancelTournament tests cancellation flow. func TestCancelTournament(t *testing.T) { if os.Getenv("CGO_ENABLED") == "0" { t.Skip("requires CGO for libsql") } db := setupTestDB(t) svc := newTestService(t, db) ctx := context.Background() // Create and start a tournament tournamentID := "cancel-test" _, _ = db.Exec( `INSERT INTO tournaments (id, name, buyin_config_id, payout_structure_id, status, min_players) VALUES (?, 'Cancel Test', 1, 1, 'running', 2)`, tournamentID, ) // Add some transactions _, _ = db.Exec( `INSERT INTO transactions (id, tournament_id, player_id, type, amount, chips, operator_id, created_at) VALUES ('cancel-tx-1', ?, 'p1', 'buyin', 50000, 10000, 'op1', 0)`, tournamentID, ) // Cancel err := svc.CancelTournament(ctx, tournamentID) if err != nil { t.Fatalf("cancel: %v", err) } // Verify status var status string db.QueryRow("SELECT status FROM tournaments WHERE id = ?", tournamentID).Scan(&status) if status != StatusCancelled { t.Errorf("expected 'cancelled', got '%s'", status) } // Verify transactions marked with cancelled metadata var meta sql.NullString db.QueryRow( "SELECT metadata FROM transactions WHERE id = 'cancel-tx-1'", ).Scan(&meta) if !meta.Valid { t.Error("transaction metadata should be set after cancel") } } // TestPauseResume tests pause and resume flow. func TestPauseResume(t *testing.T) { if os.Getenv("CGO_ENABLED") == "0" { t.Skip("requires CGO for libsql") } db := setupTestDB(t) svc := newTestService(t, db) ctx := context.Background() tournamentID := "pause-test" _, _ = db.Exec( `INSERT INTO tournaments (id, name, buyin_config_id, payout_structure_id, status, min_players) VALUES (?, 'Pause Test', 1, 1, 'running', 2)`, tournamentID, ) // Pause err := svc.PauseTournament(ctx, tournamentID) if err != nil { t.Fatalf("pause: %v", err) } var status string db.QueryRow("SELECT status FROM tournaments WHERE id = ?", tournamentID).Scan(&status) if status != StatusPaused { t.Errorf("expected 'paused', got '%s'", status) } // Resume err = svc.ResumeTournament(ctx, tournamentID) if err != nil { t.Fatalf("resume: %v", err) } db.QueryRow("SELECT status FROM tournaments WHERE id = ?", tournamentID).Scan(&status) if status != StatusRunning { t.Errorf("expected 'running', got '%s'", status) } }