package main

import (
	"context"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

// newFakeCalcomServer returns a httptest server whose handler is fn. The
// returned client is preconfigured to talk to it.
func newFakeCalcomServer(t *testing.T, fn http.HandlerFunc) (*httptest.Server, *CalcomClient) {
	t.Helper()
	srv := httptest.NewServer(fn)
	t.Cleanup(srv.Close)
	return srv, &CalcomClient{
		http:    srv.Client(),
		apiKey:  "test-key",
		baseURL: srv.URL,
	}
}

// TestGetSlots_URLAndHeaders verifies the request shape sent by GetSlots:
// path /v2/slots (or just /slots against our fake), correct query string, and
// the per-endpoint cal-api-version header.
func TestGetSlots_URLAndHeaders(t *testing.T) {
	var gotPath string
	var gotQuery string
	var gotHeaders http.Header
	srv, c := newFakeCalcomServer(t, func(w http.ResponseWriter, r *http.Request) {
		gotPath = r.URL.Path
		gotQuery = r.URL.RawQuery
		gotHeaders = r.Header
		_, _ = io.WriteString(w, `{"status":"success","data":{}}`)
	})
	_ = srv

	_, err := c.GetSlots(context.Background(),
		"alice", "30min",
		"2026-05-27T00:00:00Z", "2026-05-28T00:00:00Z",
		"America/Toronto")
	if err != nil {
		t.Fatalf("GetSlots returned error: %v", err)
	}

	if gotPath != "/slots" {
		t.Errorf("path: got %q, want /slots", gotPath)
	}
	for _, want := range []string{
		"username=alice",
		"eventTypeSlug=30min",
		"start=2026-05-27T00%3A00%3A00Z",
		"end=2026-05-28T00%3A00%3A00Z",
		"timeZone=America%2FToronto",
	} {
		if !strings.Contains(gotQuery, want) {
			t.Errorf("query missing %q (got: %q)", want, gotQuery)
		}
	}
	if got := gotHeaders.Get("cal-api-version"); got != versionSlots {
		t.Errorf("cal-api-version: got %q, want %q", got, versionSlots)
	}
	if got := gotHeaders.Get("Authorization"); got != "Bearer test-key" {
		t.Errorf("Authorization: got %q, want Bearer test-key", got)
	}
}

// TestGetSlots_ParsesResponse asserts the Cal.com nested-by-date shape is
// decoded into SlotsResponse.Data correctly.
func TestGetSlots_ParsesResponse(t *testing.T) {
	srv, c := newFakeCalcomServer(t, func(w http.ResponseWriter, _ *http.Request) {
		// cal-api-version 2024-09-04 shape: each slot carries `start` (not the
		// legacy `time` field). Millisecond precision mirrors the real wire.
		_, _ = io.WriteString(w, `{
			"status":"success",
			"data":{
				"2026-05-27":[{"start":"2026-05-27T13:00:00.000Z"},{"start":"2026-05-27T13:30:00.000Z"}],
				"2026-05-28":[{"start":"2026-05-28T10:00:00.000Z"}]
			}
		}`)
	})
	_ = srv

	got, err := c.GetSlots(context.Background(), "alice", "30min", "2026-05-27T00:00:00Z", "2026-05-29T00:00:00Z", "UTC")
	if err != nil {
		t.Fatalf("GetSlots: %v", err)
	}
	if len(got.Data["2026-05-27"]) != 2 {
		t.Errorf("expected 2 slots for 2026-05-27, got %d", len(got.Data["2026-05-27"]))
	}
	if got.Data["2026-05-27"][0].When() != "2026-05-27T13:00:00.000Z" {
		t.Errorf("first slot start mismatch: %q", got.Data["2026-05-27"][0].When())
	}
	if len(got.Data["2026-05-28"]) != 1 {
		t.Errorf("expected 1 slot for 2026-05-28, got %d", len(got.Data["2026-05-28"]))
	}
}

// TestGetSlots_LegacyTimeFieldFallback asserts that if Cal.com (or a rolled-back
// cal-api-version) replies with the older {"time":...} slot shape, SlotEntry.When()
// still resolves the timestamp. Guards the dual-shape decode that protects the
// widget from Cal.com's documented response-shape drift across API versions.
func TestGetSlots_LegacyTimeFieldFallback(t *testing.T) {
	srv, c := newFakeCalcomServer(t, func(w http.ResponseWriter, _ *http.Request) {
		_, _ = io.WriteString(w, `{"status":"success","data":{"2026-05-27":[{"time":"2026-05-27T13:00:00Z"}]}}`)
	})
	_ = srv

	got, err := c.GetSlots(context.Background(), "alice", "30min", "2026-05-27T00:00:00Z", "2026-05-28T00:00:00Z", "UTC")
	if err != nil {
		t.Fatalf("GetSlots: %v", err)
	}
	if len(got.Data["2026-05-27"]) != 1 {
		t.Fatalf("expected 1 slot, got %d", len(got.Data["2026-05-27"]))
	}
	if got.Data["2026-05-27"][0].When() != "2026-05-27T13:00:00Z" {
		t.Errorf("legacy time field not resolved by When(): %q", got.Data["2026-05-27"][0].When())
	}
}

// TestListEventTypes_HeaderAndParsing asserts the event-types header and the
// EventTypeSummary parsing including lengthInMinutes.
func TestListEventTypes_HeaderAndParsing(t *testing.T) {
	var gotHeaders http.Header
	srv, c := newFakeCalcomServer(t, func(w http.ResponseWriter, r *http.Request) {
		gotHeaders = r.Header
		_, _ = io.WriteString(w, `{
			"status":"success",
			"data":[
				{"id":1,"slug":"30min","title":"30 Minute Meeting","lengthInMinutes":30},
				{"id":2,"slug":"60min","title":"60 Minute Meeting","lengthInMinutes":60}
			]
		}`)
	})
	_ = srv

	ets, err := c.ListEventTypes(context.Background(), "alice")
	if err != nil {
		t.Fatalf("ListEventTypes: %v", err)
	}
	if got := gotHeaders.Get("cal-api-version"); got != versionEventTypes {
		t.Errorf("cal-api-version: got %q, want %q", got, versionEventTypes)
	}
	if len(ets) != 2 {
		t.Fatalf("expected 2 event types, got %d", len(ets))
	}
	if ets[0].LengthInMinutes != 30 {
		t.Errorf("lengthInMinutes not parsed: %+v", ets[0])
	}
	if ets[1].Slug != "60min" {
		t.Errorf("second slug wrong: %+v", ets[1])
	}
}

// TestGetEventType_BookingFieldsParsing verifies the full EventType shape
// including custom bookingFields with options is parsed correctly.
func TestGetEventType_BookingFieldsParsing(t *testing.T) {
	srv, c := newFakeCalcomServer(t, func(w http.ResponseWriter, _ *http.Request) {
		_, _ = io.WriteString(w, `{
			"status":"success",
			"data":[{
				"id":42,
				"slug":"intake",
				"title":"Intake Call",
				"lengthInMinutes":15,
				"bookingFields":[
					{"slug":"name","label":"Your name","type":"name","required":true},
					{"slug":"company_size","label":"Company size","type":"select","required":true,
					 "options":[{"label":"1-10","value":"small"},{"label":"11-50","value":"medium"}]}
				]
			}]
		}`)
	})
	_ = srv

	et, err := c.GetEventType(context.Background(), "alice", "intake")
	if err != nil {
		t.Fatalf("GetEventType: %v", err)
	}
	if et.ID != 42 {
		t.Errorf("ID: got %d, want 42", et.ID)
	}
	if len(et.BookingFields) != 2 {
		t.Fatalf("expected 2 booking fields, got %d", len(et.BookingFields))
	}
	if et.BookingFields[1].Type != "select" || len(et.BookingFields[1].Options) != 2 {
		t.Errorf("select field options not parsed: %+v", et.BookingFields[1])
	}
	if et.BookingFields[1].Options[0].Value != "small" {
		t.Errorf("first option value wrong: %+v", et.BookingFields[1].Options[0])
	}
}

// TestGetEventType_RealV2ShapeNormalizesFields pins the ACTUAL Cal.com v2
// /event-types bookingFields shape (cal-api-version 2024-06-14), which differs
// from what the plugin originally assumed:
//
//   - the field identifier is "slug", NOT "name"
//   - default/system fields return an empty "label" (Cal.com's own booker
//     derives the visible label client-side from i18n keyed on the field type)
//   - some system fields are "hidden":true and must not be rendered
//
// Decoding this with the old struct produced inputs with name="" and blank
// labels — invisible AND unsubmittable. GetEventType must normalize:
// slug→Name, a resolved label for system fields, and drop hidden fields.
func TestGetEventType_RealV2ShapeNormalizesFields(t *testing.T) {
	srv, c := newFakeCalcomServer(t, func(w http.ResponseWriter, _ *http.Request) {
		_, _ = io.WriteString(w, `{
			"status":"success",
			"data":[{
				"id":7,
				"slug":"30min",
				"title":"30 Min Meeting",
				"lengthInMinutes":30,
				"bookingFields":[
					{"type":"name","slug":"name","required":true,"label":"","isDefault":true},
					{"type":"email","slug":"email","required":true,"label":"","isDefault":true},
					{"type":"phone","slug":"attendeePhoneNumber","required":true,"label":"","placeholder":"","isDefault":true},
					{"type":"textarea","slug":"notes","required":false,"label":"","isDefault":true},
					{"type":"text","slug":"company","required":false,"label":"Company name","isDefault":false},
					{"type":"text","slug":"title","required":false,"label":"","hidden":true,"isDefault":true}
				]
			}]
		}`)
	})
	_ = srv

	et, err := c.GetEventType(context.Background(), "bidbuddy", "30min")
	if err != nil {
		t.Fatalf("GetEventType: %v", err)
	}

	// The hidden "title" system field must be dropped.
	if len(et.BookingFields) != 5 {
		t.Fatalf("expected 5 visible booking fields (hidden dropped), got %d: %+v", len(et.BookingFields), et.BookingFields)
	}

	want := []struct {
		name  string
		label string
	}{
		{"name", "Name"},
		{"email", "Email"},
		{"attendeePhoneNumber", "Phone number"},
		{"notes", "Additional notes"},
		{"company", "Company name"}, // custom field: explicit label preserved
	}
	for i, w := range want {
		got := et.BookingFields[i]
		if got.Name != w.name {
			t.Errorf("field %d Name: got %q, want %q (slug must populate Name)", i, got.Name, w.name)
		}
		if got.Label != w.label {
			t.Errorf("field %d Label: got %q, want %q", i, got.Label, w.label)
		}
	}
}

// TestGetEventType_NotFound asserts a descriptive error is returned when the
// event type isn't found in the response.
func TestGetEventType_NotFound(t *testing.T) {
	srv, c := newFakeCalcomServer(t, func(w http.ResponseWriter, _ *http.Request) {
		_, _ = io.WriteString(w, `{"status":"success","data":[]}`)
	})
	_ = srv

	_, err := c.GetEventType(context.Background(), "alice", "nope")
	if err == nil {
		t.Fatalf("expected error for missing event type")
	}
	if !strings.Contains(err.Error(), "not found") {
		t.Errorf("expected 'not found' in error, got: %v", err)
	}
}

// TestCreateBooking_RequestShape verifies the WO-008 body contract:
// uses eventTypeSlug + username (no eventTypeId); attendee has timeZone;
// bookingFieldsResponses carries custom fields; cal-api-version header is the
// bookings-specific 2026-02-25.
func TestCreateBooking_RequestShape(t *testing.T) {
	var gotBody []byte
	var gotHeaders http.Header
	srv, c := newFakeCalcomServer(t, func(w http.ResponseWriter, r *http.Request) {
		gotBody, _ = io.ReadAll(r.Body)
		gotHeaders = r.Header
		_, _ = io.WriteString(w, `{"status":"success","data":{"uid":"abc","rescheduleUrl":"https://cal.com/reschedule/abc","cancelUrl":"https://cal.com/booking/abc?cancel=true"}}`)
	})
	_ = srv

	_, err := c.CreateBooking(context.Background(), BookingRequest{
		EventTypeSlug: "30min",
		Username:      "alice",
		Start:         "2026-05-27T10:00:00Z",
		Attendee: BookingAttendee{
			Name: "Bob", Email: "bob@example.com", TimeZone: "America/Toronto",
		},
		BookingFieldsResponses: map[string]any{"q1": "yes"},
	})
	if err != nil {
		t.Fatalf("CreateBooking: %v", err)
	}
	if got := gotHeaders.Get("cal-api-version"); got != versionBookings {
		t.Errorf("cal-api-version: got %q, want %q", got, versionBookings)
	}

	var parsed map[string]any
	if err := json.Unmarshal(gotBody, &parsed); err != nil {
		t.Fatalf("decode body: %v", err)
	}
	if parsed["eventTypeSlug"] != "30min" {
		t.Errorf("eventTypeSlug missing/wrong: %v", parsed["eventTypeSlug"])
	}
	if parsed["username"] != "alice" {
		t.Errorf("username missing/wrong: %v", parsed["username"])
	}
	if _, present := parsed["eventTypeId"]; present {
		t.Errorf("body must NOT contain eventTypeId (old shape); body=%s", string(gotBody))
	}
	att, _ := parsed["attendee"].(map[string]any)
	if att["timeZone"] != "America/Toronto" {
		t.Errorf("attendee.timeZone wrong: %v", att["timeZone"])
	}
	resps, _ := parsed["bookingFieldsResponses"].(map[string]any)
	if resps["q1"] != "yes" {
		t.Errorf("bookingFieldsResponses.q1 wrong: %v", resps)
	}
}

// TestCreateBooking_ParsesLocationAndUID verifies the 2026-02-25 response: the
// booking UID and the canonical `location` meeting URL parse, and MeetingLink()
// resolves to `location`. The response carries no reschedule/cancel URLs, so
// those fields were intentionally removed from BookingResponseData.
func TestCreateBooking_ParsesLocationAndUID(t *testing.T) {
	srv, c := newFakeCalcomServer(t, func(w http.ResponseWriter, _ *http.Request) {
		_, _ = io.WriteString(w, `{"status":"success","data":{
			"id":123,
			"uid":"xyz",
			"start":"2026-05-27T10:00:00Z",
			"end":"2026-05-27T10:30:00Z",
			"location":"https://meet.example.com/xyz"
		}}`)
	})
	_ = srv

	resp, err := c.CreateBooking(context.Background(), BookingRequest{
		EventTypeSlug: "30min", Username: "alice", Start: "2026-05-27T10:00:00Z",
		Attendee: BookingAttendee{Name: "Bob", Email: "b@x.com", TimeZone: "UTC"},
	})
	if err != nil {
		t.Fatalf("CreateBooking: %v", err)
	}
	if resp.Data.UID != "xyz" {
		t.Errorf("UID: got %q, want xyz", resp.Data.UID)
	}
	if resp.Data.MeetingLink() != "https://meet.example.com/xyz" {
		t.Errorf("MeetingLink: got %q, want the `location` URL", resp.Data.MeetingLink())
	}
}

// TestBookingMeetingLink_Resolution covers MeetingLink()'s field precedence and
// the URL guard that prevents a physical-address/phone `location` (used by
// non-video event types) from being surfaced as a clickable link.
func TestBookingMeetingLink_Resolution(t *testing.T) {
	cases := []struct {
		name string
		data BookingResponseData
		want string
	}{
		{"location url preferred", BookingResponseData{Location: "https://a/x", MeetingURL: "https://b/y"}, "https://a/x"},
		{"falls back to meetingUrl", BookingResponseData{Location: "", MeetingURL: "https://b/y"}, "https://b/y"},
		{"non-url location is not a link", BookingResponseData{Location: "123 Main St, Perth", MeetingURL: ""}, ""},
		{"non-url location falls back to meetingUrl", BookingResponseData{Location: "+61 8 1234 5678", MeetingURL: "https://b/y"}, "https://b/y"},
		{"nothing", BookingResponseData{}, ""},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			if got := tc.data.MeetingLink(); got != tc.want {
				t.Errorf("MeetingLink() = %q, want %q", got, tc.want)
			}
		})
	}
}

