package main

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"sync"
	"sync/atomic"
	"testing"

	"git.dev.alexdunmow.com/block/calcomblock/internal/db"
	"git.dev.alexdunmow.com/block/calcomblock/internal/helpers"
	"github.com/jackc/pgx/v5/pgconn"
)

// fakeQuerier is an in-memory implementation of helpers.PluginSettingsQuerier
// used by tests so SettingsManager can persist and read settings without a DB.
type fakeQuerier struct {
	mu   sync.Mutex
	data map[string][]byte
}

func newFakeQuerier() *fakeQuerier {
	return &fakeQuerier{data: map[string][]byte{}}
}

func (f *fakeQuerier) GetSetting(_ context.Context, key string) (db.Setting, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	v, ok := f.data[key]
	if !ok {
		return db.Setting{}, &pgconn.PgError{Code: "P0002", Message: "no rows"}
	}
	return db.Setting{Key: key, Value: v}, nil
}

func (f *fakeQuerier) UpsertSetting(_ context.Context, arg db.UpsertSettingParams) (db.Setting, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.data[arg.Key] = arg.Value
	return db.Setting(arg), nil
}

// newTestSettings builds a SettingsManager backed by an in-memory querier and
// a real crypto with a deterministic key — suitable for handler tests.
func newTestSettings() (*SettingsManager, *fakeQuerier) {
	q := newFakeQuerier()
	c := helpers.NewPluginCrypto("test-secret-key-for-unit-test!!")
	return NewSettingsManager(c, q), q
}

// newTestHandler builds a CalcomHandler wired with an isolated rate limiter
// — handy for tests that need to drive HandleCreateBooking.
func newTestHandler() (*CalcomHandler, *fakeQuerier) {
	s, q := newTestSettings()
	return NewCalcomHandler(s, NewRateLimiter(bookingsPerHourPerIP), "https://test.blockninja.dev"), q
}

// TestWriteServerError_DoesNotLeakInternalError verifies that writeServerError
// returns the public message to the HTTP client and does NOT expose the raw
// internal error string. This is the canonical behaviour required for the
// HandleSaveSettings error branch.
func TestWriteServerError_DoesNotLeakInternalError(t *testing.T) {
	internalErrMsg := "secret connection string dsn=postgres://user:pass@db/internal"
	internalErr := errors.New(internalErrMsg)

	rec := httptest.NewRecorder()
	writeServerError(rec, internalErr, "Failed to save settings", "calcomblock.HandleSaveSettings")

	if rec.Code != http.StatusInternalServerError {
		t.Errorf("expected status 500, got %d", rec.Code)
	}

	responseBody := rec.Body.String()

	if !strings.Contains(responseBody, "Failed to save settings") {
		t.Errorf("expected response to contain \"Failed to save settings\", got: %q", responseBody)
	}

	if strings.Contains(responseBody, internalErrMsg) {
		t.Errorf("response leaks internal error detail: %q", responseBody)
	}

	for _, sensitiveFragment := range []string{"postgres://", "user:pass", "dsn=", "internal"} {
		if strings.Contains(responseBody, sensitiveFragment) {
			t.Errorf("response leaks sensitive fragment %q in body: %q", sensitiveFragment, responseBody)
		}
	}
}

// TestWriteInternalServerError_ReturnsGenericMessage verifies that
// writeInternalServerError returns "Internal Server Error" (not the raw err).
func TestWriteInternalServerError_ReturnsGenericMessage(t *testing.T) {
	internalErr := errors.New("secret internal detail: file=/etc/secret key=abc123")

	rec := httptest.NewRecorder()
	writeInternalServerError(rec, internalErr, "calcomblock.SomeHandler")

	if rec.Code != http.StatusInternalServerError {
		t.Errorf("expected status 500, got %d", rec.Code)
	}

	responseBody := rec.Body.String()

	if !strings.Contains(responseBody, "Internal Server Error") {
		t.Errorf("expected \"Internal Server Error\" in body, got: %q", responseBody)
	}

	if strings.Contains(responseBody, "secret internal detail") {
		t.Errorf("response leaks internal error: %q", responseBody)
	}
}

// TestHandleSaveSettings_SuccessPath verifies the success branch works correctly.
func TestHandleSaveSettings_SuccessPath(t *testing.T) {
	h, _ := newTestHandler()

	body, _ := json.Marshal(map[string]string{"api_key": "cal_live_abc123"})
	req := httptest.NewRequest(http.MethodPost, "/settings", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()

	h.HandleSaveSettings(rec, req)

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

	var resp map[string]any
	if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
		t.Fatalf("failed to decode response: %v", err)
	}
	if success, _ := resp["success"].(bool); !success {
		t.Errorf("expected success=true in response, got: %v", resp)
	}
}

