package domain import ( "testing" ) func mkEvent(typ string, ts int64) Event { return 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([]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 := []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_DayStartWithOutFirst(t *testing.T) { events := []Event{ mkEvent("out", 200), mkEvent("in", 300), mkEvent("out", 400), } got := ComputeDailyTotals(events, 0, 100, 500) if got.TotalSeconds != 200 { t.Errorf("dayStart with out first: TotalSeconds=%d, want 200", got.TotalSeconds) } } func TestComputeDailyTotals_DayStartWithInFirst(t *testing.T) { events := []Event{ mkEvent("in", 100), mkEvent("out", 200), mkEvent("in", 300), mkEvent("out", 400), } got := ComputeDailyTotals(events, 0, 50, 500) if got.TotalSeconds != 200 { t.Errorf("dayStart with in first: TotalSeconds=%d, want 200", got.TotalSeconds) } if got.BreakSeconds != 200 { t.Errorf("dayStart with in first: BreakSeconds=%d, want 200", got.BreakSeconds) } } func TestComputeDailyTotals_DayStartInFirstRealistic(t *testing.T) { events := []Event{ mkEvent("in", 33060), mkEvent("out", 47100), mkEvent("in", 48780), mkEvent("out", 74460), } got := ComputeDailyTotals(events, 0, 0, 86399) if got.TotalSeconds != 39720 { t.Errorf("realistic day: TotalSeconds=%d, want 39720 (11h 02m)", got.TotalSeconds) } if got.BreakSeconds != 13619 { t.Errorf("realistic day: BreakSeconds=%d, want 13619 (3h 19m)", got.BreakSeconds) } } func TestComputeDailyTotals_MissingLastOut(t *testing.T) { events := []Event{ mkEvent("in", 100), mkEvent("out", 200), mkEvent("in", 300), } 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 := []Event{ mkEvent("in", 100), mkEvent("out", 200), mkEvent("in", 250), mkEvent("out", 400), } 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 := []Event{ mkEvent("in", 100), mkEvent("out", 200), mkEvent("in", 300), mkEvent("out", 400), } 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 := []Event{ mkEvent("in", 100), mkEvent("out", 200), mkEvent("out", 250), mkEvent("in", 300), mkEvent("out", 400), } 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 := []Event{ mkEvent("in", 100), mkEvent("in", 150), mkEvent("out", 300), } 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 := []Event{ mkEvent("in", 100), } 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 := []Event{ mkEvent("out", 200), } 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 := []Event{ mkEvent("in", 100), mkEvent("out", 200), mkEvent("in", 250), mkEvent("out", 400), } 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 := []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 := []Event{ mkEvent("in", 100), mkEvent("out", 200), mkEvent("in", 300), mkEvent("out", 400), mkEvent("in", 500), mkEvent("out", 600), } 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 := []Event{ mkEvent("out", 200), } 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 := []Event{ mkEvent("in", 100), } got := ComputeDailyTotals(events, 0, 0, 0) if got.TotalSeconds != 0 { t.Errorf("only in no dayEnd: TotalSeconds=%d, want 0", got.TotalSeconds) } }