// TestCreateBooking_RecurringArrayData asserts that a recurring event type —
// whose response `data` is an ARRAY of occurrences — no longer hard-fails the
// booking. The first occurrence is surfaced (its UID + location).
func TestCreateBooking_RecurringArrayData(t *testing.T) {
	srv, c := newFakeCalcomServer(t, func(w http.ResponseWriter, _ *http.Request) {
		_, _ = io.WriteString(w, `{"status":"success","data":[
			{"uid":"occ-1","start":"2026-05-27T10:00:00Z","location":"https://meet.example.com/occ-1"},
			{"uid":"occ-2","start":"2026-06-03T10:00:00Z","location":"https://meet.example.com/occ-2"}
		]}`)
	})
	_ = srv

	resp, err := c.CreateBooking(context.Background(), BookingRequest{
		EventTypeSlug: "weekly", Username: "alice", Start: "2026-05-27T10:00:00Z",
		Attendee: BookingAttendee{Name: "Bob", Email: "b@x.com", TimeZone: "UTC"},
	})
	if err != nil {
		t.Fatalf("CreateBooking (recurring): %v", err)
	}
	if resp.Data.UID != "occ-1" {
		t.Errorf("recurring booking should surface first occurrence UID; got %q", resp.Data.UID)
	}
	if resp.Data.MeetingLink() != "https://meet.example.com/occ-1" {
		t.Errorf("recurring MeetingLink: got %q", resp.Data.MeetingLink())
	}
}