// TestHandleSaveSettings_InvalidBody verifies that a malformed request body
// returns 400 Bad Request.
func TestHandleSaveSettings_InvalidBody(t *testing.T) {
	h, _ := newTestHandler()

	req := httptest.NewRequest(http.MethodPost, "/settings", strings.NewReader("{bad json}"))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()

	h.HandleSaveSettings(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Errorf("expected status 400, got %d", rec.Code)
	}
}

// TestSettingsManager_APIKeyPersistsAcrossInstances ensures a new SettingsManager
// backed by the same querier still sees a previously stored API key — verifying
// the rewrite genuinely persists rather than holding the value in memory.
func TestSettingsManager_APIKeyPersistsAcrossInstances(t *testing.T) {
	q := newFakeQuerier()
	c := helpers.NewPluginCrypto("test-secret-key-for-unit-test!!")
	a := NewSettingsManager(c, q)

	if err := a.SetAPIKey(context.Background(), "cal_live_persisted"); err != nil {
		t.Fatalf("SetAPIKey: %v", err)
	}

	// Build a fresh manager over the same querier — simulates server restart.
	b := NewSettingsManager(c, q)
	got, err := b.GetAPIKey(context.Background())
	if err != nil {
		t.Fatalf("GetAPIKey: %v", err)
	}
	if got != "cal_live_persisted" {
		t.Errorf("expected persisted key 'cal_live_persisted', got %q", got)
	}
}

// ---------------------------------------------------------------------------
// WO-043 — HandleGetSlots error paths (no Cal.com call needed for these paths)
// ---------------------------------------------------------------------------

// TestHandleGetSlots_MissingParams verifies that a GET request missing required
// params (username, eventType, date) causes HandleGetSlots to render an HTML
// error rather than attempt any Cal.com request. The apiKey must be set so the
// handler reaches the param-check logic.
func TestHandleGetSlots_MissingParams(t *testing.T) {
	h, q := newTestHandler()
	if err := h.settings.SetAPIKey(context.Background(), "cal_live_test"); err != nil {
		t.Fatalf("SetAPIKey: %v", err)
	}
	_ = q

	// Missing username, eventType, and date.
	req := httptest.NewRequest(http.MethodGet, "/slots?blockId=b1", nil)
	rec := httptest.NewRecorder()
	h.HandleGetSlots(rec, req)

	if rec.Code != http.StatusOK {
		t.Errorf("expected 200 (error rendered in body), got %d", rec.Code)
	}
	body := rec.Body.String()
	if !strings.Contains(body, "Missing required parameters") {
		t.Errorf("expected 'Missing required parameters' in body, got: %q", body)
	}
}

// TestHandleGetSlots_NoAPIKey verifies that when no API key is configured the
// handler renders a "not configured" error without making any Cal.com request.
func TestHandleGetSlots_NoAPIKey(t *testing.T) {
	h, _ := newTestHandler()
	// No apiKey stored — leave settings empty.

	req := httptest.NewRequest(http.MethodGet, "/slots?username=alice&eventType=30min&date=2026-05-27&blockId=b1", nil)
	rec := httptest.NewRecorder()
	h.HandleGetSlots(rec, req)

	if rec.Code != http.StatusOK {
		t.Errorf("expected 200 (error rendered in body), got %d", rec.Code)
	}
	body := rec.Body.String()
	if !strings.Contains(body, "not configured") {
		t.Errorf("expected 'not configured' in body, got: %q", body)
	}
}

// TestHandleGetSlots_InvalidDate verifies that a date param that doesn't match
// YYYY-MM-DD produces a "Invalid date format" error rendered in HTML.
func TestHandleGetSlots_InvalidDate(t *testing.T) {
	h, _ := newTestHandler()
	if err := h.settings.SetAPIKey(context.Background(), "cal_live_test"); err != nil {
		t.Fatalf("SetAPIKey: %v", err)
	}

	req := httptest.NewRequest(http.MethodGet, "/slots?username=alice&eventType=30min&date=not-a-date&blockId=b1", nil)
	rec := httptest.NewRecorder()
	h.HandleGetSlots(rec, req)

	if rec.Code != http.StatusOK {
		t.Errorf("expected 200 (error rendered in body), got %d", rec.Code)
	}
	body := rec.Body.String()
	if !strings.Contains(body, "Invalid date format") {
		t.Errorf("expected 'Invalid date format' in body, got: %q", body)
	}
}

