package coderd_test

import (
	"context"
	"fmt"
	"net/http"
	"testing"

	"github.com/google/uuid"
	"github.com/stretchr/testify/require"
	"golang.org/x/xerrors"

	"github.com/coder/coder/v2/coderd/coderdtest"
	"github.com/coder/coder/v2/coderd/database"
	"github.com/coder/coder/v2/coderd/database/dbtestutil"
	"github.com/coder/coder/v2/coderd/notifications"
	"github.com/coder/coder/v2/codersdk"
	"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
	"github.com/coder/coder/v2/testutil"
)

func createOpts(t *testing.T) *coderdenttest.Options {
	t.Helper()

	dt := coderdtest.DeploymentValues(t)
	dt.Experiments = []string{string(codersdk.ExperimentNotifications)}
	return &coderdenttest.Options{
		Options: &coderdtest.Options{
			DeploymentValues: dt,
		},
	}
}

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

	t.Run("Happy path", func(t *testing.T) {
		t.Parallel()

		if !dbtestutil.WillUsePostgres() {
			t.Skip("This test requires postgres; it relies on read from and writing to the notification_templates table")
		}

		ctx := testutil.Context(t, testutil.WaitSuperLong)
		api, _ := coderdenttest.New(t, createOpts(t))

		var (
			method     = string(database.NotificationMethodSmtp)
			templateID = notifications.TemplateWorkspaceDeleted
		)

		// Given: a template whose method is initially empty (i.e. deferring to the global method value).
		template, err := getTemplateByID(t, ctx, api, templateID)
		require.NoError(t, err)
		require.NotNil(t, template)
		require.Empty(t, template.Method)

		// When: calling the API to update the method.
		require.NoError(t, api.UpdateNotificationTemplateMethod(ctx, notifications.TemplateWorkspaceDeleted, method), "initial request to set the method failed")

		// Then: the method should be set.
		template, err = getTemplateByID(t, ctx, api, templateID)
		require.NoError(t, err)
		require.NotNil(t, template)
		require.Equal(t, method, template.Method)
	})

	t.Run("Insufficient permissions", func(t *testing.T) {
		t.Parallel()

		if !dbtestutil.WillUsePostgres() {
			t.Skip("This test requires postgres; it relies on read from and writing to the notification_templates table")
		}

		ctx := testutil.Context(t, testutil.WaitSuperLong)

		// Given: the first user which has an "owner" role, and another user which does not.
		api, firstUser := coderdenttest.New(t, createOpts(t))
		anotherClient, _ := coderdtest.CreateAnotherUser(t, api, firstUser.OrganizationID)

		// When: calling the API as an unprivileged user.
		err := anotherClient.UpdateNotificationTemplateMethod(ctx, notifications.TemplateWorkspaceDeleted, string(database.NotificationMethodWebhook))

		// Then: the request is denied because of insufficient permissions.
		var sdkError *codersdk.Error
		require.Error(t, err)
		require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
		require.Equal(t, http.StatusNotFound, sdkError.StatusCode())
		require.Equal(t, "Resource not found or you do not have access to this resource", sdkError.Response.Message)
	})

	t.Run("Invalid notification method", func(t *testing.T) {
		t.Parallel()

		if !dbtestutil.WillUsePostgres() {
			t.Skip("This test requires postgres; it relies on read from and writing to the notification_templates table")
		}

		ctx := testutil.Context(t, testutil.WaitSuperLong)

		// Given: the first user which has an "owner" role
		api, _ := coderdenttest.New(t, createOpts(t))

		// When: calling the API with an invalid method.
		const method = "nope"

		// nolint:gocritic // Using an owner-scope user is kinda the point.
		err := api.UpdateNotificationTemplateMethod(ctx, notifications.TemplateWorkspaceDeleted, method)

		// Then: the request is invalid because of the unacceptable method.
		var sdkError *codersdk.Error
		require.Error(t, err)
		require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
		require.Equal(t, http.StatusBadRequest, sdkError.StatusCode())
		require.Equal(t, "Invalid request to update notification template method", sdkError.Response.Message)
		require.Len(t, sdkError.Response.Validations, 1)
		require.Equal(t, "method", sdkError.Response.Validations[0].Field)
		require.Equal(t, fmt.Sprintf("%q is not a valid method; smtp, webhook are the available options", method), sdkError.Response.Validations[0].Detail)
	})

	t.Run("Not modified", func(t *testing.T) {
		t.Parallel()

		if !dbtestutil.WillUsePostgres() {
			t.Skip("This test requires postgres; it relies on read from and writing to the notification_templates table")
		}

		ctx := testutil.Context(t, testutil.WaitSuperLong)
		api, _ := coderdenttest.New(t, createOpts(t))

		var (
			method     = string(database.NotificationMethodSmtp)
			templateID = notifications.TemplateWorkspaceDeleted
		)

		template, err := getTemplateByID(t, ctx, api, templateID)
		require.NoError(t, err)
		require.NotNil(t, template)

		// Given: a template whose method is initially empty (i.e. deferring to the global method value).
		require.Empty(t, template.Method)

		// When: calling the API to update the method, it should set it.
		require.NoError(t, api.UpdateNotificationTemplateMethod(ctx, notifications.TemplateWorkspaceDeleted, method), "initial request to set the method failed")
		template, err = getTemplateByID(t, ctx, api, templateID)
		require.NoError(t, err)
		require.NotNil(t, template)
		require.Equal(t, method, template.Method)

		// Then: when calling the API again with the same method, the method will remain unchanged.
		require.NoError(t, api.UpdateNotificationTemplateMethod(ctx, notifications.TemplateWorkspaceDeleted, method), "second request to set the method failed")
		template, err = getTemplateByID(t, ctx, api, templateID)
		require.NoError(t, err)
		require.NotNil(t, template)
		require.Equal(t, method, template.Method)
	})
}

// nolint:revive // t takes precedence.
func getTemplateByID(t *testing.T, ctx context.Context, api *codersdk.Client, id uuid.UUID) (*codersdk.NotificationTemplate, error) {
	t.Helper()

	var template codersdk.NotificationTemplate
	templates, err := api.GetSystemNotificationTemplates(ctx)
	if err != nil {
		return nil, err
	}

	for _, tmpl := range templates {
		if tmpl.ID == id {
			template = tmpl
		}
	}

	if template.ID == uuid.Nil {
		return nil, xerrors.Errorf("template not found: %q", id.String())
	}

	return &template, nil
}
