package identity

import (
	"context"
	"fmt"
	"testing"

	"github.com/stretchr/testify/require"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"

	"github.com/grafana/grafana/pkg/apiserver/rest"
	"github.com/grafana/grafana/pkg/services/featuremgmt"
	"github.com/grafana/grafana/pkg/services/serviceaccounts"
	"github.com/grafana/grafana/pkg/setting"
	"github.com/grafana/grafana/pkg/tests/apis"
	"github.com/grafana/grafana/pkg/tests/testinfra"
	"github.com/grafana/grafana/pkg/util/testutil"
)

var gvrServiceAccounts = schema.GroupVersionResource{
	Group:    "iam.grafana.app",
	Version:  "v0alpha1",
	Resource: "serviceaccounts",
}

func TestIntegrationServiceAccounts(t *testing.T) {
	testutil.SkipIntegrationTestInShortMode(t)

	// TODO: Figure out why rest.Mode4 is failing
	modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3}
	for _, mode := range modes {
		t.Run(fmt.Sprintf("Service Account CRUD operations with dual writer mode %d", mode), func(t *testing.T) {
			helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
				AppModeProduction:    false,
				DisableAnonymous:     true,
				APIServerStorageType: "unified",
				UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
					"serviceaccounts.iam.grafana.app": {
						DualWriterMode: mode,
					},
				},
				EnableFeatureToggles: []string{
					featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs,
					featuremgmt.FlagKubernetesAuthnMutation,
				},
			})
			doServiceAccountCRUDTestsUsingTheNewAPIs(t, helper)

			if mode < 3 {
				doServiceAccountCRUDTestsUsingTheLegacyAPIs(t, helper)
			}
		})
	}
}

func doServiceAccountCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHelper) {
	t.Run("should create service account and get it using the new APIs as admin", func(t *testing.T) {
		ctx := context.Background()

		saClient := helper.GetResourceClient(apis.ResourceClientArgs{
			User:      helper.Org1.Admin,
			Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
			GVR:       gvrServiceAccounts,
		})

		created, err := saClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/serviceaccount-test-create-v0.yaml"), metav1.CreateOptions{})
		require.NoError(t, err)
		require.NotNil(t, created)

		createdSpec := created.Object["spec"].(map[string]interface{})
		require.Equal(t, "Test Service Account 1", createdSpec["title"])
		require.Equal(t, false, createdSpec["disabled"])
		require.Empty(t, createdSpec["plugin"])

		createdUID := created.GetName()
		require.NotEmpty(t, createdUID)

		_, err = saClient.Resource.List(ctx, metav1.ListOptions{})
		require.NoError(t, err)

		fetched, err := saClient.Resource.Get(ctx, createdUID, metav1.GetOptions{})
		require.NoError(t, err)
		require.NotNil(t, fetched)

		fetchedSpec := fetched.Object["spec"].(map[string]interface{})
		require.Equal(t, "Test Service Account 1", fetchedSpec["title"])
		require.Equal(t, false, fetchedSpec["disabled"])
		require.Empty(t, fetchedSpec["plugin"])

		require.Equal(t, createdUID, fetched.GetName())
		require.Equal(t, "default", fetched.GetNamespace())

		err = saClient.Resource.Delete(ctx, createdUID, metav1.DeleteOptions{})
		require.NoError(t, err)

		_, err = saClient.Resource.Get(ctx, createdUID, metav1.GetOptions{})
		require.Error(t, err)
		var statusErr *errors.StatusError
		require.ErrorAs(t, err, &statusErr)
		require.Equal(t, int32(404), statusErr.ErrStatus.Code)
	})

	t.Run("should not be able to create service account when using a user with insufficient permissions", func(t *testing.T) {
		for _, user := range []apis.User{
			helper.Org1.Editor,
			helper.Org1.Viewer,
		} {
			t.Run(fmt.Sprintf("with basic role_%s", user.Identity.GetOrgRole()), func(t *testing.T) {
				ctx := context.Background()
				saClient := helper.GetResourceClient(apis.ResourceClientArgs{
					User:      user,
					Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
					GVR:       gvrServiceAccounts,
				})

				_, err := saClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/serviceaccount-test-create-v0.yaml"), metav1.CreateOptions{})
				require.Error(t, err)
				var statusErr *errors.StatusError
				require.ErrorAs(t, err, &statusErr)
				require.Equal(t, int32(403), statusErr.ErrStatus.Code)
			})
		}
	})

	t.Run("should not be able to create service account with invalid role", func(t *testing.T) {
		ctx := context.Background()
		saClient := helper.GetResourceClient(apis.ResourceClientArgs{
			User:      helper.Org1.Admin,
			Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
			GVR:       gvrServiceAccounts,
		})

		saToCreate := helper.LoadYAMLOrJSONFile("testdata/serviceaccount-test-invalid-role-v0.yaml")

		_, err := saClient.Resource.Create(ctx, saToCreate, metav1.CreateOptions{})
		require.Error(t, err)
		var statusErr *errors.StatusError
		require.ErrorAs(t, err, &statusErr)
		require.Equal(t, int32(400), statusErr.ErrStatus.Code)
		require.Contains(t, statusErr.ErrStatus.Message, "invalid role: InvalidRole")
	})

	t.Run("should not be able to create service account without a title", func(t *testing.T) {
		ctx := context.Background()
		saClient := helper.GetResourceClient(apis.ResourceClientArgs{
			User:      helper.Org1.Admin,
			Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
			GVR:       gvrServiceAccounts,
		})

		saToCreate := helper.LoadYAMLOrJSONFile("testdata/serviceaccount-test-no-title-v0.yaml")

		_, err := saClient.Resource.Create(ctx, saToCreate, metav1.CreateOptions{})
		require.Error(t, err)
		var statusErr *errors.StatusError
		require.ErrorAs(t, err, &statusErr)
		require.Equal(t, int32(400), statusErr.ErrStatus.Code)
		require.Contains(t, statusErr.ErrStatus.Message, "service account must have a title")
	})

	t.Run("should not be able to create external service account as a user", func(t *testing.T) {
		ctx := context.Background()
		saClient := helper.GetResourceClient(apis.ResourceClientArgs{
			User:      helper.Org1.Admin,
			Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
			GVR:       gvrServiceAccounts,
		})

		saToCreate := helper.LoadYAMLOrJSONFile("testdata/serviceaccount-test-external-v0.yaml")

		_, err := saClient.Resource.Create(ctx, saToCreate, metav1.CreateOptions{})
		require.Error(t, err)
		var statusErr *errors.StatusError
		require.ErrorAs(t, err, &statusErr)
		require.Equal(t, int32(403), statusErr.ErrStatus.Code)
		require.Contains(t, statusErr.ErrStatus.Message, "only service identities can create external service accounts")
	})

	t.Run("should create service account with generateName and get it using the new APIs as a GrafanaAdmin", func(t *testing.T) {
		ctx := context.Background()

		saClient := helper.GetResourceClient(apis.ResourceClientArgs{
			User:      helper.Org1.Admin,
			Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
			GVR:       gvrServiceAccounts,
		})

		created, err := saClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/serviceaccount-test-generate-name-v0.yaml"), metav1.CreateOptions{})
		require.NoError(t, err)
		require.NotNil(t, created)

		createdSpec := created.Object["spec"].(map[string]interface{})
		require.Equal(t, "Test Service Account with GenerateName", createdSpec["title"])
		require.Equal(t, false, createdSpec["disabled"])
		require.Empty(t, createdSpec["plugin"])

		createdUID := created.GetName()
		require.NotEmpty(t, createdUID)
		require.Contains(t, createdUID, "sa-")

		_, err = saClient.Resource.List(ctx, metav1.ListOptions{})
		require.NoError(t, err)

		fetched, err := saClient.Resource.Get(ctx, createdUID, metav1.GetOptions{})
		require.NoError(t, err)
		require.NotNil(t, fetched)

		fetchedSpec := fetched.Object["spec"].(map[string]interface{})
		require.Equal(t, "Test Service Account with GenerateName", fetchedSpec["title"])
		require.Equal(t, false, fetchedSpec["disabled"])
		require.Empty(t, fetchedSpec["plugin"])

		require.Equal(t, createdUID, fetched.GetName())
		require.Equal(t, "default", fetched.GetNamespace())
	})
}