// TestHandleGetSlots_HappyPath — TODO: requires a live-or-mocked Cal.com
// endpoint because the handler constructs its own client internally via
// NewCalcomClient(apiKey). Injecting a fake transport at handler level is out
// of scope for WO-043. The client-level happy-path coverage exists in
// client_test.go (TestGetSlots_URLAndHeaders, TestGetSlots_ParsesResponse).

// TestHandleGetSlots_TimezonePassthrough — TODO: same constraint as HappyPath.
// Timezone propagation is covered at the client layer in
// TestGetSlots_URLAndHeaders which verifies the timeZone query param.

// ---------------------------------------------------------------------------
// WO-044 — Anti-abuse: honeypot, invalid email, rate limit
// ---------------------------------------------------------------------------

// TestHandleCreateBooking_HoneypotShortCircuits verifies that a POST with the
// honeypot field ("bn_message_extra") populated never reaches Cal.com (hitCount stays 0)
// and instead returns a fake 200 confirmation to fool the bot.
func TestHandleCreateBooking_HoneypotShortCircuits(t *testing.T) {
	h, _ := newTestHandler()
	if err := h.settings.SetAPIKey(context.Background(), "cal_live_test"); err != nil {
		t.Fatalf("SetAPIKey: %v", err)
	}

	form := url.Values{
		"blockId":          {"b1"},
		"username":         {"alice"},
		"eventType":        {"30min"},
		"start":            {"2026-05-27T10:00:00Z"},
		"name":             {"Bob"},
		"email":            {"bob@example.com"},
		"bn_message_extra": {"spam"}, // honeypot tripped
	}
	req := httptest.NewRequest(http.MethodPost, "/book", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	rec := httptest.NewRecorder()
	h.HandleCreateBooking(rec, req)

	if rec.Code != http.StatusOK {
		t.Errorf("expected 200 (bot thinks it succeeded), got %d", rec.Code)
	}
	// The honeypot path renders CalcomConfirmation — it contains "Booking Confirmed!"
	body := rec.Body.String()
	if !strings.Contains(body, "Booking Confirmed") {
		t.Errorf("expected honeypot response to contain 'Booking Confirmed', got: %q", body)
	}
}

// TestHandleCreateBooking_InvalidEmail verifies that an email address failing
// the regex check returns a rendered HTML error with "Invalid email" text.
// The email check happens BEFORE rate-limit consumption (spec § 3.8), so a
// flood of bad-email requests cannot drain a victim IP's quota.
func TestHandleCreateBooking_InvalidEmail(t *testing.T) {
	h, _ := newTestHandler()
	if err := h.settings.SetAPIKey(context.Background(), "cal_live_test"); err != nil {
		t.Fatalf("SetAPIKey: %v", err)
	}

	for _, badEmail := range []string{"", "not-an-email", "@example.com", "x@"} {
		form := url.Values{
			"blockId":   {"b1"},
			"username":  {"alice"},
			"eventType": {"30min"},
			"start":     {"2026-05-27T10:00:00Z"},
			"name":      {"Bob"},
			"email":     {badEmail},
		}
		req := httptest.NewRequest(http.MethodPost, "/book", strings.NewReader(form.Encode()))
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		rec := httptest.NewRecorder()
		h.HandleCreateBooking(rec, req)

		if rec.Code != http.StatusOK {
			t.Errorf("email=%q: expected 200 with error body, got %d", badEmail, rec.Code)
		}
		body := rec.Body.String()
		// Empty email triggers "Name and email are required"; others trigger "Invalid email address"
		if !strings.Contains(body, "email") && !strings.Contains(body, "Email") {
			t.Errorf("email=%q: expected email-related error in body, got: %q", badEmail, body)
		}
	}
}

// validBookingFormRequest builds a POST request whose form passes every cheap
// validation (name+email present, email regex). The Cal.com call therefore
// fires, so callers must point calcomBaseURL at a fake server via
// withCalcomBaseURL.
func validBookingFormRequest(t *testing.T, ip string) *http.Request {
	t.Helper()
	form := url.Values{
		"blockId":   {"b1"},
		"username":  {"alice"},
		"eventType": {"30min"},
		"start":     {"2026-05-27T10:00:00Z"},
		"name":      {"Bob"},
		"email":     {"bob@example.com"},
	}
	req := httptest.NewRequest(http.MethodPost, "/book", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	if ip != "" {
		req.Header.Set("X-Forwarded-For", ip)
	}
	return req
}

// TestHandleCreateBooking_BadEmailDoesNotConsumeRateLimit guards the spec § 3.8
// requirement that the email regex runs BEFORE rate-limit consumption. Without
// this ordering, a single malicious actor could drain an honest user's IP
// quota by spraying invalid-email requests.
func TestHandleCreateBooking_BadEmailDoesNotConsumeRateLimit(t *testing.T) {
	// Fake Cal.com — counts booking creations. Garbage-email requests must
	// never reach this server; only the final honest request may. (The honest
	// request also triggers the event-timezone lookup — /schedules — which is
	// not a booking and not counted.)
	var calcomHits atomic.Int32
	fake := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if strings.HasPrefix(r.URL.Path, "/bookings") {
			calcomHits.Add(1)
		}
		_, _ = io.WriteString(w, `{"status":"success","data":{"uid":"u"}}`)
	}))
	t.Cleanup(fake.Close)
	withCalcomBaseURL(t, fake.URL)

	h, _ := newTestHandler()
	if err := h.settings.SetAPIKey(context.Background(), "cal_live_test"); err != nil {
		t.Fatalf("SetAPIKey: %v", err)
	}

	const ip = "203.0.113.99"

	// bookingsPerHourPerIP+1 garbage-email requests — each rejected at the
	// email-regex check, no rate-limit slots consumed, no Cal.com calls.
	for i := range bookingsPerHourPerIP + 1 {
		form := url.Values{
			"blockId":   {"b1"},
			"username":  {"alice"},
			"eventType": {"30min"},
			"start":     {"2026-05-27T10:00:00Z"},
			"name":      {"Bot"},
			"email":     {"not-an-email"},
		}
		req := httptest.NewRequest(http.MethodPost, "/book", strings.NewReader(form.Encode()))
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		req.Header.Set("X-Forwarded-For", ip)
		rec := httptest.NewRecorder()
		h.HandleCreateBooking(rec, req)

		if rec.Code == http.StatusTooManyRequests {
			t.Fatalf("garbage-email attempt %d was rate-limited — email regex must run before rate-limit per spec § 3.8", i+1)
		}
	}
	if hits := calcomHits.Load(); hits != 0 {
		t.Fatalf("garbage-email phase hit Cal.com %d times — should be 0", hits)
	}

	// Final honest request — passes regex, consumes first rate-limit slot,
	// reaches Cal.com. Asserts the bucket was untouched by the garbage flood.
	rec := httptest.NewRecorder()
	h.HandleCreateBooking(rec, validBookingFormRequest(t, ip))
	if rec.Code == http.StatusTooManyRequests {
		t.Errorf("honest request was rate-limited because garbage-email requests burned the bucket (spec § 3.8 violation)")
	}
	if hits := calcomHits.Load(); hits != 1 {
		t.Errorf("expected exactly 1 Cal.com hit (the honest request), got %d", hits)
	}
}

