package ticket_test import ( "testing" "time" "task/internal/ticket" ) func TestAggregateByProject(t *testing.T) { entries := []ticket.TimeEntry{ {TicketID: "a", ProjectID: "p1", Duration: time.Hour}, {TicketID: "b", ProjectID: "p1", Duration: 30 * time.Minute}, {TicketID: "c", ProjectID: "p2", Duration: 15 * time.Minute}, } rows := ticket.Aggregate(entries, nil, ticket.GroupByProject) if len(rows) != 2 { t.Fatalf("rows=%d want 2", len(rows)) } if rows[0].Key != "p1" || rows[0].Duration != 90*time.Minute { t.Fatalf("first row: %+v", rows[0]) } if ticket.TotalDuration(entries) != 105*time.Minute { t.Fatalf("total=%v", ticket.TotalDuration(entries)) } } func TestAggregateByStatusAndTicket(t *testing.T) { tickets := map[string]ticket.Ticket{ "a": {ID: "a", Title: "Alpha", Status: ticket.StatusTodo}, "b": {ID: "b", Title: "Beta", Status: ticket.StatusDone}, } entries := []ticket.TimeEntry{ {TicketID: "a", Duration: time.Hour}, {TicketID: "b", Duration: 20 * time.Minute}, {TicketID: "a", Duration: 10 * time.Minute}, } byStatus := ticket.Aggregate(entries, tickets, ticket.GroupByStatus) if len(byStatus) != 2 { t.Fatalf("status rows=%d", len(byStatus)) } byTicket := ticket.Aggregate(entries, tickets, ticket.GroupByTicket) if len(byTicket) != 2 { t.Fatalf("ticket rows=%d", len(byTicket)) } if byTicket[0].Label != "Alpha" || byTicket[0].Duration != 70*time.Minute { t.Fatalf("top ticket: %+v", byTicket[0]) } } func TestAggregateByDay(t *testing.T) { d1 := time.Date(2026, 10, 18, 10, 0, 0, 0, time.Local) d2 := time.Date(2026, 10, 19, 11, 0, 0, 0, time.Local) entries := []ticket.TimeEntry{ {Start: d1, Duration: time.Hour}, {Start: d2, Duration: 30 * time.Minute}, {Start: d1, Duration: 15 * time.Minute}, } rows := ticket.Aggregate(entries, nil, ticket.GroupByDay) if len(rows) != 2 { t.Fatalf("day rows=%d", len(rows)) } // Newest day first. if rows[0].Key != "2026-10-19" { t.Fatalf("first day key=%q", rows[0].Key) } if rows[1].Duration != 75*time.Minute { t.Fatalf("older day dur=%v", rows[1].Duration) } } func TestFilterEntries(t *testing.T) { d1 := time.Date(2026, 10, 18, 10, 0, 0, 0, time.Local) d2 := time.Date(2026, 10, 20, 11, 0, 0, 0, time.Local) entries := []ticket.TimeEntry{ {ID: "1", TicketID: "a", Start: d1, Duration: time.Hour}, {ID: "2", TicketID: "b", Start: d2, Duration: 30 * time.Minute}, {ID: "3", TicketID: "a", Start: d2, Duration: 15 * time.Minute}, } byTicket := ticket.FilterEntries(entries, ticket.TimeFilter{TicketID: "a"}) if len(byTicket) != 2 { t.Fatalf("ticket filter=%d", len(byTicket)) } from := time.Date(2026, 10, 19, 0, 0, 0, 0, time.Local) to := ticket.EndOfReportDay(time.Date(2026, 10, 20, 0, 0, 0, 0, time.Local)) byRange := ticket.FilterEntries(entries, ticket.TimeFilter{From: from, To: to}) if len(byRange) != 2 { t.Fatalf("range filter=%d", len(byRange)) } both := ticket.FilterEntries(entries, ticket.TimeFilter{From: from, To: to, TicketID: "a"}) if len(both) != 1 || both[0].ID != "3" { t.Fatalf("combined=%+v", both) } } func TestParseReportDay(t *testing.T) { got, err := ticket.ParseReportDay("2026-10-18") if err != nil { t.Fatal(err) } y, m, d := got.Date() if y != 2026 || m != 10 || d != 18 || got.Hour() != 0 { t.Fatalf("got %v", got) } empty, err := ticket.ParseReportDay("") if err != nil || !empty.IsZero() { t.Fatalf("empty: %v %v", empty, err) } if _, err := ticket.ParseReportDay("nope"); err == nil { t.Fatal("expected error") } }