package auth

import (
	"bytes"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"regexp"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/sms"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
)

func TestNewMobileVerificationCodeReturnsDigits(t *testing.T) {
	t.Parallel()

	code, err := newMobileVerificationCode(mobileVerificationCodeLength)
	require.NoError(t, err)
	require.Len(t, code, mobileVerificationCodeLength)
	require.True(t, regexp.MustCompile(`^[0-9]+$`).MatchString(code))
}

func TestMobileRequestVerificationReturnsUnavailableWhenPasswordAuthDisabled(t *testing.T) {
	t.Parallel()

	handler := &MobileHandler{passwordAuthDisabled: true}
	router := gin.New()
	router.POST("/api/v1/auth/mobile/request-verification", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, handleAuthJSON[dto.MobileVerificationRequest](handler.RequestVerification))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/mobile/request-verification", bytes.NewBufferString(`{"mobile":"+15551234567"}`))
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusServiceUnavailable, recorder.Code)
	require.Contains(t, recorder.Body.String(), "Mobile verification is disabled when password auth is disabled")
}

func TestMobileVerifyReturnsUnavailableWhenPasswordAuthDisabled(t *testing.T) {
	t.Parallel()

	handler := &MobileHandler{passwordAuthDisabled: true}
	router := gin.New()
	router.POST("/api/v1/auth/mobile/verify", func(ctx *gin.Context) {
		api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
	}, handleAuthJSON[dto.MobileVerifyRequest](handler.Verify))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/mobile/verify", bytes.NewBufferString(`{"code":"123456"}`))
	router.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusServiceUnavailable, recorder.Code)
	require.Contains(t, recorder.Body.String(), "Mobile verification is disabled when password auth is disabled")
}

func TestUserResponseFromDomainIncludesMobileVerification(t *testing.T) {
	t.Parallel()

	now := time.Now().UTC()
	createdAt := time.Date(2026, time.August, 6, 8, 30, 0, 0, time.UTC)
	response := userResponseFromDomain(identity.User{
		ID:               "usr_test",
		Email:            "user@example.com",
		Mobile:           "+15551234567",
		Name:             "user",
		DisplayName:      "User",
		IsAdmin:          true,
		EmailVerifiedAt:  nil,
		MobileVerifiedAt: &now,
		CreatedAt:        createdAt,
	})

	require.Equal(t, "+15551234567", response.Mobile)
	require.True(t, response.IsAdmin)
	require.False(t, response.EmailVerified)
	require.True(t, response.MobileVerified)
	require.Equal(t, createdAt, response.CreatedAt)
}

func TestMobileVerificationProviderFallsBackToUnknown(t *testing.T) {
	t.Parallel()

	require.Equal(t, "unknown", mobileVerificationProvider(sms.Receipt{}))
	require.Equal(t, sms.ProviderTwilio, mobileVerificationProvider(sms.Receipt{Provider: sms.ProviderTwilio}))
}

func TestMobileVerificationLogAttrsIncludeProviderMetadata(t *testing.T) {
	t.Parallel()

	expiresAt := time.Date(2026, time.March, 23, 12, 30, 0, 0, time.UTC)
	attrs := mobileVerificationLogAttrs("mvh_test", "usr_test", "+15551234567", expiresAt, sms.Receipt{
		Provider:          sms.ProviderTwilio,
		SenderIdentity:    "MG123",
		ProviderMessageID: "SM123",
		ProviderStatus:    "queued",
	})

	require.Contains(t, attrs, slog.String("attempt_id", "mvh_test"))
	require.Contains(t, attrs, slog.String("user_id", "usr_test"))
	require.Contains(t, attrs, slog.String("provider", sms.ProviderTwilio))
	require.Contains(t, attrs, slog.String("mobile_hint", "+1555*****67"))
	require.Contains(t, attrs, slog.String("expires_at", expiresAt.Format(time.RFC3339)))
	require.Contains(t, attrs, slog.String("sender_identity", "MG123"))
	require.Contains(t, attrs, slog.String("provider_message_id", "SM123"))
	require.Contains(t, attrs, slog.String("provider_status", "queued"))
}

func TestMobileVerificationAttemptViewFromStoreRedactsMobile(t *testing.T) {
	t.Parallel()

	completedAt := time.Date(2026, time.March, 23, 12, 31, 0, 0, time.UTC)
	view := mobileVerificationAttemptViewFromStore(identity.MobileVerificationAttempt{
		ID:             "mvh_test",
		Mobile:         "+15551234567",
		Provider:       sms.ProviderTwilio,
		Status:         identity.MobileVerificationAttemptStatusSent,
		ProviderStatus: "queued",
		ExpiresAt:      time.Date(2026, time.March, 23, 12, 45, 0, 0, time.UTC),
		RequestedAt:    time.Date(2026, time.March, 23, 12, 30, 0, 0, time.UTC),
		CompletedAt:    &completedAt,
	})

	require.Equal(t, "mvh_test", view.ID)
	require.Equal(t, "+1555*****67", view.MobileHint)
	require.Equal(t, sms.ProviderTwilio, view.Provider)
	require.Equal(t, identity.MobileVerificationAttemptStatusSent, view.Status)
	require.False(t, view.CodeActive)
	require.Equal(t, "provider queued", view.StatusDetail)
	require.Equal(t, "queued", view.ProviderStatus)
	require.Equal(t, completedAt, *view.CompletedAt)
}

