package main

import (
	"context"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"
)

// These tests pin the calendar-grid layout contract, written against the
// second bidbuddy.com.au report (2026-06-10, 20:59): the grid renders fixed
// Sun–Sat column headers but poured consecutive day buttons in from column 1
// with no offset, so June 10 (a Wednesday) sat under "Sun" and tapping the
// cell under "Tue" (showing 12) selected Friday, June 12. The first date must
// be preceded by int(weekday) spacer cells so every button lands under its
// real weekday header. "Today" (window start + IsPast/IsToday flags) must be
// resolved in the visitor/site timezone, not the server clock's.

// divergentZone returns an IANA zone whose current calendar date differs from
// the server-local date right now. Pacific/Kiritimati (UTC+14) and
// Pacific/Pago_Pago (UTC-11) are 25 hours apart, so at any instant at least
// one of them is on a different date than the server.
func divergentZone(t *testing.T) (*time.Location, string) {
	t.Helper()
	serverToday := time.Now().Format("2006-01-02")
	for _, name := range []string{"Pacific/Kiritimati", "Pacific/Pago_Pago"} {
		loc, err := time.LoadLocation(name)
		if err != nil {
			t.Fatalf("LoadLocation(%s): %v", name, err)
		}
		if time.Now().In(loc).Format("2006-01-02") != serverToday {
			return loc, name
		}
	}
	t.Fatal("no zone diverges from server-local date — zones are 25h apart, impossible")
	return nil, ""
}

// firstDateParam extracts the YYYY-MM-DD value of the first date= query param
// in rendered HTML — i.e. the first day button of the grid.
func firstDateParam(t *testing.T, html string) string {
	t.Helper()
	i := strings.Index(html, "date=")
	if i < 0 || i+15 > len(html) {
		t.Fatalf("no date= param found in rendered HTML")
	}
	return html[i+5 : i+15]
}

func TestLeadingBlanks(t *testing.T) {
	cell := func(date string) DateCell { return DateCell{Date: date} }
	cases := []struct {
		name  string
		dates []DateCell
		want  int
	}{
		{"wednesday start needs 3 pads", []DateCell{cell("2026-06-10")}, 3},
		{"sunday start needs none", []DateCell{cell("2026-06-14")}, 0},
		{"saturday start needs 6", []DateCell{cell("2026-06-13")}, 6},
		{"empty grid", nil, 0},
		{"unparseable date", []DateCell{cell("garbage")}, 0},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if got := leadingBlanks(tc.dates); got != tc.want {
				t.Errorf("leadingBlanks(%v) = %d, want %d", tc.dates, got, tc.want)
			}
		})
	}
}

// TestHandleGetDateGrid_AlignsFirstDateToWeekdayColumn drives /date-grid with
// a future Wednesday weekStart and asserts exactly 3 spacer cells render
// before the first day button, so the 7-column grid lines dates up with the
// Sun–Sat headers.
func TestHandleGetDateGrid_AlignsFirstDateToWeekdayColumn(t *testing.T) {
	router := newTestRouter(t)

	// Next Wednesday at least a week out, so it is never clamped to today.
	start := time.Now().AddDate(0, 0, 7)
	for start.Weekday() != time.Wednesday {
		start = start.AddDate(0, 0, 1)
	}
	req := httptest.NewRequest(http.MethodGet,
		"/date-grid?blockId=b-align&username=alice&eventType=30min&weeks=2&weekStart="+start.Format("2006-01-02"), nil)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d (body: %s)", rec.Code, rec.Body.String())
	}
	body := rec.Body.String()

	if got := strings.Count(body, "calcom-date-pad"); got != 3 {
		t.Errorf("Wednesday window: want 3 leading pad cells, got %d", got)
	}
	padIdx := strings.Index(body, "calcom-date-pad")
	btnIdx := strings.Index(body, "date=")
	if padIdx < 0 || btnIdx < 0 || padIdx > btnIdx {
		t.Errorf("pad cells must precede the first day button (padIdx=%d, btnIdx=%d)", padIdx, btnIdx)
	}
	if !strings.Contains(body, `calcom-date-pad" aria-hidden="true"`) {
		t.Errorf("pad cells must be aria-hidden")
	}
}