// TestHandleCreateBooking_RateLimitAfter10 confirms the per-IP cap: 10 valid
// requests from the same IP pass, the 11th gets 429. The fake Cal.com is
// configured to return success so every legitimate request consumes one slot.
func TestHandleCreateBooking_RateLimitAfter10(t *testing.T) {
	fake := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = io.WriteString(w, `{"status":"success","data":{"uid":"u","rescheduleUrl":"r","cancelUrl":"c"}}`)
	}))
	t.Cleanup(fake.Close)
	withCalcomBaseURL(t, fake.URL)

	h, _ := newTestHandler()
	if err := h.settings.SetAPIKey(context.Background(), "cal_live_test"); err != nil {
		t.Fatalf("SetAPIKey: %v", err)
	}

	const testIP = "203.0.113.42" // TEST-NET-3, safe for tests

	for i := range bookingsPerHourPerIP {
		rec := httptest.NewRecorder()
		h.HandleCreateBooking(rec, validBookingFormRequest(t, testIP))
		if rec.Code == http.StatusTooManyRequests {
			t.Fatalf("attempt %d was rate-limited too early (expected first %d to be allowed)", i+1, bookingsPerHourPerIP)
		}
	}

	// 11th request — must be rate-limited.
	rec := httptest.NewRecorder()
	h.HandleCreateBooking(rec, validBookingFormRequest(t, testIP))
	if rec.Code != http.StatusTooManyRequests {
		t.Errorf("11th attempt: expected 429, got %d (body: %q)", rec.Code, rec.Body.String())
	}
}

