package clock import ( "testing" "time" ) // testLevels returns a set of levels for testing: // Level 0: Round (NLHE 100/200, 15 min) // Level 1: Break (5 min) // Level 2: Round (NLHE 200/400, 15 min) func testLevels() []Level { return []Level{ {Position: 0, LevelType: "round", GameType: "nlhe", SmallBlind: 10000, BigBlind: 20000, Ante: 0, DurationSeconds: 900}, {Position: 1, LevelType: "break", GameType: "", SmallBlind: 0, BigBlind: 0, Ante: 0, DurationSeconds: 300}, {Position: 2, LevelType: "round", GameType: "nlhe", SmallBlind: 20000, BigBlind: 40000, Ante: 5000, DurationSeconds: 900}, } } func TestClockStartStop(t *testing.T) { engine := NewClockEngine("test-tournament-1", nil) engine.LoadLevels(testLevels()) // Start if err := engine.Start("op1"); err != nil { t.Fatalf("Start failed: %v", err) } if engine.State() != StateRunning { t.Errorf("expected state running, got %s", engine.State()) } // Can't start again if err := engine.Start("op1"); err == nil { t.Error("expected error starting already running clock") } // Stop if err := engine.Stop("op1"); err != nil { t.Fatalf("Stop failed: %v", err) } if engine.State() != StateStopped { t.Errorf("expected state stopped, got %s", engine.State()) } // Can't stop again if err := engine.Stop("op1"); err == nil { t.Error("expected error stopping already stopped clock") } } func TestClockPauseResume(t *testing.T) { engine := NewClockEngine("test-tournament-2", nil) engine.LoadLevels(testLevels()) engine.Start("op1") // Get initial remaining time snap1 := engine.Snapshot() initialMs := snap1.RemainingMs // Wait a bit time.Sleep(50 * time.Millisecond) // Pause if err := engine.Pause("op1"); err != nil { t.Fatalf("Pause failed: %v", err) } if engine.State() != StatePaused { t.Errorf("expected state paused, got %s", engine.State()) } // Verify time was deducted snapPaused := engine.Snapshot() if snapPaused.RemainingMs >= initialMs { t.Error("expected remaining time to decrease after pause") } // Record paused remaining time pausedMs := snapPaused.RemainingMs // Wait while paused -- time should NOT change time.Sleep(50 * time.Millisecond) snapStillPaused := engine.Snapshot() if snapStillPaused.RemainingMs != pausedMs { t.Errorf("remaining time changed while paused: %d -> %d", pausedMs, snapStillPaused.RemainingMs) } // Resume if err := engine.Resume("op1"); err != nil { t.Fatalf("Resume failed: %v", err) } if engine.State() != StateRunning { t.Errorf("expected state running, got %s", engine.State()) } // After resume, time should start decreasing again time.Sleep(50 * time.Millisecond) snapAfterResume := engine.Snapshot() if snapAfterResume.RemainingMs >= pausedMs { t.Error("expected remaining time to decrease after resume") } } func TestClockCountsDown(t *testing.T) { engine := NewClockEngine("test-tournament-3", nil) // Use a very short level for testing engine.LoadLevels([]Level{ {Position: 0, LevelType: "round", GameType: "nlhe", SmallBlind: 100, BigBlind: 200, DurationSeconds: 1}, }) engine.Start("op1") // Check initial time snap := engine.Snapshot() if snap.RemainingMs < 900 || snap.RemainingMs > 1000 { t.Errorf("expected ~1000ms remaining, got %dms", snap.RemainingMs) } // Wait 200ms and check time decreased time.Sleep(200 * time.Millisecond) snap2 := engine.Snapshot() if snap2.RemainingMs >= snap.RemainingMs { t.Error("clock did not count down") } } func TestLevelAutoAdvance(t *testing.T) { engine := NewClockEngine("test-tournament-4", nil) // Level 0: 100ms, Level 1: 100ms engine.LoadLevels([]Level{ {Position: 0, LevelType: "round", GameType: "nlhe", SmallBlind: 100, BigBlind: 200, DurationSeconds: 1}, {Position: 1, LevelType: "round", GameType: "nlhe", SmallBlind: 200, BigBlind: 400, DurationSeconds: 1}, }) engine.Start("op1") snap := engine.Snapshot() if snap.CurrentLevel != 0 { t.Errorf("expected level 0, got %d", snap.CurrentLevel) } // Simulate ticks until level changes for i := 0; i < 150; i++ { time.Sleep(10 * time.Millisecond) engine.Tick() snap = engine.Snapshot() if snap.CurrentLevel > 0 { break } } if snap.CurrentLevel != 1 { t.Errorf("expected auto-advance to level 1, got %d", snap.CurrentLevel) } } func TestAdvanceLevel(t *testing.T) { engine := NewClockEngine("test-tournament-5", nil) engine.LoadLevels(testLevels()) engine.Start("op1") if err := engine.AdvanceLevel("op1"); err != nil { t.Fatalf("AdvanceLevel failed: %v", err) } snap := engine.Snapshot() if snap.CurrentLevel != 1 { t.Errorf("expected level 1, got %d", snap.CurrentLevel) } if snap.Level.LevelType != "break" { t.Errorf("expected break level, got %s", snap.Level.LevelType) } // Remaining time should be the break duration if snap.RemainingMs < 295000 || snap.RemainingMs > 305000 { t.Errorf("expected ~300000ms for 5-min break, got %dms", snap.RemainingMs) } } func TestRewindLevel(t *testing.T) { engine := NewClockEngine("test-tournament-6", nil) engine.LoadLevels(testLevels()) engine.Start("op1") // Can't rewind at first level if err := engine.RewindLevel("op1"); err == nil { t.Error("expected error rewinding at first level") } // Advance then rewind engine.AdvanceLevel("op1") if err := engine.RewindLevel("op1"); err != nil { t.Fatalf("RewindLevel failed: %v", err) } snap := engine.Snapshot() if snap.CurrentLevel != 0 { t.Errorf("expected level 0 after rewind, got %d", snap.CurrentLevel) } // Should have full duration of level 0 if snap.RemainingMs < 895000 || snap.RemainingMs > 905000 { t.Errorf("expected ~900000ms after rewind, got %dms", snap.RemainingMs) } } func TestJumpToLevel(t *testing.T) { engine := NewClockEngine("test-tournament-7", nil) engine.LoadLevels(testLevels()) engine.Start("op1") // Jump to level 2 if err := engine.JumpToLevel(2, "op1"); err != nil { t.Fatalf("JumpToLevel failed: %v", err) } snap := engine.Snapshot() if snap.CurrentLevel != 2 { t.Errorf("expected level 2, got %d", snap.CurrentLevel) } if snap.Level.SmallBlind != 20000 { t.Errorf("expected SB 20000, got %d", snap.Level.SmallBlind) } // Jump out of range if err := engine.JumpToLevel(99, "op1"); err == nil { t.Error("expected error jumping to invalid level") } if err := engine.JumpToLevel(-1, "op1"); err == nil { t.Error("expected error jumping to negative level") } } func TestHandForHand(t *testing.T) { engine := NewClockEngine("test-tournament-8", nil) engine.LoadLevels(testLevels()) engine.Start("op1") // Enable hand-for-hand (should pause clock) if err := engine.SetHandForHand(true, "op1"); err != nil { t.Fatalf("SetHandForHand failed: %v", err) } snap := engine.Snapshot() if !snap.HandForHand { t.Error("expected hand_for_hand to be true") } if snap.State != "paused" { t.Errorf("expected paused state, got %s", snap.State) } // Disable hand-for-hand (should resume clock) if err := engine.SetHandForHand(false, "op1"); err != nil { t.Fatalf("SetHandForHand disable failed: %v", err) } snap = engine.Snapshot() if snap.HandForHand { t.Error("expected hand_for_hand to be false") } if snap.State != "running" { t.Errorf("expected running state, got %s", snap.State) } } func TestMultipleEnginesIndependent(t *testing.T) { engine1 := NewClockEngine("tournament-a", nil) engine2 := NewClockEngine("tournament-b", nil) engine1.LoadLevels([]Level{ {Position: 0, LevelType: "round", GameType: "nlhe", SmallBlind: 100, BigBlind: 200, DurationSeconds: 900}, }) engine2.LoadLevels([]Level{ {Position: 0, LevelType: "round", GameType: "plo", SmallBlind: 500, BigBlind: 1000, DurationSeconds: 600}, }) engine1.Start("op1") engine2.Start("op2") // Pause engine1 only engine1.Pause("op1") snap1 := engine1.Snapshot() snap2 := engine2.Snapshot() if snap1.State != "paused" { t.Errorf("engine1 should be paused, got %s", snap1.State) } if snap2.State != "running" { t.Errorf("engine2 should be running, got %s", snap2.State) } // Verify they have different game types if snap1.Level.GameType != "nlhe" { t.Errorf("engine1 should be nlhe, got %s", snap1.Level.GameType) } if snap2.Level.GameType != "plo" { t.Errorf("engine2 should be plo, got %s", snap2.Level.GameType) } } func TestSnapshotFields(t *testing.T) { engine := NewClockEngine("test-snapshot-9", nil) engine.LoadLevels(testLevels()) engine.Start("op1") snap := engine.Snapshot() // Check all required fields if snap.TournamentID != "test-snapshot-9" { t.Errorf("expected tournament_id test-snapshot-9, got %s", snap.TournamentID) } if snap.State != "running" { t.Errorf("expected state running, got %s", snap.State) } if snap.CurrentLevel != 0 { t.Errorf("expected level 0, got %d", snap.CurrentLevel) } if snap.Level.LevelType != "round" { t.Errorf("expected round level, got %s", snap.Level.LevelType) } if snap.NextLevel == nil { t.Error("expected next_level to be non-nil") } if snap.NextLevel != nil && snap.NextLevel.LevelType != "break" { t.Errorf("expected next_level to be break, got %s", snap.NextLevel.LevelType) } if snap.RemainingMs <= 0 { t.Errorf("expected positive remaining_ms, got %d", snap.RemainingMs) } if snap.ServerTimeMs <= 0 { t.Errorf("expected positive server_time_ms, got %d", snap.ServerTimeMs) } if snap.LevelCount != 3 { t.Errorf("expected level_count 3, got %d", snap.LevelCount) } if len(snap.Warnings) != 3 { t.Errorf("expected 3 warnings, got %d", len(snap.Warnings)) } } func TestTotalElapsedExcludesPaused(t *testing.T) { engine := NewClockEngine("test-elapsed-10", nil) engine.LoadLevels(testLevels()) engine.Start("op1") // Run for 100ms time.Sleep(100 * time.Millisecond) engine.Pause("op1") snapPaused := engine.Snapshot() elapsedAtPause := snapPaused.TotalElapsedMs // Stay paused for 200ms time.Sleep(200 * time.Millisecond) snapStillPaused := engine.Snapshot() // Elapsed should NOT have increased while paused if snapStillPaused.TotalElapsedMs != elapsedAtPause { t.Errorf("elapsed time changed while paused: %d -> %d", elapsedAtPause, snapStillPaused.TotalElapsedMs) } // Resume and run for another 100ms engine.Resume("op1") time.Sleep(100 * time.Millisecond) snapAfterResume := engine.Snapshot() // Elapsed should now be ~200ms (100 before pause + 100 after resume) // NOT ~500ms (which would include the 200ms pause) if snapAfterResume.TotalElapsedMs < 150 || snapAfterResume.TotalElapsedMs > 350 { t.Errorf("expected total elapsed ~200ms, got %dms", snapAfterResume.TotalElapsedMs) } } func TestCrashRecovery(t *testing.T) { engine := NewClockEngine("test-recovery-11", nil) engine.LoadLevels(testLevels()) // Simulate crash recovery from a running state engine.RestoreState(1, 150*int64(time.Second), 750*int64(time.Second), "running", false) snap := engine.Snapshot() // Should be paused for safety (not running) if snap.State != "paused" { t.Errorf("expected paused state after crash recovery, got %s", snap.State) } if snap.CurrentLevel != 1 { t.Errorf("expected level 1, got %d", snap.CurrentLevel) } if snap.RemainingMs != 150000 { t.Errorf("expected 150000ms remaining, got %dms", snap.RemainingMs) } if snap.TotalElapsedMs != 750000 { t.Errorf("expected 750000ms elapsed, got %dms", snap.TotalElapsedMs) } } func TestNoLevelsError(t *testing.T) { engine := NewClockEngine("test-empty-12", nil) // Start without levels if err := engine.Start("op1"); err == nil { t.Error("expected error starting without levels") } } func TestNextLevelNilAtLast(t *testing.T) { engine := NewClockEngine("test-last-13", nil) engine.LoadLevels([]Level{ {Position: 0, LevelType: "round", GameType: "nlhe", SmallBlind: 100, BigBlind: 200, DurationSeconds: 900}, }) engine.Start("op1") snap := engine.Snapshot() if snap.NextLevel != nil { t.Error("expected nil next_level at last level") } } func TestOvertimeRepeat(t *testing.T) { engine := NewClockEngine("test-overtime-14", nil) engine.SetOvertimeMode(OvertimeRepeat) engine.LoadLevels([]Level{ {Position: 0, LevelType: "round", GameType: "nlhe", SmallBlind: 100, BigBlind: 200, DurationSeconds: 1}, }) engine.Start("op1") // Simulate ticks past the level duration for i := 0; i < 150; i++ { time.Sleep(10 * time.Millisecond) engine.Tick() } snap := engine.Snapshot() // Should still be on level 0 (repeated), still running if snap.CurrentLevel != 0 { t.Errorf("expected level 0 in overtime repeat, got %d", snap.CurrentLevel) } if snap.State != "running" { t.Errorf("expected running in overtime repeat, got %s", snap.State) } // Remaining time should have been reset if snap.RemainingMs <= 0 { t.Error("expected positive remaining time in overtime repeat") } } func TestOvertimeStop(t *testing.T) { engine := NewClockEngine("test-overtime-stop-15", nil) engine.SetOvertimeMode(OvertimeStop) engine.LoadLevels([]Level{ {Position: 0, LevelType: "round", GameType: "nlhe", SmallBlind: 100, BigBlind: 200, DurationSeconds: 1}, }) engine.Start("op1") // Simulate ticks past the level duration for i := 0; i < 150; i++ { time.Sleep(10 * time.Millisecond) engine.Tick() } snap := engine.Snapshot() if snap.State != "stopped" { t.Errorf("expected stopped in overtime stop, got %s", snap.State) } }