package api

import (
	"encoding/json"
	"net/http"
	"testing"

	"net/http/httptest"

	"github.com/pkg/errors"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"
	"github.com/supabase/auth/internal/conf"
	"github.com/supabase/auth/internal/hooks"
	"github.com/supabase/auth/internal/models"
	"github.com/supabase/auth/internal/storage"

	"gopkg.in/h2non/gock.v1"
)

var handleApiRequest func(*http.Request) (*http.Response, error)

type HooksTestSuite struct {
	suite.Suite
	API      *API
	Config   *conf.GlobalConfiguration
	TestUser *models.User
}

type MockHttpClient struct {
	mock.Mock
}

func (m *MockHttpClient) Do(req *http.Request) (*http.Response, error) {
	return handleApiRequest(req)
}

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

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

	suite.Run(t, ts)
}

func (ts *HooksTestSuite) SetupTest() {
	models.TruncateAll(ts.API.db)
	u, err := models.NewUser("123456789", "testemail@gmail.com", "securetestpassword", ts.Config.JWT.Aud, nil)
	require.NoError(ts.T(), err, "Error creating test user model")
	require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user")
	ts.TestUser = u
}

func (ts *HooksTestSuite) TestRunHTTPHook() {
	// setup mock requests for hooks
	defer gock.OffAll()

	input := hooks.SendSMSInput{
		User: ts.TestUser,
		SMS: hooks.SMS{
			OTP: "123456",
		},
	}
	testURL := "http://localhost:54321/functions/v1/custom-sms-sender"
	ts.Config.Hook.SendSMS.URI = testURL

	unsuccessfulResponse := hooks.AuthHookError{
		HTTPCode: http.StatusUnprocessableEntity,
		Message:  "test error",
	}

	testCases := []struct {
		description  string
		expectError  bool
		mockResponse hooks.AuthHookError
	}{
		{
			description:  "Hook returns success",
			expectError:  false,
			mockResponse: hooks.AuthHookError{},
		},
		{
			description:  "Hook returns error",
			expectError:  true,
			mockResponse: unsuccessfulResponse,
		},
	}

	gock.New(ts.Config.Hook.SendSMS.URI).
		Post("/").
		MatchType("json").
		Reply(http.StatusOK).
		JSON(hooks.SendSMSOutput{})

	gock.New(ts.Config.Hook.SendSMS.URI).
		Post("/").
		MatchType("json").
		Reply(http.StatusUnprocessableEntity).
		JSON(hooks.SendSMSOutput{HookError: unsuccessfulResponse})

	for _, tc := range testCases {
		ts.Run(tc.description, func() {
			req, _ := http.NewRequest("POST", ts.Config.Hook.SendSMS.URI, nil)
			body, err := ts.API.runHTTPHook(req, ts.Config.Hook.SendSMS, &input)

			if !tc.expectError {
				require.NoError(ts.T(), err)
			} else {
				require.Error(ts.T(), err)
				if body != nil {
					var output hooks.SendSMSOutput
					require.NoError(ts.T(), json.Unmarshal(body, &output))
					require.Equal(ts.T(), unsuccessfulResponse.HTTPCode, output.HookError.HTTPCode)
					require.Equal(ts.T(), unsuccessfulResponse.Message, output.HookError.Message)
				}
			}
		})
	}
	require.True(ts.T(), gock.IsDone())
}

func (ts *HooksTestSuite) TestShouldRetryWithRetryAfterHeader() {
	defer gock.OffAll()

	input := hooks.SendSMSInput{
		User: ts.TestUser,
		SMS: hooks.SMS{
			OTP: "123456",
		},
	}
	testURL := "http://localhost:54321/functions/v1/custom-sms-sender"
	ts.Config.Hook.SendSMS.URI = testURL

	gock.New(testURL).
		Post("/").
		MatchType("json").
		Reply(http.StatusTooManyRequests).
		SetHeader("retry-after", "true").SetHeader("content-type", "application/json")

	// Simulate an additional response for the retry attempt
	gock.New(testURL).
		Post("/").
		MatchType("json").
		Reply(http.StatusOK).
		JSON(hooks.SendSMSOutput{}).SetHeader("content-type", "application/json")

	// Simulate the original HTTP request which triggered the hook
	req, err := http.NewRequest("POST", "http://localhost:9998/otp", nil)
	require.NoError(ts.T(), err)

	body, err := ts.API.runHTTPHook(req, ts.Config.Hook.SendSMS, &input)
	require.NoError(ts.T(), err)

	var output hooks.SendSMSOutput
	err = json.Unmarshal(body, &output)
	require.NoError(ts.T(), err, "Unmarshal should not fail")

	// Ensure that all expected HTTP interactions (mocks) have been called
	require.True(ts.T(), gock.IsDone(), "Expected all mocks to have been called including retry")
}