// ---------------------------------------------------------------------------
// WO-045 — routeBookingForm unit tests (field-routing correctness)
// ---------------------------------------------------------------------------

// TestRouteBookingForm_BuiltinExcluded verifies control fields and the attendee
// phone are kept out of bookingFieldsResponses, the phone routes to
// attendee.phoneNumber (E.164-normalized), and only genuinely custom keys
// survive as responses.
func TestRouteBookingForm_BuiltinExcluded(t *testing.T) {
	form := map[string][]string{
		// Control/built-ins that must be excluded from responses:
		"blockId":          {"b1"},
		"username":         {"alice"},
		"eventType":        {"30min"},
		"start":            {"2026-05-27T10:00:00Z"},
		"timezone":         {"America/Toronto"},
		"bn_message_extra": {""}, // honeypot (empty)
		"name":             {"Bob"},
		"email":            {"bob@example.com"},
		"phone":            {"+1-555-1234"},
		// Custom fields that MUST survive:
		"q1":           {"yes"},
		"custom_field": {"custom_value"},
	}

	got := routeBookingForm(form, "")
	if got.Responses == nil {
		t.Fatal("expected non-nil responses when custom fields are present")
	}
	for _, builtin := range []string{"blockId", "username", "eventType", "start", "timezone", "bn_message_extra", "name", "email", "phone"} {
		if _, present := got.Responses[builtin]; present {
			t.Errorf("built-in field %q must not appear in bookingFieldsResponses", builtin)
		}
	}
	// Phone routes to the attendee, separators stripped.
	if got.Phone != "+15551234" {
		t.Errorf("expected attendee phone '+15551234', got: %q", got.Phone)
	}
	if got.Responses["q1"] != "yes" {
		t.Errorf("expected q1='yes', got: %v", got.Responses["q1"])
	}
	if got.Responses["custom_field"] != "custom_value" {
		t.Errorf("expected custom_field='custom_value', got: %v", got.Responses["custom_field"])
	}
}

// TestRouteBookingForm_MultiValue verifies that form values with multiple
// entries (multi-select checkboxes) are preserved as a JSON array rather than
// collapsed to a single string.
func TestRouteBookingForm_MultiValue(t *testing.T) {
	form := map[string][]string{
		"topics": {"go", "rust", "zig"}, // multiple selections
	}

	got := routeBookingForm(form, "")
	if got.Responses == nil {
		t.Fatal("expected non-nil responses")
	}
	vs, ok := got.Responses["topics"].([]any)
	if !ok {
		t.Fatalf("expected topics to be []any, got %T: %v", got.Responses["topics"], got.Responses["topics"])
	}
	if len(vs) != 3 || vs[0] != "go" || vs[1] != "rust" || vs[2] != "zig" {
		t.Errorf("multi-value slice wrong: %v", vs)
	}
}

// TestRouteBookingForm_EmptyReturnsNil verifies that a form containing only
// control fields yields nil responses so the omitempty JSON tag drops the field.
func TestRouteBookingForm_EmptyReturnsNil(t *testing.T) {
	form := map[string][]string{
		"blockId":  {"b1"},
		"username": {"alice"},
		"name":     {"Bob"},
		"email":    {"bob@example.com"},
	}

	got := routeBookingForm(form, "")
	if got.Responses != nil {
		t.Errorf("expected nil responses when only control fields present, got: %v", got.Responses)
	}
}

// ---------------------------------------------------------------------------
// WO-046 — HandleGetForm default rendering + admin endpoint shapes
// ---------------------------------------------------------------------------

// TestHandleGetForm_NoBookingFieldsRendersDefaults verifies that when no API
// key is configured (so the Cal.com event-type fetch is skipped), the form
// falls back to rendering the default name + email fields.
func TestHandleGetForm_NoBookingFieldsRendersDefaults(t *testing.T) {
	h, _ := newTestHandler()
	// No apiKey set — handler skips GetEventType and uses default fields.

	req := httptest.NewRequest(http.MethodGet,
		"/form?blockId=b1&start=2026-05-27T10:00:00Z&date=2026-05-27&username=alice&eventType=30min&timezone=UTC",
		nil)
	rec := httptest.NewRecorder()
	h.HandleGetForm(rec, req)

	if rec.Code != http.StatusOK {
		t.Errorf("expected 200, got %d (body: %q)", rec.Code, rec.Body.String())
	}
	body := rec.Body.String()
	if !strings.Contains(body, `name="name"`) {
		t.Errorf("expected name input in default form, body: %q", body)
	}
	if !strings.Contains(body, `name="email"`) {
		t.Errorf("expected email input in default form, body: %q", body)
	}
}