func doServiceAccountCRUDTestsUsingTheLegacyAPIs(t *testing.T, helper *apis.K8sTestHelper) {
	t.Run("should create service account using legacy APIs and get it using the new APIs", func(t *testing.T) {
		ctx := context.Background()
		saClient := helper.GetResourceClient(apis.ResourceClientArgs{
			User:      helper.Org1.Admin,
			Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
			GVR:       gvrServiceAccounts,
		})

		legacySAPayload := `{
			"name": "Test Service Account 2",
			"role": "Viewer"
		}`

		rsp := apis.DoRequest(helper, apis.RequestParams{
			User:   helper.Org1.Admin,
			Method: "POST",
			Path:   "/api/serviceaccounts",
			Body:   []byte(legacySAPayload),
		}, &serviceaccounts.ServiceAccountDTO{})

		require.NotNil(t, rsp)
		require.Equal(t, 201, rsp.Response.StatusCode)
		require.NotEmpty(t, rsp.Result.UID)

		sa, err := saClient.Resource.Get(ctx, rsp.Result.UID, metav1.GetOptions{})
		require.NoError(t, err)
		require.NotNil(t, sa)

		saSpec := sa.Object["spec"].(map[string]interface{})
		require.Equal(t, "Test Service Account 2", saSpec["title"])
		require.Equal(t, false, saSpec["disabled"])
		require.Empty(t, saSpec["plugin"])

		require.Equal(t, rsp.Result.UID, sa.GetName())
		require.Equal(t, "default", sa.GetNamespace())
	})

	t.Run("should create service account using legacy APIs and delete it using the new APIs", func(t *testing.T) {
		ctx := context.Background()
		saClient := helper.GetResourceClient(apis.ResourceClientArgs{
			User:      helper.Org1.Admin,
			Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
			GVR:       gvrServiceAccounts,
		})

		legacySAPayload := `{
			"name": "Test Service Account to delete",
			"role": "Editor"
		}`

		rsp := apis.DoRequest(helper, apis.RequestParams{
			User:   helper.Org1.Admin,
			Method: "POST",
			Path:   "/api/serviceaccounts",
			Body:   []byte(legacySAPayload),
		}, &serviceaccounts.ServiceAccountDTO{})

		require.NotNil(t, rsp)
		require.Equal(t, 201, rsp.Response.StatusCode)
		require.NotEmpty(t, rsp.Result.UID)

		_, err := saClient.Resource.Get(ctx, rsp.Result.UID, metav1.GetOptions{})
		require.NoError(t, err)

		err = saClient.Resource.Delete(ctx, rsp.Result.UID, metav1.DeleteOptions{})
		require.NoError(t, err)

		_, err = saClient.Resource.Get(ctx, rsp.Result.UID, metav1.GetOptions{})
		require.Error(t, err)
		var statusErr *errors.StatusError
		require.ErrorAs(t, err, &statusErr)
		require.Equal(t, int32(404), statusErr.ErrStatus.Code)
	})
}
