package calendar

import (
	"strings"
	"testing"
	"time"
)

func TestParseICalDayKeepsOnlyToday(t *testing.T) {
	now := time.Date(2026, 7, 3, 12, 0, 0, 0, time.UTC)

	body := []byte(`BEGIN:VCALENDAR
BEGIN:VEVENT
UID:a@example
DTSTART:20260703T090000Z
DTEND:20260703T100000Z
SUMMARY:Morning standup
END:VEVENT
BEGIN:VEVENT
DTSTART:20260703T140000Z
DTEND:20260703T150000Z
SUMMARY:Team sync
END:VEVENT
BEGIN:VEVENT
DTSTART:20260702T140000Z
DTEND:20260702T150000Z
SUMMARY:Yesterday retro
END:VEVENT
BEGIN:VEVENT
DTSTART:20260704T090000Z
DTEND:20260704T100000Z
SUMMARY:Tomorrow standup
END:VEVENT
BEGIN:VEVENT
DTSTART;VALUE=DATE:20260704
DTEND;VALUE=DATE:20260705
SUMMARY:All-day event
END:VEVENT
END:VCALENDAR`)

	events := ParseICalDay(body, now)
	if len(events) != 2 {
		t.Fatalf("got %d events, want 2 (today only, no all-day/past/future)", len(events))
	}
	if events[0].Summary != "Morning standup" || events[0].UID != "a@example" {
		t.Errorf("events[0] = %+v", events[0])
	}
	if !events[0].Start.Equal(time.Date(2026, 7, 3, 9, 0, 0, 0, time.UTC)) {
		t.Errorf("events[0].Start = %v", events[0].Start)
	}
	if !events[0].End.Equal(time.Date(2026, 7, 3, 10, 0, 0, 0, time.UTC)) {
		t.Errorf("events[0].End = %v", events[0].End)
	}
	if events[1].Summary != "Team sync" {
		t.Errorf("events[1].Summary = %q", events[1].Summary)
	}
}

// Regression: "today" is the owner's day, in the owner's location. Taking the
// day number off a local clock but building the boundaries in UTC made the
// evening fall outside the window on any box east of Greenwich.
func TestParseICalDayUsesOwnersDay(t *testing.T) {
	plus4 := time.FixedZone("+04", 4*60*60)
	// 01:00 on Aug 1 local is 21:00 on Jul 31 UTC.
	now := time.Date(2026, 8, 1, 1, 0, 0, 0, plus4)
	body := []byte("BEGIN:VCALENDAR\nBEGIN:VEVENT\n" +
		"DTSTART:20260731T195406Z\nDTEND:20260731T235406Z\nSUMMARY:Current meeting\n" +
		"END:VEVENT\nEND:VCALENDAR")

	events := ParseICalDay(body, now)
	if len(events) != 1 {
		t.Fatalf("got %d events, want the in-progress one", len(events))
	}
	if !Busy(events, now.UTC()) {
		t.Error("an event in progress right now must read as busy")
	}
}

func TestParseVEVENT(t *testing.T) {
	block := "DTSTART;TZID=Europe/Moscow:20260703T130000\nDTEND:20260703T140000Z\nSUMMARY:Stand up meeting"
	e, ok := parseVEVENT(block, time.UTC)
	if !ok {
		t.Fatal("expected a parsed event")
	}
	// 13:00 Moscow is 10:00Z. Reading it as 13:00Z is the bug that put the
	// event three hours late in the day plan.
	if !e.Start.Equal(time.Date(2026, 7, 3, 10, 0, 0, 0, time.UTC)) {
		t.Errorf("start = %v", e.Start)
	}
	if !e.End.Equal(time.Date(2026, 7, 3, 14, 0, 0, 0, time.UTC)) {
		t.Errorf("end = %v", e.End)
	}
	if e.Summary != "Stand up meeting" {
		t.Errorf("summary = %q", e.Summary)
	}

	allDay := "DTSTART;VALUE=DATE:20260703\nDTEND;VALUE=DATE:20260704\nSUMMARY:All-day"
	if _, ok := parseVEVENT(allDay, time.UTC); ok {
		t.Error("all-day event should be rejected")
	}
}