// TestHandleListEventTypes_NotConfigured verifies the JSON error shape when no
// API key has been stored. No outbound request should be made.
func TestHandleListEventTypes_NotConfigured(t *testing.T) {
	h, _ := newTestHandler()
	// No apiKey — handler returns the "not configured" JSON response.

	req := httptest.NewRequest(http.MethodGet, "/event-types?username=alice", nil)
	rec := httptest.NewRecorder()
	h.HandleListEventTypes(rec, req)

	if rec.Code != http.StatusOK {
		t.Errorf("expected 200, got %d", rec.Code)
	}
	var got map[string]any
	if err := json.NewDecoder(rec.Body).Decode(&got); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if success, _ := got["success"].(bool); success {
		t.Errorf("expected success=false, got: %v", got)
	}
	if errMsg, _ := got["error"].(string); errMsg != "API key not configured" {
		t.Errorf("expected error='API key not configured', got: %q", errMsg)
	}
}

// TestHandleGetSettings_ShapeWhenEmpty verifies the JSON shape when no Cal.com
// settings have been configured: api_key_configured=false, webhook_url present,
// webhook_secret_configured=false.
func TestHandleGetSettings_ShapeWhenEmpty(t *testing.T) {
	h, _ := newTestHandler()

	req := httptest.NewRequest(http.MethodGet, "/settings", nil)
	rec := httptest.NewRecorder()
	h.HandleGetSettings(rec, req)

	if rec.Code != http.StatusOK {
		t.Errorf("expected 200, got %d (body: %q)", rec.Code, rec.Body.String())
	}
	var got map[string]any
	if err := json.NewDecoder(rec.Body).Decode(&got); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if v, _ := got["api_key_configured"].(bool); v {
		t.Errorf("expected api_key_configured=false, got true")
	}
	if _, present := got["webhook_url"]; !present {
		t.Errorf("expected webhook_url field in response, got: %v", got)
	}
	if v, _ := got["webhook_secret_configured"].(bool); v {
		t.Errorf("expected webhook_secret_configured=false, got true")
	}
}

// TestHandleGetSettings_ShapeWhenConfigured verifies that after storing an API
// key and a webhook secret the settings response exposes masked values and sets
// the boolean flags to true.
func TestHandleGetSettings_ShapeWhenConfigured(t *testing.T) {
	h, _ := newTestHandler()
	ctx := context.Background()

	if err := h.settings.SetAPIKey(ctx, "cal_live_abc123456789"); err != nil {
		t.Fatalf("SetAPIKey: %v", err)
	}
	if err := h.settings.SetWebhookSecret(ctx, "deadbeefdeadbeefdeadbeefdeadbeef00000000000000000000000000000000"); err != nil {
		t.Fatalf("SetWebhookSecret: %v", err)
	}

	req := httptest.NewRequest(http.MethodGet, "/settings", nil)
	rec := httptest.NewRecorder()
	h.HandleGetSettings(rec, req)

	if rec.Code != http.StatusOK {
		t.Errorf("expected 200, got %d (body: %q)", rec.Code, rec.Body.String())
	}
	var got map[string]any
	if err := json.NewDecoder(rec.Body).Decode(&got); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if v, _ := got["api_key_configured"].(bool); !v {
		t.Errorf("expected api_key_configured=true, got false; resp=%v", got)
	}
	if _, present := got["api_key_masked"]; !present {
		t.Errorf("expected api_key_masked field in response")
	}
	if v, _ := got["webhook_secret_configured"].(bool); !v {
		t.Errorf("expected webhook_secret_configured=true, got false; resp=%v", got)
	}
	if _, present := got["webhook_secret_masked"]; !present {
		t.Errorf("expected webhook_secret_masked field in response")
	}
}

// TestHandleSaveSettings_PersistsUsernameFromMe asserts that a successful API
// key save fires Cal.com /v2/me and persists the resolved username so the
// block editor doesn't have to ask the admin to type it.
func TestHandleSaveSettings_PersistsUsernameFromMe(t *testing.T) {
	fake := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/me" {
			t.Errorf("unexpected upstream path %q (want /me)", r.URL.Path)
		}
		_, _ = io.WriteString(w, `{"status":"success","data":{"username":"alice","email":"alice@example.com"}}`)
	}))
	t.Cleanup(fake.Close)
	withCalcomBaseURL(t, fake.URL)

	h, _ := newTestHandler()
	body := bytes.NewBufferString(`{"api_key":"cal_live_test"}`)
	req := httptest.NewRequest(http.MethodPost, "/settings", body)
	rec := httptest.NewRecorder()
	h.HandleSaveSettings(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
	}
	got, err := h.settings.GetUsername(context.Background())
	if err != nil {
		t.Fatalf("GetUsername: %v", err)
	}
	if got != "alice" {
		t.Errorf("expected username alice, got %q", got)
	}
}