// TestCancelBooking_RequestShape verifies CancelBooking sends the confirmed v2
// shape: POST /bookings/{uid}/cancel with the bookings cal-api-version, the
// Bearer token, and a {"cancellationReason": ...} body. The uid must be
// URL-path-escaped so a path-significant character can't break out of the route.
func TestCancelBooking_RequestShape(t *testing.T) {
	var gotMethod, gotEscapedPath string
	var gotHeaders http.Header
	var gotBody []byte
	srv, c := newFakeCalcomServer(t, func(w http.ResponseWriter, r *http.Request) {
		gotMethod = r.Method
		// EscapedPath() preserves the on-the-wire escaping (r.URL.Path is decoded
		// by the server, hiding whether the uid was escaped). This is what proves
		// the uid was path-escaped and couldn't break out of the route.
		gotEscapedPath = r.URL.EscapedPath()
		gotHeaders = r.Header
		gotBody, _ = io.ReadAll(r.Body)
		_, _ = io.WriteString(w, `{"status":"success","data":{"id":1,"uid":"abc","status":"cancelled"}}`)
	})
	_ = srv

	// A uid with a path-significant character (slash) must be escaped into a
	// single path segment, not split the route.
	if err := c.CancelBooking(context.Background(), "uid/with space", "Cancelled by attendee"); err != nil {
		t.Fatalf("CancelBooking: %v", err)
	}
	if gotMethod != http.MethodPost {
		t.Errorf("method: got %q, want POST", gotMethod)
	}
	if gotEscapedPath != "/bookings/uid%2Fwith%20space/cancel" {
		t.Errorf("escaped path: got %q, want /bookings/uid%%2Fwith%%20space/cancel (uid path-escaped)", gotEscapedPath)
	}
	if got := gotHeaders.Get("cal-api-version"); got != versionBookings {
		t.Errorf("cal-api-version: got %q, want %q", got, versionBookings)
	}
	if got := gotHeaders.Get("Authorization"); got != "Bearer test-key" {
		t.Errorf("Authorization: got %q, want Bearer test-key", got)
	}
	var parsed map[string]any
	if err := json.Unmarshal(gotBody, &parsed); err != nil {
		t.Fatalf("decode body: %v", err)
	}
	if parsed["cancellationReason"] != "Cancelled by attendee" {
		t.Errorf("cancellationReason: got %v, want %q", parsed["cancellationReason"], "Cancelled by attendee")
	}
}

