package bot import ( "testing" "worktimeBot/internal/db" ) func mkEvent(typ string, ts int64) db.Event { return db.Event{EventType: typ, OccurredAt: ts} } func TestComputeDailyTotals_Empty(t *testing.T) { got := ComputeDailyTotals(nil, 0, 0, 0) if got.TotalSeconds != 0 || got.BreakSeconds != 0 { t.Errorf("empty input: got %+v", got) } } func TestComputeDailyTotals_NoEvents(t *testing.T) { got := ComputeDailyTotals([]db.Event{}, 0, 0, 0) if got.TotalSeconds != 0 || got.BreakSeconds != 0 { t.Errorf("no events: got %+v", got) } } func TestComputeDailyTotals_SingleInOut(t *testing.T) { events := []db.Event{ mkEvent("in", 100), mkEvent("out", 200), } got := ComputeDailyTotals(events, 0, 0, 0) if got.TotalSeconds != 100 { t.Errorf("single in/out: TotalSeconds=%d, want 100", got.TotalSeconds) } if got.BreakSeconds != 0 { t.Errorf("single in/out: BreakSeconds=%d, want 0", got.BreakSeconds) } } func TestComputeDailyTotals_MissingFirstIn(t *testing.T) { events := []db.Event{ mkEvent("out", 200), mkEvent("in", 300), mkEvent("out", 400), } // synthetic "in" at dayStart=100 => in(100), out(200)=100s work, break(200-300), in(300), out(400)=100s work got := ComputeDailyTotals(events, 0, 100, 500) if got.TotalSeconds != 200 { t.Errorf("missing first in: TotalSeconds=%d, want 200", got.TotalSeconds) } } func TestComputeDailyTotals_MissingLastOut(t *testing.T) { events := []db.Event{ mkEvent("in", 100), mkEvent("out", 200), mkEvent("in", 300), } // synthetic "out" at dayEnd=500 => 100s + 200s got := ComputeDailyTotals(events, 0, 0, 500) if got.TotalSeconds != 300 { t.Errorf("missing last out: TotalSeconds=%d, want 300", got.TotalSeconds) } } func TestComputeDailyTotals_BreakUnderThreshold(t *testing.T) { events := []db.Event{ mkEvent("in", 100), mkEvent("out", 200), mkEvent("in", 250), mkEvent("out", 400), } // break = 50s, threshold = 60s => break absorbed into work // work: 100-200(100s), then 200-250 absorbed(50s), 250-400(150s) = 300s got := ComputeDailyTotals(events, 60, 0, 0) if got.TotalSeconds != 300 { t.Errorf("break under threshold: TotalSeconds=%d, want 300", got.TotalSeconds) } if got.BreakSeconds != 0 { t.Errorf("break under threshold: BreakSeconds=%d, want 0", got.BreakSeconds) } } func TestComputeDailyTotals_BreakOverThreshold(t *testing.T) { events := []db.Event{ mkEvent("in", 100), mkEvent("out", 200), mkEvent("in", 300), mkEvent("out", 400), } // break = 100s, threshold = 60s => counted as break got := ComputeDailyTotals(events, 60, 0, 0) if got.TotalSeconds != 200 { t.Errorf("break over threshold: TotalSeconds=%d, want 200", got.TotalSeconds) } if got.BreakSeconds != 100 { t.Errorf("break over threshold: BreakSeconds=%d, want 100", got.BreakSeconds) } } func TestComputeDailyTotals_ConsecutiveOuts(t *testing.T) { events := []db.Event{ mkEvent("in", 100), mkEvent("out", 200), mkEvent("out", 250), mkEvent("in", 300), mkEvent("out", 400), } // in(100)..out(200)=100s work, state=OnBreak breakStart=200 // out(250) while OnBreak => reset breakStart=250 // in(300) => state=Working, workStart=300 // out(400) => work=100s got := ComputeDailyTotals(events, 0, 0, 0) if got.TotalSeconds != 200 { t.Errorf("consecutive outs: TotalSeconds=%d, want 200", got.TotalSeconds) } } func TestComputeDailyTotals_ConsecutiveIns(t *testing.T) { events := []db.Event{ mkEvent("in", 100), mkEvent("in", 150), mkEvent("out", 300), } // second "in" while already working is ignored got := ComputeDailyTotals(events, 0, 0, 0) if got.TotalSeconds != 200 { t.Errorf("consecutive ins: TotalSeconds=%d, want 200", got.TotalSeconds) } } func TestComputeDailyTotals_SingleInEndOfDay(t *testing.T) { events := []db.Event{ mkEvent("in", 100), } // missing out => synthetic out at dayEnd got := ComputeDailyTotals(events, 0, 0, 500) if got.TotalSeconds != 400 { t.Errorf("single in with dayEnd: TotalSeconds=%d, want 400", got.TotalSeconds) } } func TestComputeDailyTotals_SingleOutStartOfDay(t *testing.T) { events := []db.Event{ mkEvent("out", 200), } // missing in => synthetic in at dayStart got := ComputeDailyTotals(events, 0, 100, 500) if got.TotalSeconds != 100 { t.Errorf("single out with dayStart: TotalSeconds=%d, want 100", got.TotalSeconds) } } func TestComputeDailyTotals_ZeroThreshold(t *testing.T) { events := []db.Event{ mkEvent("in", 100), mkEvent("out", 200), mkEvent("in", 250), mkEvent("out", 400), } // threshold = 0, any break is counted // work: 100-200(100s) + 250-400(150s) = 250s, break: 200-250(50s) got := ComputeDailyTotals(events, 0, 0, 0) if got.TotalSeconds != 250 { t.Errorf("zero threshold: TotalSeconds=%d, want 250", got.TotalSeconds) } if got.BreakSeconds != 50 { t.Errorf("zero threshold: BreakSeconds=%d, want 50", got.BreakSeconds) } } func TestComputeDailyTotals_LargeBreak(t *testing.T) { events := []db.Event{ mkEvent("in", 100), mkEvent("out", 200), mkEvent("in", 10000), mkEvent("out", 10100), } got := ComputeDailyTotals(events, 0, 0, 0) if got.TotalSeconds != 200 { t.Errorf("large break: TotalSeconds=%d, want 200", got.TotalSeconds) } if got.BreakSeconds != 9800 { t.Errorf("large break: BreakSeconds=%d, want 9800", got.BreakSeconds) } } func TestComputeDailyTotals_MultipleSessions(t *testing.T) { events := []db.Event{ mkEvent("in", 100), mkEvent("out", 200), mkEvent("in", 300), mkEvent("out", 400), mkEvent("in", 500), mkEvent("out", 600), } // 3 work sessions: 100+100+100 = 300s got := ComputeDailyTotals(events, 0, 0, 0) if got.TotalSeconds != 300 { t.Errorf("multiple sessions: TotalSeconds=%d, want 300", got.TotalSeconds) } if got.BreakSeconds != 200 { t.Errorf("multiple sessions: BreakSeconds=%d, want 200", got.BreakSeconds) } } func TestComputeDailyTotals_OnlyOut(t *testing.T) { events := []db.Event{ mkEvent("out", 200), } // no dayStart => no synthetic in => idle -> out triggers OnBreak got := ComputeDailyTotals(events, 0, 0, 0) if got.TotalSeconds != 0 { t.Errorf("only out no dayStart: TotalSeconds=%d, want 0", got.TotalSeconds) } } func TestComputeDailyTotals_OnlyIn(t *testing.T) { events := []db.Event{ mkEvent("in", 100), } // no dayEnd => no synthetic out => still working at end got := ComputeDailyTotals(events, 0, 0, 0) if got.TotalSeconds != 0 { t.Errorf("only in no dayEnd: TotalSeconds=%d, want 0", got.TotalSeconds) } }