func (ts *HooksTestSuite) TestShouldReturnErrorForNonJSONContentType() {
	defer gock.OffAll()

	input := hooks.SendSMSInput{
		User: ts.TestUser,
		SMS: hooks.SMS{
			OTP: "123456",
		},
	}
	testURL := "http://localhost:54321/functions/v1/custom-sms-sender"
	ts.Config.Hook.SendSMS.URI = testURL

	gock.New(testURL).
		Post("/").
		MatchType("json").
		Reply(http.StatusOK).
		SetHeader("content-type", "text/plain")

	req, err := http.NewRequest("POST", "http://localhost:9999/otp", nil)
	require.NoError(ts.T(), err)

	_, err = ts.API.runHTTPHook(req, ts.Config.Hook.SendSMS, &input)
	require.Error(ts.T(), err, "Expected an error due to wrong content type")
	require.Contains(ts.T(), err.Error(), "Invalid JSON response.")

	require.True(ts.T(), gock.IsDone(), "Expected all mocks to have been called")
}

func (ts *HooksTestSuite) TestInvokeHookIntegration() {
	// We use the Send Email Hook as illustration
	defer gock.OffAll()
	hookFunctionSQL := `
        create or replace function invoke_test(input jsonb)
        returns json as $$
        begin
            return input;
        end; $$ language plpgsql;`
	require.NoError(ts.T(), ts.API.db.RawQuery(hookFunctionSQL).Exec())

	testHTTPUri := "http://myauthservice.com/signup"
	testHTTPSUri := "https://myauthservice.com/signup"
	testPGUri := "pg-functions://postgres/auth/invoke_test"
	successOutput := map[string]interface{}{}
	authEndpoint := "https://app.myapp.com/otp"
	gock.New(testHTTPUri).
		Post("/").
		MatchType("json").
		Reply(http.StatusOK).
		JSON(successOutput).SetHeader("content-type", "application/json")

	gock.New(testHTTPSUri).
		Post("/").
		MatchType("json").
		Reply(http.StatusOK).
		JSON(successOutput).SetHeader("content-type", "application/json")

	tests := []struct {
		description   string
		conn          *storage.Connection
		request       *http.Request
		input         any
		output        any
		uri           string
		expectedError error
	}{
		{
			description: "HTTP endpoint success",
			conn:        nil,
			request:     httptest.NewRequest("POST", authEndpoint, nil),
			input:       &hooks.SendEmailInput{},
			output:      &hooks.SendEmailOutput{},
			uri:         testHTTPUri,
		},
		{
			description: "HTTPS endpoint success",
			conn:        nil,
			request:     httptest.NewRequest("POST", authEndpoint, nil),
			input:       &hooks.SendEmailInput{},
			output:      &hooks.SendEmailOutput{},
			uri:         testHTTPSUri,
		},
		{
			description: "PostgreSQL function success",
			conn:        ts.API.db,
			request:     httptest.NewRequest("POST", authEndpoint, nil),
			input:       &hooks.SendEmailInput{},
			output:      &hooks.SendEmailOutput{},
			uri:         testPGUri,
		},
		{
			description:   "Unsupported protocol error",
			conn:          nil,
			request:       httptest.NewRequest("POST", authEndpoint, nil),
			input:         &hooks.SendEmailInput{},
			output:        &hooks.SendEmailOutput{},
			uri:           "ftp://example.com/path",
			expectedError: errors.New("unsupported protocol: \"ftp://example.com/path\" only postgres hooks and HTTPS functions are supported at the moment"),
		},
	}

	var err error
	for _, tc := range tests {
		// Set up hook config
		ts.Config.Hook.SendEmail.Enabled = true
		ts.Config.Hook.SendEmail.URI = tc.uri
		require.NoError(ts.T(), ts.Config.Hook.SendEmail.PopulateExtensibilityPoint())

		ts.Run(tc.description, func() {
			err = ts.API.invokeHook(tc.conn, tc.request, tc.input, tc.output)
			if tc.expectedError != nil {
				require.EqualError(ts.T(), err, tc.expectedError.Error())
			} else {
				require.NoError(ts.T(), err)
			}
		})

	}
	// Ensure that all expected HTTP interactions (mocks) have been called
	require.True(ts.T(), gock.IsDone(), "Expected all mocks to have been called including retry")
}