// TestCancelBooking_NonOKReturnsAPIError confirms a non-2xx cancel response
// surfaces as a *CalcomAPIError carrying the status (so the handler can classify
// it / log it) and is never silently swallowed.
func TestCancelBooking_NonOKReturnsAPIError(t *testing.T) {
	srv, c := newFakeCalcomServer(t, func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusNotFound)
		_, _ = io.WriteString(w, `{"error":{"code":"NotFoundException","message":"Booking not found"}}`)
	})
	_ = srv

	err := c.CancelBooking(context.Background(), "missing-uid", "reason")
	if err == nil {
		t.Fatalf("expected error from 404 cancel response")
	}
	var apiErr *CalcomAPIError
	if !errors.As(err, &apiErr) {
		t.Fatalf("expected *CalcomAPIError, got %T: %v", err, err)
	}
	if apiErr.Status != http.StatusNotFound {
		t.Errorf("status: got %d, want 404", apiErr.Status)
	}
	if !strings.Contains(err.Error(), "404") {
		t.Errorf("error should mention status code, got: %v", err)
	}
}

// TestCancelBooking_OmitsReasonWhenEmpty confirms an empty reason still sends a
// well-formed body (cancellationReason is optional per the v2 docs; we always
// send the field, empty when no reason is supplied).
func TestCancelBooking_AcceptsEmptyReason(t *testing.T) {
	var gotBody []byte
	srv, c := newFakeCalcomServer(t, func(w http.ResponseWriter, r *http.Request) {
		gotBody, _ = io.ReadAll(r.Body)
		_, _ = io.WriteString(w, `{"status":"success","data":{"status":"cancelled"}}`)
	})
	_ = srv

	if err := c.CancelBooking(context.Background(), "abc", ""); err != nil {
		t.Fatalf("CancelBooking with empty reason: %v", err)
	}
	if !strings.Contains(string(gotBody), `"cancellationReason":""`) {
		t.Errorf("body should carry an (empty) cancellationReason field; got %s", string(gotBody))
	}
}

