package backendclient_test

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

	"galaxy/gateway/internal/backendclient"
	"galaxy/gateway/internal/session"

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

func newRESTClient(t *testing.T, server *httptest.Server) *backendclient.RESTClient {
	t.Helper()
	cfg := backendclient.Config{
		HTTPBaseURL:              server.URL,
		GRPCPushURL:              "passthrough://test",
		GatewayClientID:          "test-gateway",
		HTTPTimeout:              time.Second,
		PushReconnectBaseBackoff: 10 * time.Millisecond,
		PushReconnectMaxBackoff:  100 * time.Millisecond,
	}
	client, err := backendclient.NewRESTClient(cfg)
	require.NoError(t, err)
	t.Cleanup(func() { _ = client.Close() })
	return client
}

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

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodGet, r.Method)
		require.Equal(t, "/api/v1/internal/sessions/device-1", r.URL.Path)
		writeJSON(t, w, http.StatusOK, map[string]any{
			"device_session_id": "device-1",
			"user_id":           "user-1",
			"status":            "active",
			"client_public_key": "pk-1",
			"created_at":        "2026-04-01T00:00:00Z",
		})
	}))
	t.Cleanup(server.Close)

	client := newRESTClient(t, server)
	rec, err := client.LookupSession(context.Background(), "device-1")
	require.NoError(t, err)
	assert.Equal(t, session.Record{
		DeviceSessionID: "device-1",
		UserID:          "user-1",
		ClientPublicKey: "pk-1",
		Status:          session.StatusActive,
	}, rec)
}

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

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		writeJSON(t, w, http.StatusOK, map[string]any{
			"device_session_id": "device-2",
			"user_id":           "user-2",
			"status":            "revoked",
			"client_public_key": "pk-2",
			"created_at":        "2026-04-01T00:00:00Z",
			"revoked_at":        "2026-04-01T00:01:00Z",
		})
	}))
	t.Cleanup(server.Close)

	client := newRESTClient(t, server)
	rec, err := client.LookupSession(context.Background(), "device-2")
	require.NoError(t, err)
	assert.Equal(t, session.StatusRevoked, rec.Status)
	require.NotNil(t, rec.RevokedAtMS)
	assert.Equal(t, time.Date(2026, 4, 1, 0, 1, 0, 0, time.UTC).UnixMilli(), *rec.RevokedAtMS)
}

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

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		writeJSON(t, w, http.StatusNotFound, map[string]any{"error": map[string]any{"code": "subject_not_found", "message": "missing"}})
	}))
	t.Cleanup(server.Close)

	client := newRESTClient(t, server)
	_, err := client.LookupSession(context.Background(), "missing")
	require.Error(t, err)
	assert.True(t, errors.Is(err, session.ErrNotFound))
}

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

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		writeJSON(t, w, http.StatusOK, map[string]any{
			"device_session_id": "other",
			"user_id":           "user-1",
			"status":            "active",
			"client_public_key": "pk-1",
			"created_at":        "2026-04-01T00:00:00Z",
		})
	}))
	t.Cleanup(server.Close)

	client := newRESTClient(t, server)
	_, err := client.LookupSession(context.Background(), "device-1")
	require.Error(t, err)
	assert.Contains(t, err.Error(), "does not match requested")
}

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

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodPost, r.Method)
		require.Equal(t, "/api/v1/public/auth/send-email-code", r.URL.Path)
		require.Equal(t, "ru-RU", r.Header.Get("Accept-Language"))
		writeJSON(t, w, http.StatusOK, map[string]any{"challenge_id": "challenge-1"})
	}))
	t.Cleanup(server.Close)

	client := newRESTClient(t, server)
	out, err := client.SendEmailCode(context.Background(), backendclient.SendEmailCodeInput{
		Email:             "user@example.com",
		PreferredLanguage: "ru-RU",
	})
	require.NoError(t, err)
	assert.Equal(t, "challenge-1", out.ChallengeID)
}

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

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		writeJSON(t, w, http.StatusBadRequest, map[string]any{
			"error": map[string]any{"code": "invalid_request", "message": "bad email"},
		})
	}))
	t.Cleanup(server.Close)

	client := newRESTClient(t, server)
	_, err := client.SendEmailCode(context.Background(), backendclient.SendEmailCodeInput{Email: "user@example.com"})
	require.Error(t, err)
	var authErr *backendclient.AuthError
	require.ErrorAs(t, err, &authErr)
	assert.Equal(t, http.StatusBadRequest, authErr.StatusCode)
	assert.Equal(t, "invalid_request", authErr.Code)
	assert.Equal(t, "bad email", authErr.Message)
}

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

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, "/api/v1/public/auth/confirm-email-code", r.URL.Path)

		var body backendclient.ConfirmEmailCodeInput
		require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
		assert.Equal(t, "challenge-1", body.ChallengeID)
		writeJSON(t, w, http.StatusOK, map[string]any{"device_session_id": "device-1"})
	}))
	t.Cleanup(server.Close)

	client := newRESTClient(t, server)
	out, err := client.ConfirmEmailCode(context.Background(), backendclient.ConfirmEmailCodeInput{
		ChallengeID: "challenge-1",
		Code:        "12345",
	})
	require.NoError(t, err)
	assert.Equal(t, "device-1", out.DeviceSessionID)
}

func writeJSON(t *testing.T, w http.ResponseWriter, status int, body any) {
	t.Helper()
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	require.NoError(t, json.NewEncoder(w).Encode(body))
}

// guard ensures package keeps testify dependency.
var _ = strings.TrimSpace