func TestMobileVerificationAttemptViewsFromStoreMarksActiveCode(t *testing.T) {
	t.Parallel()

	expiresAt := time.Date(2026, time.March, 23, 12, 45, 0, 0, time.UTC)
	views := mobileVerificationAttemptViewsFromStore([]identity.MobileVerificationAttempt{
		{
			ID:          "mvh_latest",
			Mobile:      "+15551234567",
			Provider:    sms.ProviderTwilio,
			Status:      identity.MobileVerificationAttemptStatusSent,
			ExpiresAt:   expiresAt,
			RequestedAt: time.Date(2026, time.March, 23, 12, 30, 0, 0, time.UTC),
		},
	}, &identity.ActiveMobileVerificationCode{
		Mobile:    "+15551234567",
		ExpiresAt: expiresAt,
	})

	require.Len(t, views, 1)
	require.True(t, views[0].CodeActive)
}

func TestMobileVerificationAttemptViewsFromStoreLeavesInactiveCodeFalse(t *testing.T) {
	t.Parallel()

	latestExpiresAt := time.Date(2026, time.March, 23, 12, 50, 0, 0, time.UTC)
	olderExpiresAt := time.Date(2026, time.March, 23, 12, 45, 0, 0, time.UTC)
	views := mobileVerificationAttemptViewsFromStore([]identity.MobileVerificationAttempt{
		{
			ID:          "mvh_latest",
			Mobile:      "+15551234567",
			Provider:    sms.ProviderTwilio,
			Status:      identity.MobileVerificationAttemptStatusSent,
			ExpiresAt:   latestExpiresAt,
			RequestedAt: time.Date(2026, time.March, 23, 12, 35, 0, 0, time.UTC),
		},
		{
			ID:          "mvh_older",
			Mobile:      "+15551234567",
			Provider:    sms.ProviderTwilio,
			Status:      identity.MobileVerificationAttemptStatusSent,
			ExpiresAt:   olderExpiresAt,
			RequestedAt: time.Date(2026, time.March, 23, 12, 30, 0, 0, time.UTC),
		},
		{
			ID:          "mvh_failed",
			Mobile:      "+15557654321",
			Provider:    sms.ProviderTwilio,
			Status:      identity.MobileVerificationAttemptStatusFailed,
			ExpiresAt:   time.Date(2026, time.March, 23, 12, 55, 0, 0, time.UTC),
			RequestedAt: time.Date(2026, time.March, 23, 12, 40, 0, 0, time.UTC),
		},
	}, &identity.ActiveMobileVerificationCode{
		Mobile:    "+15551234567",
		ExpiresAt: latestExpiresAt,
	})

	require.Len(t, views, 3)
	require.True(t, views[0].CodeActive)
	require.False(t, views[1].CodeActive)
	require.False(t, views[2].CodeActive)
}

func TestMobileVerificationAttemptViewFromStoreSanitizesFailureDetail(t *testing.T) {
	t.Parallel()

	view := mobileVerificationAttemptViewFromStore(identity.MobileVerificationAttempt{
		ID:           "mvh_test",
		Mobile:       "+15551234567",
		Provider:     sms.ProviderTwilio,
		Status:       identity.MobileVerificationAttemptStatusFailed,
		ErrorMessage: "send twilio verification SMS: status 400 code 21211: The 'To' number is not a valid phone number. (https://www.twilio.com/docs/errors/21211)",
		ExpiresAt:    time.Date(2026, time.March, 23, 12, 45, 0, 0, time.UTC),
		RequestedAt:  time.Date(2026, time.March, 23, 12, 30, 0, 0, time.UTC),
	})

	require.Equal(t, "The 'To' number is not a valid phone number.", view.StatusDetail)
}

func TestMobileVerificationAttemptViewFromStoreFallsBackToGenericFailureDetail(t *testing.T) {
	t.Parallel()

	view := mobileVerificationAttemptViewFromStore(identity.MobileVerificationAttempt{
		ID:           "mvh_test",
		Mobile:       "+15551234567",
		Provider:     sms.ProviderTwilio,
		Status:       identity.MobileVerificationAttemptStatusFailed,
		ErrorMessage: "unexpected provider failure",
		ExpiresAt:    time.Date(2026, time.March, 23, 12, 45, 0, 0, time.UTC),
		RequestedAt:  time.Date(2026, time.March, 23, 12, 30, 0, 0, time.UTC),
	})

	require.Equal(t, "delivery failed", view.StatusDetail)
}

func TestMobileVerificationAttemptParamsIncludesFailureMessage(t *testing.T) {
	t.Parallel()

	completedAt := time.Date(2026, time.March, 23, 12, 31, 0, 0, time.UTC)
	params := mobileVerificationAttemptParams(identity.MobileVerificationAttemptStatusFailed, completedAt, sms.Receipt{
		Provider:          sms.ProviderTwilio,
		SenderIdentity:    "MG123",
		ProviderMessageID: "SM123",
		ProviderStatus:    "undelivered",
	}, assertErr("twilio rejected number"))

	require.Equal(t, sms.ProviderTwilio, params.Provider)
	require.Equal(t, identity.MobileVerificationAttemptStatusFailed, params.Status)
	require.Equal(t, "MG123", params.SenderIdentity)
	require.Equal(t, "SM123", params.ProviderMessageID)
	require.Equal(t, "undelivered", params.ProviderStatus)
	require.Equal(t, "twilio rejected number", params.ErrorMessage)
	require.Equal(t, completedAt, params.CompletedAt)
}

type assertErr string

func (err assertErr) Error() string {
	return string(err)
}