// TestClient_ErrorResponseSanitization confirms 4xx responses surface as
// errors (not panics) and that the error string includes the status code so
// callers can branch on it.
func TestClient_ErrorResponseSanitization(t *testing.T) {
	srv, c := newFakeCalcomServer(t, func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusUnauthorized)
		_, _ = io.WriteString(w, `{"error":"unauthorized","message":"Invalid API key"}`)
	})
	_ = srv

	_, err := c.GetSlots(context.Background(), "alice", "30min", "2026-05-27T00:00:00Z", "2026-05-28T00:00:00Z", "UTC")
	if err == nil {
		t.Fatalf("expected error from 401 response")
	}
	if !strings.Contains(err.Error(), "401") {
		t.Errorf("error should mention status code, got: %v", err)
	}
}

// TestClient_HandlesNetworkFailure ensures a connection-level error surfaces
// without panicking.
func TestClient_HandlesNetworkFailure(t *testing.T) {
	c := &CalcomClient{http: &http.Client{}, apiKey: "k", baseURL: "http://127.0.0.1:1"}
	_, err := c.GetSlots(context.Background(), "alice", "30min", "x", "y", "UTC")
	if err == nil {
		t.Fatalf("expected error from unreachable server")
	}
}

// Compile-time guard: ensure SlotEntry retains BOTH timestamp fields. Catches
// accidental renames during refactors — the dual shape (2024-09-04 `start` plus
// legacy `time`) is what keeps the widget resilient to Cal.com version drift.
var _ = SlotEntry{Start: "", Time: ""}