// TestHandleSaveSettings_MeFailureDoesNotBlockSave guarantees that a transient
// Cal.com /me failure does NOT cause the API key save to fail. The key is
// still persisted; username is left empty for the admin to refresh later.
func TestHandleSaveSettings_MeFailureDoesNotBlockSave(t *testing.T) {
	fake := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusServiceUnavailable)
	}))
	t.Cleanup(fake.Close)
	withCalcomBaseURL(t, fake.URL)

	h, _ := newTestHandler()
	body := bytes.NewBufferString(`{"api_key":"cal_live_test"}`)
	req := httptest.NewRequest(http.MethodPost, "/settings", body)
	rec := httptest.NewRecorder()
	h.HandleSaveSettings(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected 200 even when /me is down, got %d: %s", rec.Code, rec.Body.String())
	}
	gotKey, err := h.settings.GetAPIKey(context.Background())
	if err != nil {
		t.Fatalf("GetAPIKey: %v", err)
	}
	if gotKey != "cal_live_test" {
		t.Errorf("API key should still be saved, got %q", gotKey)
	}
	gotUser, _ := h.settings.GetUsername(context.Background())
	if gotUser != "" {
		t.Errorf("username should be empty when /me failed, got %q", gotUser)
	}
}

// TestHandleSaveSettings_ClearingKeyClearsUsername confirms that posting an
// empty api_key both clears the key and the cached username so the next save
// re-derives identity from the fresh key.
func TestHandleSaveSettings_ClearingKeyClearsUsername(t *testing.T) {
	h, _ := newTestHandler()
	ctx := context.Background()
	if err := h.settings.SetAPIKey(ctx, "cal_live_test"); err != nil {
		t.Fatalf("SetAPIKey: %v", err)
	}
	if err := h.settings.SetUsername(ctx, "alice"); err != nil {
		t.Fatalf("SetUsername: %v", err)
	}

	body := bytes.NewBufferString(`{"api_key":""}`)
	req := httptest.NewRequest(http.MethodPost, "/settings", body)
	rec := httptest.NewRecorder()
	h.HandleSaveSettings(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d", rec.Code)
	}
	got, _ := h.settings.GetUsername(ctx)
	if got != "" {
		t.Errorf("username should be cleared on key removal, got %q", got)
	}
}

// TestHandleGetSettings_IncludesUsername confirms the JSON response surfaces
// the cached username so the block editor can stamp it into content without
// asking the admin to type it.
func TestHandleGetSettings_IncludesUsername(t *testing.T) {
	h, _ := newTestHandler()
	ctx := context.Background()
	if err := h.settings.SetAPIKey(ctx, "cal_live_test"); err != nil {
		t.Fatalf("SetAPIKey: %v", err)
	}
	if err := h.settings.SetUsername(ctx, "alice"); err != nil {
		t.Fatalf("SetUsername: %v", err)
	}

	req := httptest.NewRequest(http.MethodGet, "/settings", nil)
	rec := httptest.NewRecorder()
	h.HandleGetSettings(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d", rec.Code)
	}
	var got map[string]any
	if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if got["username"] != "alice" {
		t.Errorf("expected username=alice in response, got %v", got["username"])
	}
}

