package clock import ( "testing" "time" ) func TestWarningThresholdDetection(t *testing.T) { engine := NewClockEngine("test-warnings-1", nil) engine.SetWarnings([]Warning{ {Seconds: 5, Type: "both", SoundID: "warning_5s", Message: "5 seconds"}, }) engine.LoadLevels([]Level{ {Position: 0, LevelType: "round", GameType: "nlhe", SmallBlind: 100, BigBlind: 200, DurationSeconds: 8}, }) engine.Start("op1") // Tick until we cross the 5s threshold // The level is 8 seconds, so after ~3 seconds we should hit the 5s warning warningFired := false for i := 0; i < 100; i++ { time.Sleep(50 * time.Millisecond) engine.Tick() snap := engine.Snapshot() if snap.RemainingMs < 5000 { // Check if warning was emitted by checking internal state engine.mu.RLock() warningFired = engine.emittedWarnings[5] engine.mu.RUnlock() if warningFired { break } } } if !warningFired { t.Error("expected 5-second warning to fire") } } func TestWarningNotReEmitted(t *testing.T) { engine := NewClockEngine("test-warnings-2", nil) engine.SetWarnings([]Warning{ {Seconds: 5, Type: "both", SoundID: "warning_5s", Message: "5 seconds"}, }) engine.LoadLevels([]Level{ {Position: 0, LevelType: "round", GameType: "nlhe", SmallBlind: 100, BigBlind: 200, DurationSeconds: 8}, }) engine.Start("op1") // Tick until warning fires for i := 0; i < 100; i++ { time.Sleep(50 * time.Millisecond) engine.Tick() engine.mu.RLock() fired := engine.emittedWarnings[5] engine.mu.RUnlock() if fired { break } } // Mark that we've seen the warning engine.mu.RLock() firstFired := engine.emittedWarnings[5] engine.mu.RUnlock() if !firstFired { t.Fatal("warning never fired") } // Continue ticking -- emittedWarnings[5] should remain true (not re-emitted) for i := 0; i < 10; i++ { time.Sleep(10 * time.Millisecond) engine.Tick() } engine.mu.RLock() stillFired := engine.emittedWarnings[5] engine.mu.RUnlock() if !stillFired { t.Error("warning flag was cleared unexpectedly") } } func TestWarningResetOnLevelChange(t *testing.T) { engine := NewClockEngine("test-warnings-3", nil) engine.SetWarnings([]Warning{ {Seconds: 3, Type: "both", SoundID: "warning_3s", Message: "3 seconds"}, }) engine.LoadLevels([]Level{ {Position: 0, LevelType: "round", GameType: "nlhe", SmallBlind: 100, BigBlind: 200, DurationSeconds: 5}, {Position: 1, LevelType: "round", GameType: "nlhe", SmallBlind: 200, BigBlind: 400, DurationSeconds: 5}, }) engine.Start("op1") // Tick until warning fires for level 0 for i := 0; i < 100; i++ { time.Sleep(30 * time.Millisecond) engine.Tick() engine.mu.RLock() fired := engine.emittedWarnings[3] engine.mu.RUnlock() if fired { break } } // Manual advance to level 1 engine.AdvanceLevel("op1") // After advance, warnings should be reset engine.mu.RLock() resetCheck := engine.emittedWarnings[3] engine.mu.RUnlock() if resetCheck { t.Error("expected warnings to be reset after level change") } } func TestDefaultWarnings(t *testing.T) { warnings := DefaultWarnings() if len(warnings) != 3 { t.Fatalf("expected 3 default warnings, got %d", len(warnings)) } expectedSeconds := []int{60, 30, 10} for i, w := range warnings { if w.Seconds != expectedSeconds[i] { t.Errorf("warning %d: expected %ds, got %ds", i, expectedSeconds[i], w.Seconds) } if w.Type != "both" { t.Errorf("warning %d: expected type 'both', got '%s'", i, w.Type) } } } func TestCustomWarnings(t *testing.T) { engine := NewClockEngine("test-custom-warnings", nil) custom := []Warning{ {Seconds: 120, Type: "visual", SoundID: "custom_120s", Message: "2 minutes"}, {Seconds: 15, Type: "audio", SoundID: "custom_15s", Message: "15 seconds"}, } engine.SetWarnings(custom) got := engine.GetWarnings() if len(got) != 2 { t.Fatalf("expected 2 custom warnings, got %d", len(got)) } if got[0].Seconds != 120 { t.Errorf("expected first warning at 120s, got %ds", got[0].Seconds) } if got[1].Type != "audio" { t.Errorf("expected second warning type 'audio', got '%s'", got[1].Type) } }