// Compile-time guard for the legacy fmt import in test diagnostics.
var _ = ""

// TestGetEventType_SendsEventSlugQuery verifies that GetEventType sends BOTH
// `username=` and `eventSlug=` as query params. The existing booking-fields
// parsing test (TestGetEventType_BookingFieldsParsing) does not capture the
// outbound URL — without this assertion the client could silently regress to
// omitting one or both params and the broken request would still parse the
// stub response in tests.
func TestGetEventType_SendsEventSlugQuery(t *testing.T) {
	var gotQuery string
	srv, c := newFakeCalcomServer(t, func(w http.ResponseWriter, r *http.Request) {
		gotQuery = r.URL.RawQuery
		_, _ = io.WriteString(w, `{"status":"success","data":[{"id":1,"slug":"intake","title":"x","lengthInMinutes":15,"bookingFields":[]}]}`)
	})
	_ = srv

	if _, err := c.GetEventType(context.Background(), "alice", "intake"); err != nil {
		t.Fatalf("GetEventType: %v", err)
	}
	if !strings.Contains(gotQuery, "eventSlug=intake") {
		t.Errorf("outbound query missing eventSlug=intake (got: %q)", gotQuery)
	}
	if !strings.Contains(gotQuery, "username=alice") {
		t.Errorf("outbound query missing username=alice (got: %q)", gotQuery)
	}
}