func TestParseDT(t *testing.T) {
	plus4 := time.FixedZone("+04", 4*60*60)
	tests := []struct {
		name   string
		line   string
		loc    *time.Location
		want   time.Time
		wantOK bool
	}{
		{"UTC", "DTEND:20260703T100000Z", plus4, time.Date(2026, 7, 3, 10, 0, 0, 0, time.UTC), true},
		{"tzid", "DTSTART;TZID=Europe/Moscow:20260703T130000", plus4, time.Date(2026, 7, 3, 10, 0, 0, 0, time.UTC), true},
		{"tzid quoted", `DTSTART;TZID="Europe/Moscow":20260703T130000`, plus4, time.Date(2026, 7, 3, 10, 0, 0, 0, time.UTC), true},
		{"tzid with other params", "DTSTART;VALUE=DATE-TIME;TZID=Asia/Tokyo:20260703T130000", plus4, time.Date(2026, 7, 3, 4, 0, 0, 0, time.UTC), true},
		// An unloadable zone falls back to the reader's own clock, not to UTC.
		{"unknown tzid", "DTSTART;TZID=Mars/Olympus:20260703T130000", plus4, time.Date(2026, 7, 3, 13, 0, 0, 0, plus4), true},
		// Floating: no Z, no TZID. Local to whoever reads it.
		{"floating", "DTSTART:20260703T130000", plus4, time.Date(2026, 7, 3, 13, 0, 0, 0, plus4), true},
		{"all-day", "DTSTART;VALUE=DATE:20260703", plus4, time.Time{}, false},
		{"garbage", "DTSTART:garbage", plus4, time.Time{}, false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, ok := parseDT(tt.line, tt.loc)
			if ok != tt.wantOK {
				t.Errorf("ok = %v, want %v", ok, tt.wantOK)
			}
			if !got.Equal(tt.want) {
				t.Errorf("got %v, want %v", got, tt.want)
			}
		})
	}
}

func TestSafeKey(t *testing.T) {
	tests := []struct{ in, want string }{
		{"Stand up meeting", "Stand-up-meeting"},
		{"Hello_World", "Hello-World"},
		{"special@#$chars!!", "specialchars"},
		{"ALL_CAPS_123", "ALL-CAPS-123"},
		// His calendar is Russian. These reduced to "--" and "--" (Vikunja #443).
		{"Встреча с Аней", "Встреча-с-Аней"},
		{"Обед с мамой", "Обед-с-мамой"},
	}
	for _, tt := range tests {
		if got := safeKey(tt.in); got != tt.want {
			t.Errorf("safeKey(%q) = %q, want %q", tt.in, got, tt.want)
		}
	}
}

func TestFactKeyAndValue(t *testing.T) {
	e := Event{
		Summary: "Team sync",
		Start:   time.Date(2026, 7, 3, 14, 0, 0, 0, time.UTC),
		End:     time.Date(2026, 7, 3, 15, 0, 0, 0, time.UTC),
	}
	if got, want := FactKeyIn(e, time.UTC), "calendar_event_20260703_Team-sync"; got != want {
		t.Errorf("FactKey = %q, want %q", got, want)
	}
	if got, want := FactValueIn(e, time.UTC), "Team sync @ 14:00-15:00"; got != want {
		t.Errorf("FactValue = %q, want %q", got, want)
	}
	if got, want := KeyPrefixForDay(e.Start), "calendar_event_20260703"; got != want {
		t.Errorf("KeyPrefixForDay = %q, want %q", got, want)
	}
	if !strings.HasPrefix(FactKeyIn(e, time.UTC), KeyPrefixForDay(e.Start)) {
		t.Error("FactKey must start with the day prefix the store range-scans on")
	}
}

// The key date and the printed time are the owner's, not the calendar
// server's. A 23:00 Moscow event read on a +04 box belongs to the next local
// day, and filing it under the Moscow day would hide it from the day plan the
// store range-scans for.
func TestFactKeyAndValueUseTheOwnersClock(t *testing.T) {
	msk := time.FixedZone("MSK", 3*60*60)
	plus4 := time.FixedZone("+04", 4*60*60)
	e := Event{
		Summary: "Late sync",
		Start:   time.Date(2026, 7, 3, 23, 30, 0, 0, msk),
		End:     time.Date(2026, 7, 4, 0, 30, 0, 0, msk),
	}
	if got, want := FactKeyIn(e, plus4), "calendar_event_20260704_Late-sync"; got != want {
		t.Errorf("FactKeyIn = %q, want %q", got, want)
	}
	if got, want := FactValueIn(e, plus4), "Late sync @ 00:30-01:30"; got != want {
		t.Errorf("FactValueIn = %q, want %q", got, want)
	}
}

