package api

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gofrs/uuid"
	jwt "github.com/golang-jwt/jwt/v5"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"
	"github.com/supabase/auth/internal/conf"
	mail "github.com/supabase/auth/internal/mailer"
	"github.com/supabase/auth/internal/models"
)

type AnonymousTestSuite struct {
	suite.Suite
	API    *API
	Config *conf.GlobalConfiguration
}

func TestAnonymous(t *testing.T) {
	api, config, err := setupAPIForTest()
	require.NoError(t, err)

	ts := &AnonymousTestSuite{
		API:    api,
		Config: config,
	}
	defer api.db.Close()

	suite.Run(t, ts)
}

func (ts *AnonymousTestSuite) SetupTest() {
	models.TruncateAll(ts.API.db)

	// Create anonymous user
	params := &SignupParams{
		Aud:      ts.Config.JWT.Aud,
		Provider: "anonymous",
	}
	u, err := params.ToUserModel(false)
	require.NoError(ts.T(), err, "Error creating test user model")
	require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new anonymous test user")
}

func (ts *AnonymousTestSuite) TestAnonymousLogins() {
	ts.Config.External.AnonymousUsers.Enabled = true
	// Request body
	var buffer bytes.Buffer
	require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
		"data": map[string]interface{}{
			"field": "foo",
		},
	}))

	req := httptest.NewRequest(http.MethodPost, "/signup", &buffer)
	req.Header.Set("Content-Type", "application/json")

	w := httptest.NewRecorder()

	ts.API.handler.ServeHTTP(w, req)
	require.Equal(ts.T(), http.StatusOK, w.Code)

	data := &AccessTokenResponse{}
	require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
	assert.NotEmpty(ts.T(), data.User.ID)
	assert.Equal(ts.T(), ts.Config.JWT.Aud, data.User.Aud)
	assert.Empty(ts.T(), data.User.GetEmail())
	assert.Empty(ts.T(), data.User.GetPhone())
	assert.True(ts.T(), data.User.IsAnonymous)
	assert.Equal(ts.T(), models.JSONMap(models.JSONMap{"field": "foo"}), data.User.UserMetaData)
}

func (ts *AnonymousTestSuite) TestConvertAnonymousUserToPermanent() {
	ts.Config.External.AnonymousUsers.Enabled = true
	ts.Config.Sms.TestOTP = map[string]string{"1234567890": "000000", "1234560000": "000000"}
	// test OTPs still require setting up an sms provider
	ts.Config.Sms.Provider = "twilio"
	ts.Config.Sms.Twilio.AccountSid = "fake-sid"
	ts.Config.Sms.Twilio.AuthToken = "fake-token"
	ts.Config.Sms.Twilio.MessageServiceSid = "fake-message-service-sid"

	cases := []struct {
		desc             string
		body             map[string]interface{}
		verificationType string
	}{
		{
			desc: "convert anonymous user to permanent user with email",
			body: map[string]interface{}{
				"email": "test@example.com",
			},
			verificationType: "email_change",
		},
		{
			desc: "convert anonymous user to permanent user with phone",
			body: map[string]interface{}{
				"phone": "1234567890",
			},
			verificationType: "phone_change",
		},
		{
			desc: "convert anonymous user to permanent user with email & password",
			body: map[string]interface{}{
				"email":    "test2@example.com",
				"password": "test-password",
			},
			verificationType: "email_change",
		},
		{
			desc: "convert anonymous user to permanent user with phone & password",
			body: map[string]interface{}{
				"phone":    "1234560000",
				"password": "test-password",
			},
			verificationType: "phone_change",
		},
	}

	for _, c := range cases {
		ts.Run(c.desc, func() {
			// Request body
			var buffer bytes.Buffer
			require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{}))

			req := httptest.NewRequest(http.MethodPost, "/signup", &buffer)
			req.Header.Set("Content-Type", "application/json")

			w := httptest.NewRecorder()

			ts.API.handler.ServeHTTP(w, req)
			require.Equal(ts.T(), http.StatusOK, w.Code)

			signupResponse := &AccessTokenResponse{}
			require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&signupResponse))

			// Add email to anonymous user
			require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(c.body))

			req = httptest.NewRequest(http.MethodPut, "/user", &buffer)
			req.Header.Set("Content-Type", "application/json")
			req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", signupResponse.Token))

			w = httptest.NewRecorder()
			ts.API.handler.ServeHTTP(w, req)
			require.Equal(ts.T(), http.StatusOK, w.Code)

			// Check if anonymous user is still anonymous
			user, err := models.FindUserByID(ts.API.db, signupResponse.User.ID)
			require.NoError(ts.T(), err)
			require.NotEmpty(ts.T(), user)
			require.True(ts.T(), user.IsAnonymous)

			// Check if user has a password set
			if c.body["password"] != nil {
				require.True(ts.T(), user.HasPassword())
			}

			switch c.verificationType {
			case mail.EmailChangeVerification:
				require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
					"token_hash": user.EmailChangeTokenNew,
					"type":       c.verificationType,
				}))
			case phoneChangeVerification:
				require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
					"phone": user.PhoneChange,
					"token": "000000",
					"type":  c.verificationType,
				}))
			}

			req = httptest.NewRequest(http.MethodPost, "/verify", &buffer)
			req.Header.Set("Content-Type", "application/json")

			w = httptest.NewRecorder()
			ts.API.handler.ServeHTTP(w, req)
			require.Equal(ts.T(), http.StatusOK, w.Code)

			data := &AccessTokenResponse{}
			require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))

			// User is a permanent user and not anonymous anymore
			assert.Equal(ts.T(), signupResponse.User.ID, data.User.ID)
			assert.Equal(ts.T(), ts.Config.JWT.Aud, data.User.Aud)
			assert.False(ts.T(), data.User.IsAnonymous)

			// User should have an identity
			assert.Len(ts.T(), data.User.Identities, 1)

			switch c.verificationType {
			case mail.EmailChangeVerification:
				assert.Equal(ts.T(), c.body["email"], data.User.GetEmail())
				assert.Equal(ts.T(), models.JSONMap(models.JSONMap{"provider": "email", "providers": []interface{}{"email"}}), data.User.AppMetaData)
				assert.NotEmpty(ts.T(), data.User.EmailConfirmedAt)
			case phoneChangeVerification:
				assert.Equal(ts.T(), c.body["phone"], data.User.GetPhone())
				assert.Equal(ts.T(), models.JSONMap(models.JSONMap{"provider": "phone", "providers": []interface{}{"phone"}}), data.User.AppMetaData)
				assert.NotEmpty(ts.T(), data.User.PhoneConfirmedAt)
			}
		})
	}
}

