package calendartest import ( "strings" "testing" "time" calendar "amadan.net/oficina/calendar/internal/calendar" ) // The sentence at the top of Today is the app's one opinion, so it gets // a test rather than a hope. These assert the wording, because the // wording IS the feature — a summary that says the right number in the // wrong voice is a summary nobody reads twice. func TestDaySummaryReadsLikeASentence(t *testing.T) { const tz = "UTC" day := time.Date(2026, 9, 2, 12, 0, 0, 0, time.UTC) // a Wednesday dayStart, dayEnd, err := calendar.DayBounds(day, tz) if err != nil { t.Fatalf("DayBounds: %v", err) } at := func(hh, mm int) int64 { return time.Date(2026, 9, 2, hh, mm, 0, 0, time.UTC).Unix() } ev := func(kind string, fromH, fromM, toH, toM int) calendar.EventView { return calendar.EventView{ Event: calendar.Event{Kind: kind}, OccStartUTC: at(fromH, fromM), OccEndUTC: at(toH, toM), } } t.Run("an empty day says so plainly", func(t *testing.T) { got := calendar.SummariseDay(day, nil, dayStart, dayEnd, tz).Sentence if got != "Wednesday. Nothing booked." { t.Errorf("sentence = %q", got) } }) t.Run("one thing is not pluralised", func(t *testing.T) { got := calendar.SummariseDay(day, []calendar.EventView{ ev(calendar.KindMeeting, 9, 30, 10, 0), }, dayStart, dayEnd, tz).Sentence if !strings.HasPrefix(got, "Wednesday. One thing; the first at 09:30, the last ends at 10:00.") { t.Errorf("sentence = %q", got) } }) t.Run("the gap is between the first and last, not the whole day", func(t *testing.T) { s := calendar.SummariseDay(day, []calendar.EventView{ ev(calendar.KindMeeting, 9, 30, 10, 0), ev(calendar.KindMeeting, 16, 0, 17, 0), }, dayStart, dayEnd, tz) // 09:30 to 17:00 is 7h30; 1h30 of it is booked; 6h free. if s.UnbookedMin != 360 { t.Errorf("UnbookedMin = %d, want 360", s.UnbookedMin) } if !strings.Contains(s.Sentence, "That leaves six hours unbooked between them.") { t.Errorf("sentence = %q", s.Sentence) } }) t.Run("overlapping meetings do not consume the day twice", func(t *testing.T) { s := calendar.SummariseDay(day, []calendar.EventView{ ev(calendar.KindMeeting, 9, 0, 11, 0), ev(calendar.KindMeeting, 10, 0, 12, 0), ev(calendar.KindMeeting, 15, 0, 16, 0), }, dayStart, dayEnd, tz) // 09:00-12:00 merged is three hours, plus one at 15:00. The // span is 09:00-16:00, so three hours are free. Counting the // overlap twice would report four. if s.UnbookedMin != 180 { t.Errorf("UnbookedMin = %d, want 180 (the 10:00-11:00 overlap counted once)", s.UnbookedMin) } }) t.Run("a full day says the alarming thing plainly", func(t *testing.T) { s := calendar.SummariseDay(day, []calendar.EventView{ ev(calendar.KindMeeting, 9, 0, 13, 0), ev(calendar.KindMeeting, 13, 0, 17, 0), }, dayStart, dayEnd, tz) if !strings.Contains(s.Sentence, "There is no unbooked hour between them.") { t.Errorf("sentence = %q", s.Sentence) } }) // A birthday does not make a Tuesday fuller. Notes are excluded // from the arithmetic entirely. t.Run("notes do not occupy the day", func(t *testing.T) { s := calendar.SummariseDay(day, []calendar.EventView{ ev(calendar.KindNote, 9, 0, 10, 0), }, dayStart, dayEnd, tz) if s.Count != 0 { t.Errorf("Count = %d, want 0: a note is not a thing that occupies time", s.Count) } if s.Sentence != "Wednesday. Nothing booked." { t.Errorf("sentence = %q", s.Sentence) } }) // The sentence must never contain a digit where a word was meant, // nor a stray fragment — the failure mode of assembling prose in // code is a sentence that is grammatical nonsense. t.Run("hours and minutes read as English", func(t *testing.T) { s := calendar.SummariseDay(day, []calendar.EventView{ ev(calendar.KindMeeting, 9, 0, 9, 30), ev(calendar.KindMeeting, 11, 45, 12, 0), }, dayStart, dayEnd, tz) // 09:00-12:00 span, 45 minutes booked, so 2h15 free. if !strings.Contains(s.Sentence, "two hours and 15 minutes unbooked") { t.Errorf("sentence = %q", s.Sentence) } }) }