// TestHandleGetSettings_BackfillsUsernameWhenMissing covers the upgrade path for
// tenants whose API key was saved BEFORE the auto-username feature shipped: the
// key is present, username is empty, so HandleGetSettings lazily fires /v2/me
// and persists the result. Without this self-heal step those tenants would have
// to manually clear and re-save the key.
func TestHandleGetSettings_BackfillsUsernameWhenMissing(t *testing.T) {
	var meHits atomic.Int32
	fake := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/me" {
			t.Errorf("unexpected upstream path %q (want /me)", r.URL.Path)
		}
		meHits.Add(1)
		_, _ = io.WriteString(w, `{"status":"success","data":{"username":"alice","email":"alice@example.com"}}`)
	}))
	t.Cleanup(fake.Close)
	withCalcomBaseURL(t, fake.URL)

	h, _ := newTestHandler()
	ctx := context.Background()
	if err := h.settings.SetAPIKey(ctx, "cal_live_test"); err != nil {
		t.Fatalf("SetAPIKey: %v", err)
	}
	// Deliberately leave username unset to simulate a pre-feature save.

	req := httptest.NewRequest(http.MethodGet, "/settings", nil)
	rec := httptest.NewRecorder()
	h.HandleGetSettings(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
	}
	var got map[string]any
	if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if got["username"] != "alice" {
		t.Errorf("expected username=alice in response, got %v", got["username"])
	}
	persisted, _ := h.settings.GetUsername(ctx)
	if persisted != "alice" {
		t.Errorf("expected backfill to persist username, got %q", persisted)
	}

	// A second GET must NOT re-fire /me — the cache is now warm.
	rec2 := httptest.NewRecorder()
	h.HandleGetSettings(rec2, req)
	if hits := meHits.Load(); hits != 1 {
		t.Errorf("expected exactly 1 /me hit after both GETs, got %d", hits)
	}
}

// TestHandleGetSettings_NoBackfillWhenNoAPIKey guards against the backfill
// firing /me with an empty key — that would 401 every GET on an unconfigured
// plugin.
func TestHandleGetSettings_NoBackfillWhenNoAPIKey(t *testing.T) {
	var meHits atomic.Int32
	fake := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		meHits.Add(1)
		w.WriteHeader(http.StatusUnauthorized)
	}))
	t.Cleanup(fake.Close)
	withCalcomBaseURL(t, fake.URL)

	h, _ := newTestHandler()
	req := httptest.NewRequest(http.MethodGet, "/settings", nil)
	rec := httptest.NewRecorder()
	h.HandleGetSettings(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d", rec.Code)
	}
	if hits := meHits.Load(); hits != 0 {
		t.Errorf("expected zero /me hits without an API key, got %d", hits)
	}
}

// TestHandleGetSettings_BackfillFailureIsNonFatal asserts that a /me error
// during backfill never prevents settings from rendering — the response is
// still 200 with username left empty.
func TestHandleGetSettings_BackfillFailureIsNonFatal(t *testing.T) {
	fake := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusServiceUnavailable)
	}))
	t.Cleanup(fake.Close)
	withCalcomBaseURL(t, fake.URL)

	h, _ := newTestHandler()
	ctx := context.Background()
	if err := h.settings.SetAPIKey(ctx, "cal_live_test"); err != nil {
		t.Fatalf("SetAPIKey: %v", err)
	}

	req := httptest.NewRequest(http.MethodGet, "/settings", nil)
	rec := httptest.NewRecorder()
	h.HandleGetSettings(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
	}
	var got map[string]any
	if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if got["username"] != "" {
		t.Errorf("expected empty username on /me failure, got %v", got["username"])
	}
}

// TestSettingsManager_UsernamePersists round-trips username through the in-memory
// settings store: empty by default, persisted via SetUsername, read back via
// GetUsername. Username is NOT a secret so it is stored as plain text.
func TestSettingsManager_UsernamePersists(t *testing.T) {
	s, _ := newTestSettings()
	ctx := context.Background()

	got, err := s.GetUsername(ctx)
	if err != nil {
		t.Fatalf("GetUsername on empty store: %v", err)
	}
	if got != "" {
		t.Errorf("expected empty username, got %q", got)
	}

	if err := s.SetUsername(ctx, "alice"); err != nil {
		t.Fatalf("SetUsername: %v", err)
	}
	got, err = s.GetUsername(ctx)
	if err != nil {
		t.Fatalf("GetUsername after set: %v", err)
	}
	if got != "alice" {
		t.Errorf("Username round-trip: got %q, want alice", got)
	}
}

// TestSettingsManager_SetUsernameEmptyClears confirms that passing "" deletes
// the persisted username so the admin can effectively "forget" the cached
// owner on demand (e.g. after rotating the API key).
func TestSettingsManager_SetUsernameEmptyClears(t *testing.T) {
	s, _ := newTestSettings()
	ctx := context.Background()
	if err := s.SetUsername(ctx, "alice"); err != nil {
		t.Fatalf("SetUsername: %v", err)
	}
	if err := s.SetUsername(ctx, ""); err != nil {
		t.Fatalf("SetUsername empty: %v", err)
	}
	got, err := s.GetUsername(ctx)
	if err != nil {
		t.Fatalf("GetUsername: %v", err)
	}
	if got != "" {
		t.Errorf("expected cleared username, got %q", got)
	}
}
