package restapi

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

	"galaxy/gateway/internal/config"
	"galaxy/gateway/internal/testutil"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

const confirmEmailCodeTestTimeZone = "Europe/Kaliningrad"

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

	authService := &recordingAuthServiceClient{
		sendEmailCodeResult: SendEmailCodeResult{
			ChallengeID: "challenge-123",
		},
	}
	handler := newPublicHandler(ServerDependencies{AuthService: authService})

	req := httptest.NewRequest(
		http.MethodPost,
		"/api/v1/public/auth/send-email-code",
		strings.NewReader(`{"email":" pilot@example.com "}`),
	)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept-Language", "fr-FR, en;q=0.8")
	recorder := httptest.NewRecorder()

	handler.ServeHTTP(recorder, req)

	assert.Equal(t, http.StatusOK, recorder.Code)
	assert.Equal(t, jsonContentType, recorder.Header().Get("Content-Type"))
	assert.Equal(t, `{"challenge_id":"challenge-123"}`, recorder.Body.String())
	assert.Equal(t, 1, authService.sendEmailCodeCalls)
	assert.Equal(t, 0, authService.confirmEmailCodeCalls)
	assert.Equal(t, SendEmailCodeInput{
		Email:             "pilot@example.com",
		PreferredLanguage: "fr-FR",
	}, authService.sendEmailCodeInput)
	assert.True(t, authService.sendEmailCodeRouteClassOK)
	assert.Equal(t, PublicRouteClassPublicAuth, authService.sendEmailCodeRouteClass)
}

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

	authService := &recordingAuthServiceClient{
		sendEmailCodeResult: SendEmailCodeResult{
			ChallengeID: "challenge-456",
		},
	}
	handler := newPublicHandler(ServerDependencies{AuthService: authService})

	req := httptest.NewRequest(
		http.MethodPost,
		"/api/v1/public/auth/send-email-code",
		strings.NewReader(`{"email":"pilot@example.com","locale":"ru"}`),
	)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept-Language", "fr-FR")
	recorder := httptest.NewRecorder()

	handler.ServeHTTP(recorder, req)

	assert.Equal(t, http.StatusOK, recorder.Code)
	assert.Equal(t, SendEmailCodeInput{
		Email:             "pilot@example.com",
		Locale:             "ru",
		PreferredLanguage: "ru",
	}, authService.sendEmailCodeInput)
}

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

	authService := &recordingAuthServiceClient{
		sendEmailCodeResult: SendEmailCodeResult{
			ChallengeID: "challenge-789",
		},
	}
	handler := newPublicHandler(ServerDependencies{AuthService: authService})

	req := httptest.NewRequest(
		http.MethodPost,
		"/api/v1/public/auth/send-email-code",
		strings.NewReader(`{"email":"pilot@example.com","locale":"   "}`),
	)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept-Language", "ru-RU")
	recorder := httptest.NewRecorder()

	handler.ServeHTTP(recorder, req)

	assert.Equal(t, http.StatusOK, recorder.Code)
	assert.Equal(t, SendEmailCodeInput{
		Email:             "pilot@example.com",
		Locale:             "   ",
		PreferredLanguage: "ru-RU",
	}, authService.sendEmailCodeInput)
}

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

	authService := &recordingAuthServiceClient{
		confirmEmailCodeResult: ConfirmEmailCodeResult{
			DeviceSessionID: "device-session-123",
		},
	}
	handler := newPublicHandler(ServerDependencies{AuthService: authService})

	req := httptest.NewRequest(
		http.MethodPost,
		"/api/v1/public/auth/confirm-email-code",
		strings.NewReader(`{"challenge_id":" challenge-123 ","code":" 123456 ","client_public_key":" public-key-material ","time_zone":" `+confirmEmailCodeTestTimeZone+` "}`),
	)
	req.Header.Set("Content-Type", "application/json")
	recorder := httptest.NewRecorder()

	handler.ServeHTTP(recorder, req)

	assert.Equal(t, http.StatusOK, recorder.Code)
	assert.Equal(t, jsonContentType, recorder.Header().Get("Content-Type"))
	assert.Equal(t, `{"device_session_id":"device-session-123"}`, recorder.Body.String())
	assert.Equal(t, 0, authService.sendEmailCodeCalls)
	assert.Equal(t, 1, authService.confirmEmailCodeCalls)
	assert.Equal(t, ConfirmEmailCodeInput{
		ChallengeID:     "challenge-123",
		Code:            "123456",
		ClientPublicKey: "public-key-material",
		TimeZone:        confirmEmailCodeTestTimeZone,
	}, authService.confirmEmailCodeInput)
	assert.True(t, authService.confirmEmailCodeRouteClassOK)
	assert.Equal(t, PublicRouteClassPublicAuth, authService.confirmEmailCodeRouteClass)
}

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

	tests := []struct {
		name             string
		target           string
		body             string
		wantStatus       int
		wantBody         string
		wantSendCalls    int
		wantConfirmCalls int
	}{
		{
			name:             "send email malformed json",
			target:           "/api/v1/public/auth/send-email-code",
			body:             `{"email":`,
			wantStatus:       http.StatusBadRequest,
			wantBody:         `{"error":{"code":"invalid_request","message":"request body contains malformed JSON"}}`,
			wantSendCalls:    0,
			wantConfirmCalls: 0,
		},
		{
			name:             "send email validation error",
			target:           "/api/v1/public/auth/send-email-code",
			body:             `{"email":"not-an-email"}`,
			wantStatus:       http.StatusBadRequest,
			wantBody:         `{"error":{"code":"invalid_request","message":"email must be a single valid email address"}}`,
			wantSendCalls:    0,
			wantConfirmCalls: 0,
		},
		{
			name:             "confirm email empty code",
			target:           "/api/v1/public/auth/confirm-email-code",
			body:             `{"challenge_id":"challenge-123","code":"   ","client_public_key":"public-key-material","time_zone":"` + confirmEmailCodeTestTimeZone + `"}`,
			wantStatus:       http.StatusBadRequest,
			wantBody:         `{"error":{"code":"invalid_request","message":"code must not be empty"}}`,
			wantSendCalls:    0,
			wantConfirmCalls: 0,
		},
		{
			name:             "confirm email empty time zone",
			target:           "/api/v1/public/auth/confirm-email-code",
			body:             `{"challenge_id":"challenge-123","code":"123456","client_public_key":"public-key-material","time_zone":"   "}`,
			wantStatus:       http.StatusBadRequest,
			wantBody:         `{"error":{"code":"invalid_request","message":"time_zone must not be empty"}}`,
			wantSendCalls:    0,
			wantConfirmCalls: 0,
		},
	}

	for _, tt := range tests {
		tt := tt

		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			authService := &recordingAuthServiceClient{}
			handler := newPublicHandler(ServerDependencies{AuthService: authService})

			req := httptest.NewRequest(http.MethodPost, tt.target, strings.NewReader(tt.body))
			req.Header.Set("Content-Type", "application/json")
			recorder := httptest.NewRecorder()

			handler.ServeHTTP(recorder, req)

			assert.Equal(t, tt.wantStatus, recorder.Code)
			assert.Equal(t, jsonContentType, recorder.Header().Get("Content-Type"))
			assert.Equal(t, tt.wantBody, recorder.Body.String())
			assert.Equal(t, tt.wantSendCalls, authService.sendEmailCodeCalls)
			assert.Equal(t, tt.wantConfirmCalls, authService.confirmEmailCodeCalls)
		})
	}
}

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

	tests := []struct {
		name       string
		target     string
		body       string
		authClient *recordingAuthServiceClient
		wantStatus int
		wantBody   string
	}{
		{
			name:   "auth service projected bad request",
			target: "/api/v1/public/auth/confirm-email-code",
			body:   `{"challenge_id":"challenge-123","code":"123456","client_public_key":"public-key-material","time_zone":"` + confirmEmailCodeTestTimeZone + `"}`,
			authClient: &recordingAuthServiceClient{
				confirmEmailCodeErr: &AuthServiceError{
					StatusCode: http.StatusBadRequest,
					Code:       errorCodeInvalidRequest,
					Message:    "confirmation code is invalid",
				},
			},
			wantStatus: http.StatusBadRequest,
			wantBody:   `{"error":{"code":"invalid_request","message":"confirmation code is invalid"}}`,
		},
		{
			name:   "auth service projected custom too many requests",
			target: "/api/v1/public/auth/send-email-code",
			body:   `{"email":"pilot@example.com"}`,
			authClient: &recordingAuthServiceClient{
				sendEmailCodeErr: &AuthServiceError{
					StatusCode: http.StatusTooManyRequests,
					Code:       "upstream_rate_limited",
					Message:    "too many attempts for this email",
				},
			},
			wantStatus: http.StatusTooManyRequests,
			wantBody:   `{"error":{"code":"upstream_rate_limited","message":"too many attempts for this email"}}`,
		},
		{
			name:   "auth service projected gateway normalizes blank gateway error fields",
			target: "/api/v1/public/auth/confirm-email-code",
			body:   `{"challenge_id":"challenge-123","code":"123456","client_public_key":"public-key-material","time_zone":"` + confirmEmailCodeTestTimeZone + `"}`,
			authClient: &recordingAuthServiceClient{
				confirmEmailCodeErr: &AuthServiceError{
					StatusCode: http.StatusBadGateway,
				},
			},
			wantStatus: http.StatusBadGateway,
			wantBody:   `{"error":{"code":"internal_error","message":"internal server error"}}`,
		},
		{
			name:   "unexpected auth service error",
			target: "/api/v1/public/auth/send-email-code",
			body:   `{"email":"pilot@example.com"}`,
			authClient: &recordingAuthServiceClient{
				sendEmailCodeErr: errors.New("boom"),
			},
			wantStatus: http.StatusInternalServerError,
			wantBody:   `{"error":{"code":"internal_error","message":"internal server error"}}`,
		},
	}

	for _, tt := range tests {
		tt := tt

		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			handler := newPublicHandler(ServerDependencies{AuthService: tt.authClient})
			req := httptest.NewRequest(http.MethodPost, tt.target, strings.NewReader(tt.body))
			req.Header.Set("Content-Type", "application/json")
			recorder := httptest.NewRecorder()

			handler.ServeHTTP(recorder, req)

			assert.Equal(t, tt.wantStatus, recorder.Code)
			assert.Equal(t, jsonContentType, recorder.Header().Get("Content-Type"))
			assert.Equal(t, tt.wantBody, recorder.Body.String())
		})
	}
}

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

	handler := newPublicHandler(ServerDependencies{})

	tests := []struct {
		name       string
		method     string
		target     string
		body       string
		wantStatus int
		wantBody   string
	}{
		{
			name:       "send email code",
			method:     http.MethodPost,
			target:     "/api/v1/public/auth/send-email-code",
			body:       `{"email":"pilot@example.com"}`,
			wantStatus: http.StatusServiceUnavailable,
			wantBody:   `{"error":{"code":"service_unavailable","message":"auth service is unavailable"}}`,
		},
		{
			name:       "confirm email code",
			method:     http.MethodPost,
			target:     "/api/v1/public/auth/confirm-email-code",
			body:       `{"challenge_id":"challenge-123","code":"123456","client_public_key":"public-key-material","time_zone":"` + confirmEmailCodeTestTimeZone + `"}`,
			wantStatus: http.StatusServiceUnavailable,
			wantBody:   `{"error":{"code":"service_unavailable","message":"auth service is unavailable"}}`,
		},
		{
			name:       "healthz remains available",
			method:     http.MethodGet,
			target:     "/healthz",
			wantStatus: http.StatusOK,
			wantBody:   `{"status":"ok"}`,
		},
	}

	for _, tt := range tests {
		tt := tt

		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			req := httptest.NewRequest(tt.method, tt.target, strings.NewReader(tt.body))
			if tt.body != "" {
				req.Header.Set("Content-Type", "application/json")
			}
			recorder := httptest.NewRecorder()

			handler.ServeHTTP(recorder, req)

			assert.Equal(t, tt.wantStatus, recorder.Code)
			assert.Equal(t, jsonContentType, recorder.Header().Get("Content-Type"))
			assert.Equal(t, tt.wantBody, recorder.Body.String())
		})
	}
}

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

	authService := &recordingAuthServiceClient{
		sendEmailCodeErr: context.DeadlineExceeded,
	}
	cfg := config.DefaultPublicHTTPConfig()
	cfg.AuthUpstreamTimeout = 5 * time.Millisecond
	handler := newPublicHandlerWithConfig(cfg, ServerDependencies{AuthService: authService})

	req := httptest.NewRequest(
		http.MethodPost,
		"/api/v1/public/auth/send-email-code",
		strings.NewReader(`{"email":"pilot@example.com"}`),
	)
	req.Header.Set("Content-Type", "application/json")
	recorder := httptest.NewRecorder()

	handler.ServeHTTP(recorder, req)

	assert.Equal(t, http.StatusServiceUnavailable, recorder.Code)
	assert.Equal(t, `{"error":{"code":"service_unavailable","message":"auth service is unavailable"}}`, recorder.Body.String())
}

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

	logger, buffer := testutil.NewObservedLogger(t)
	handler := newPublicHandler(ServerDependencies{
		Logger: logger,
		AuthService: &recordingAuthServiceClient{
			confirmEmailCodeResult: ConfirmEmailCodeResult{DeviceSessionID: "device-session-123"},
		},
	})

	req := httptest.NewRequest(
		http.MethodPost,
		"/api/v1/public/auth/confirm-email-code",
		strings.NewReader(`{"challenge_id":"challenge-123","code":"123456","client_public_key":"public-key-material","time_zone":"`+confirmEmailCodeTestTimeZone+`"}`),
	)
	req.Header.Set("Content-Type", "application/json")
	recorder := httptest.NewRecorder()

	handler.ServeHTTP(recorder, req)
	require.Equal(t, http.StatusOK, recorder.Code)

	logOutput := buffer.String()
	assert.NotContains(t, logOutput, "challenge-123")
	assert.NotContains(t, logOutput, "123456")
	assert.NotContains(t, logOutput, "public-key-material")
	assert.NotContains(t, logOutput, "pilot@example.com")
}