// TestHandleGetDateGrid_SundayStartHasNoPads is the zero case: a window
// starting on a Sunday begins flush in column 1.
func TestHandleGetDateGrid_SundayStartHasNoPads(t *testing.T) {
	router := newTestRouter(t)

	start := time.Now().AddDate(0, 0, 7)
	for start.Weekday() != time.Sunday {
		start = start.AddDate(0, 0, 1)
	}
	req := httptest.NewRequest(http.MethodGet,
		"/date-grid?blockId=b-sun&username=alice&eventType=30min&weeks=2&weekStart="+start.Format("2006-01-02"), nil)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if got := strings.Count(rec.Body.String(), "calcom-date-pad"); got != 0 {
		t.Errorf("Sunday window: want 0 pad cells, got %d", got)
	}
}

// TestHandleGetDateGrid_TodayResolvedInEventTimezone pins the clamp rule to
// the MEETING's calendar, not the server's: a past weekStart must clamp to
// today in the event's schedule timezone. Pre-fix the handler clamped to the
// server-local date, so a UTC container made a Perth schedule's grid start on
// yesterday until 08:00 AWST.
func TestHandleGetDateGrid_TodayResolvedInEventTimezone(t *testing.T) {
	loc, name := divergentZone(t)
	fakeCalcom(t, name, nil)
	h, _ := newKeyedHandler(t)
	wantToday := time.Now().In(loc).Format("2006-01-02")

	req := httptest.NewRequest(http.MethodGet,
		"/date-grid?blockId=b-tz&username=alice&eventType=30min&weeks=2&weekStart=2020-01-01", nil)
	rec := httptest.NewRecorder()
	h.HandleGetDateGrid(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d (body: %s)", rec.Code, rec.Body.String())
	}
	if got := firstDateParam(t, rec.Body.String()); got != wantToday {
		t.Errorf("clamped window must start at today in %s (%s), got %s", name, wantToday, got)
	}
}

// TestCalcomBookingFunc_InitialGridUsesSiteTimezone covers the initial
// server-side block render with a cold event-timezone cache: the window must
// start at today in the SITE timezone (Site Settings → Localization),
// resolved via the settings manager stashed at router-mount time — not at
// the server clock's date. Page render never calls Cal.com.
func TestCalcomBookingFunc_InitialGridUsesSiteTimezone(t *testing.T) {
	s, q := newTestSettings()
	loc, name := divergentZone(t)
	seedSiteTimezone(q, name)
	renderSettings = s
	resetEventTZCache()
	t.Cleanup(func() { renderSettings = nil })

	html := CalcomBookingFunc(context.Background(), map[string]any{
		"_block_id":     "b-init",
		"username":      "alice",
		"eventTypeSlug": "30min",
	})

	wantToday := time.Now().In(loc).Format("2006-01-02")
	if got := firstDateParam(t, html); got != wantToday {
		t.Errorf("initial grid must start at today in site tz %s (%s), got %s", name, wantToday, got)
	}
	wantPads := int(time.Now().In(loc).Weekday())
	if got := strings.Count(html, "calcom-date-pad"); got != wantPads {
		t.Errorf("initial grid: want %d pad cells for %s, got %d", wantPads, wantToday, got)
	}
}

// TestCalcomBookingFunc_InitialGridPrefersCachedEventTimezone asserts the
// initial render uses the meeting timezone over the site setting once the
// cache is warm (any visitor's first HTMX interaction warms it).
func TestCalcomBookingFunc_InitialGridPrefersCachedEventTimezone(t *testing.T) {
	s, q := newTestSettings()
	loc, name := divergentZone(t)
	seedSiteTimezone(q, "UTC") // site setting present but unset-equivalent
	storeEventTZ("alice", "30min", name)
	renderSettings = s
	t.Cleanup(func() {
		renderSettings = nil
		resetEventTZCache()
	})

	html := CalcomBookingFunc(context.Background(), map[string]any{
		"_block_id":     "b-init2",
		"username":      "alice",
		"eventTypeSlug": "30min",
	})

	wantToday := time.Now().In(loc).Format("2006-01-02")
	if got := firstDateParam(t, html); got != wantToday {
		t.Errorf("initial grid must start at today in cached event tz %s (%s), got %s", name, wantToday, got)
	}
	if !strings.Contains(html, name) {
		t.Errorf("ShowTimezone label should carry the event tz %s", name)
	}
}