func TestFactSummaryDropsTheTimeTail(t *testing.T) {
	if got, want := FactSummary("Standup @ 14:00-14:30"), "Standup"; got != want {
		t.Errorf("FactSummary = %q, want %q", got, want)
	}
	// Nothing that is not the exact tail FactValue writes is touched.
	for _, in := range []string{"Coffee @ home", "Standup", "Standup @ 14:00-14:3", "Standup @ 1a:00-14:30"} {
		if got := FactSummary(in); got != in {
			t.Errorf("FactSummary(%q) = %q, want it unchanged", in, got)
		}
	}
}

// Regression for the mirror image of the window bug: the window is local, so
// the event must be a real instant too. A 22:00 event stated in the poller's
// own zone used to parse as 22:00Z, which on a +03 box is past the end of the
// local day, and the whole evening dropped out of both the busy gate and the
// day plan.
func TestParseICalDayKeepsTheEveningInAZonedCalendar(t *testing.T) {
	plus3 := time.FixedZone("+03", 3*60*60)
	now := time.Date(2026, 8, 1, 12, 0, 0, 0, plus3)
	body := []byte("BEGIN:VCALENDAR\nBEGIN:VEVENT\n" +
		"DTSTART;TZID=Europe/Moscow:20260801T220000\nDTEND;TZID=Europe/Moscow:20260801T230000\n" +
		"SUMMARY:Evening call\nEND:VEVENT\nEND:VCALENDAR")

	events := ParseICalDay(body, now)
	if len(events) != 1 {
		t.Fatalf("got %d events, want the evening one", len(events))
	}
	if got := events[0].Start.In(plus3).Format("15:04"); got != "22:00" {
		t.Errorf("start reads %s locally, want 22:00", got)
	}
}

func TestBusyAndOverlapping(t *testing.T) {
	base := time.Date(2026, 7, 3, 0, 0, 0, 0, time.UTC)
	events := []Event{
		{Summary: "late", Start: base.Add(15 * time.Hour), End: base.Add(16 * time.Hour)},
		{Summary: "early", Start: base.Add(9 * time.Hour), End: base.Add(10 * time.Hour)},
	}
	if !Busy(events, base.Add(9*time.Hour+30*time.Minute)) {
		t.Error("should be busy inside the early event")
	}
	if Busy(events, base.Add(12*time.Hour)) {
		t.Error("should be free at noon")
	}
	// Half-open: the end instant is free.
	if Busy(events, base.Add(10*time.Hour)) {
		t.Error("the end instant should not count as busy")
	}
	got := Overlapping(events, base.Add(8*time.Hour), base.Add(11*time.Hour))
	if len(got) != 1 || got[0].Summary != "early" {
		t.Fatalf("Overlapping = %+v", got)
	}
	all := Overlapping(events, base, base.AddDate(0, 0, 1))
	if len(all) != 2 || all[0].Summary != "early" {
		t.Fatalf("Overlapping must sort by start: %+v", all)
	}
}

func TestSourceTrust(t *testing.T) {
	if ReadOnlySource(SourcePersonal) {
		t.Error("the personal calendar is the one maven may render to")
	}
	if !ReadOnlySource(SourceWork) {
		t.Error("the work calendar must be read-only")
	}
	if !ReadOnlySource(SourceAmbient) {
		t.Error("an ambient notification is not a writable calendar")
	}
	if AmbientConfidence >= 1.0 {
		t.Error("ambient events must be less trusted than a calendar read")
	}
	if len(Sources()) != 3 {
		t.Errorf("Sources() = %v", Sources())
	}
}

// Two Russian events on one day must not share a key. They did: safeKey kept
// ASCII only, so both summaries collapsed to their spaces and the second event
// overwrote the first in the store (Vikunja #443).
func TestFactKeyDistinguishesRussianEventsOnOneDay(t *testing.T) {
	day := time.Date(2026, 8, 4, 0, 0, 0, 0, time.UTC)
	a := Event{Summary: "Встреча с Аней", Start: day.Add(10 * time.Hour), End: day.Add(11 * time.Hour)}
	b := Event{Summary: "Обед с мамой", Start: day.Add(13 * time.Hour), End: day.Add(14 * time.Hour)}
	if FactKeyIn(a, time.UTC) == FactKeyIn(b, time.UTC) {
		t.Fatalf("both events keyed as %q", FactKeyIn(a, time.UTC))
	}
	// The day prefix still has to survive, because the store range-scans on it.
	if !strings.HasPrefix(FactKeyIn(a, time.UTC), KeyPrefixForDay(day)) {
		t.Fatalf("key %q lost the day prefix %q", FactKeyIn(a, time.UTC), KeyPrefixForDay(day))
	}
}