// recordingAuthServiceClient captures handler inputs and route classification
// so tests can assert the exact adapter delegation contract.
type recordingAuthServiceClient struct {
	sendEmailCodeResult       SendEmailCodeResult
	sendEmailCodeErr          error
	sendEmailCodeInput        SendEmailCodeInput
	sendEmailCodeRouteClass   PublicRouteClass
	sendEmailCodeRouteClassOK bool
	sendEmailCodeCalls        int

	confirmEmailCodeResult       ConfirmEmailCodeResult
	confirmEmailCodeErr          error
	confirmEmailCodeInput        ConfirmEmailCodeInput
	confirmEmailCodeRouteClass   PublicRouteClass
	confirmEmailCodeRouteClassOK bool
	confirmEmailCodeCalls        int
}

func (c *recordingAuthServiceClient) SendEmailCode(ctx context.Context, input SendEmailCodeInput) (SendEmailCodeResult, error) {
	c.sendEmailCodeCalls++
	c.sendEmailCodeInput = input

	c.sendEmailCodeRouteClass, c.sendEmailCodeRouteClassOK = PublicRouteClassFromContext(ctx)

	return c.sendEmailCodeResult, c.sendEmailCodeErr
}

func (c *recordingAuthServiceClient) ConfirmEmailCode(ctx context.Context, input ConfirmEmailCodeInput) (ConfirmEmailCodeResult, error) {
	c.confirmEmailCodeCalls++
	c.confirmEmailCodeInput = input

	c.confirmEmailCodeRouteClass, c.confirmEmailCodeRouteClassOK = PublicRouteClassFromContext(ctx)

	return c.confirmEmailCodeResult, c.confirmEmailCodeErr
}