func (ts *AnonymousTestSuite) TestRateLimitAnonymousSignups() {
	var buffer bytes.Buffer
	ts.Config.External.AnonymousUsers.Enabled = true

	// It rate limits after 30 requests
	for i := 0; i < int(ts.Config.RateLimitAnonymousUsers); i++ {
		require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{}))
		req := httptest.NewRequest(http.MethodPost, "http://localhost/signup", &buffer)
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("My-Custom-Header", "1.2.3.4")
		w := httptest.NewRecorder()
		ts.API.handler.ServeHTTP(w, req)
		assert.Equal(ts.T(), http.StatusOK, w.Code)
	}

	require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{}))
	req := httptest.NewRequest(http.MethodPost, "http://localhost/signup", &buffer)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("My-Custom-Header", "1.2.3.4")
	w := httptest.NewRecorder()
	ts.API.handler.ServeHTTP(w, req)
	assert.Equal(ts.T(), http.StatusTooManyRequests, w.Code)

	// It ignores X-Forwarded-For by default
	require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{}))
	req.Header.Set("X-Forwarded-For", "1.1.1.1")
	w = httptest.NewRecorder()
	ts.API.handler.ServeHTTP(w, req)
	assert.Equal(ts.T(), http.StatusTooManyRequests, w.Code)

	// It doesn't rate limit a new value for the limited header
	req.Header.Set("My-Custom-Header", "5.6.7.8")
	w = httptest.NewRecorder()
	ts.API.handler.ServeHTTP(w, req)
	assert.Equal(ts.T(), http.StatusBadRequest, w.Code)
}

func (ts *AnonymousTestSuite) TestAdminUpdateAnonymousUser() {
	claims := &AccessTokenClaims{
		Role: "supabase_admin",
	}
	adminJwt, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(ts.Config.JWT.Secret))
	require.NoError(ts.T(), err)

	u1, err := models.NewUser("", "", "", ts.Config.JWT.Aud, nil)
	require.NoError(ts.T(), err)
	u1.IsAnonymous = true
	require.NoError(ts.T(), ts.API.db.Create(u1))

	u2, err := models.NewUser("", "", "", ts.Config.JWT.Aud, nil)
	require.NoError(ts.T(), err)
	u2.IsAnonymous = true
	require.NoError(ts.T(), ts.API.db.Create(u2))

	cases := []struct {
		desc               string
		userId             uuid.UUID
		body               map[string]interface{}
		expected           map[string]interface{}
		expectedIdentities int
	}{
		{
			desc:   "update anonymous user with email and email confirm true",
			userId: u1.ID,
			body: map[string]interface{}{
				"email":         "foo@example.com",
				"email_confirm": true,
			},
			expected: map[string]interface{}{
				"email":        "foo@example.com",
				"is_anonymous": false,
			},
			expectedIdentities: 1,
		},
		{
			desc:   "update anonymous user with email and email confirm false",
			userId: u2.ID,
			body: map[string]interface{}{
				"email":         "bar@example.com",
				"email_confirm": false,
			},
			expected: map[string]interface{}{
				"email":        "bar@example.com",
				"is_anonymous": true,
			},
			expectedIdentities: 1,
		},
	}

	for _, c := range cases {
		ts.Run(c.desc, func() {
			// Request body
			var buffer bytes.Buffer
			require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(c.body))

			req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/admin/users/%s", c.userId), &buffer)
			req.Header.Set("Content-Type", "application/json")
			req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", adminJwt))

			w := httptest.NewRecorder()
			ts.API.handler.ServeHTTP(w, req)
			require.Equal(ts.T(), http.StatusOK, w.Code)

			var data models.User
			require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))

			require.NotNil(ts.T(), data)
			require.Len(ts.T(), data.Identities, c.expectedIdentities)

			actual := map[string]interface{}{
				"email":        data.GetEmail(),
				"is_anonymous": data.IsAnonymous,
			}

			require.Equal(ts.T(), c.expected, actual)
		})
	}
}