// TestListEventTypes_SendsUsernameQuery verifies that ListEventTypes propagates
// the username argument to Cal.com as a query param. Without this, the editor
// dropdown would silently fetch the API-key owner's own event types instead of
// the user the admin selected.
func TestListEventTypes_SendsUsernameQuery(t *testing.T) {
	var gotQuery string
	srv, c := newFakeCalcomServer(t, func(w http.ResponseWriter, r *http.Request) {
		gotQuery = r.URL.RawQuery
		_, _ = io.WriteString(w, `{"status":"success","data":[]}`)
	})
	_ = srv

	if _, err := c.ListEventTypes(context.Background(), "alice"); err != nil {
		t.Fatalf("ListEventTypes: %v", err)
	}
	if !strings.Contains(gotQuery, "username=alice") {
		t.Errorf("outbound query missing username=alice (got: %q)", gotQuery)
	}
}

// TestGetMe_URLAndHeaders verifies the request shape sent by GetMe:
// path /me, no query string, and the cal-api-version header for the /me endpoint.
func TestGetMe_URLAndHeaders(t *testing.T) {
	var gotPath string
	var gotHeaders http.Header
	srv, c := newFakeCalcomServer(t, func(w http.ResponseWriter, r *http.Request) {
		gotPath = r.URL.Path
		gotHeaders = r.Header
		_, _ = io.WriteString(w, `{"status":"success","data":{"username":"alice","email":"alice@example.com"}}`)
	})
	_ = srv

	_, err := c.GetMe(context.Background())
	if err != nil {
		t.Fatalf("GetMe returned error: %v", err)
	}
	if gotPath != "/me" {
		t.Errorf("path: got %q, want /me", gotPath)
	}
	if got := gotHeaders.Get("cal-api-version"); got != versionMe {
		t.Errorf("cal-api-version: got %q, want %q", got, versionMe)
	}
	if got := gotHeaders.Get("Authorization"); got != "Bearer test-key" {
		t.Errorf("Authorization: got %q, want Bearer test-key", got)
	}
}

// TestGetMe_ParsesResponse confirms the {status, data:{username, email}} envelope
// is decoded into MeResponse correctly.
func TestGetMe_ParsesResponse(t *testing.T) {
	srv, c := newFakeCalcomServer(t, func(w http.ResponseWriter, _ *http.Request) {
		_, _ = io.WriteString(w, `{"status":"success","data":{"username":"alice","email":"alice@example.com"}}`)
	})
	_ = srv

	me, err := c.GetMe(context.Background())
	if err != nil {
		t.Fatalf("GetMe: %v", err)
	}
	if me.Username != "alice" {
		t.Errorf("Username: got %q, want alice", me.Username)
	}
	if me.Email != "alice@example.com" {
		t.Errorf("Email: got %q, want alice@example.com", me.Email)
	}
}

// TestGetMe_BubblesUpstreamError ensures a non-2xx Cal.com response surfaces
// as an error from GetMe rather than a zero-value MeResponse.
func TestGetMe_BubblesUpstreamError(t *testing.T) {
	srv, c := newFakeCalcomServer(t, func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusUnauthorized)
		_, _ = io.WriteString(w, `{"message":"invalid api key"}`)
	})
	_ = srv

	_, err := c.GetMe(context.Background())
	if err == nil {
		t.Fatal("expected error on 401, got nil")
	}
	if !strings.Contains(err.Error(), "401") {
		t.Errorf("error should mention 401, got: %v", err)
	}
}
