package identity

import (
	"context"
	"fmt"
	"testing"
	"time"

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

	"github.com/grafana/grafana/pkg/apiserver/rest"
	"github.com/grafana/grafana/pkg/services/featuremgmt"
	"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"
)

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

	// TODO: Add rest.Mode4 when it's supported
	modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3}
	for _, mode := range modes {
		t.Run(fmt.Sprintf("Team 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{
					"teams.iam.grafana.app": {
						DualWriterMode: mode,
					},
				},
				EnableFeatureToggles: []string{
					featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs,
					featuremgmt.FlagKubernetesAuthnMutation,
				},
			})

			doTeamCRUDTestsUsingTheNewAPIs(t, helper)

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

func doTeamCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHelper) {
	t.Run("should create/get/update/delete team using the new APIs as a GrafanaAdmin", func(t *testing.T) {
		ctx := context.Background()

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

		// Create the team
		created, err := teamClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/team-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 Team 1", createdSpec["title"])
		require.Equal(t, "testteam1@example123.com", createdSpec["email"])
		require.Equal(t, false, createdSpec["provisioned"])

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

		// Get the team
		fetched, err := teamClient.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 Team 1", fetchedSpec["title"])
		require.Equal(t, "testteam1@example123.com", fetchedSpec["email"])
		require.Equal(t, false, fetchedSpec["provisioned"])

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

		// Update the team
		updatedTeam, err := teamClient.Resource.Update(ctx, helper.LoadYAMLOrJSONFile("testdata/team-test-update-v0.yaml"), metav1.UpdateOptions{})
		require.NoError(t, err)
		require.NotNil(t, updatedTeam)

		updatedSpec := updatedTeam.Object["spec"].(map[string]interface{})
		require.Equal(t, "Test Team 2", updatedSpec["title"])
		require.Equal(t, "testteam2@example123.com", updatedSpec["email"])
		require.Equal(t, false, updatedSpec["provisioned"])

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

		verifiedSpec := verifiedTeam.Object["spec"].(map[string]interface{})
		require.Equal(t, "Test Team 2", verifiedSpec["title"])
		require.Equal(t, "testteam2@example123.com", verifiedSpec["email"])
		require.Equal(t, false, verifiedSpec["provisioned"])

		// Delete the team
		err = teamClient.Resource.Delete(ctx, createdUID, metav1.DeleteOptions{})
		require.NoError(t, err)

		_, err = teamClient.Resource.Get(ctx, createdUID, metav1.GetOptions{})
		require.Error(t, err)
		var statusErr *errors.StatusError
		require.ErrorAs(t, err, &statusErr)
		require.Equal(t, "Failure", statusErr.ErrStatus.Status)
		require.Contains(t, statusErr.ErrStatus.Message, "not found")
	})

	t.Run("should not be able to create team 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()
				teamClient := helper.GetResourceClient(apis.ResourceClientArgs{
					User:      user,
					Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
					GVR:       gvrTeams,
				})

				_, err := teamClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/team-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 team without a title", func(t *testing.T) {
		ctx := context.Background()
		teamClient := helper.GetResourceClient(apis.ResourceClientArgs{
			User:      helper.Org1.Admin,
			Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
			GVR:       gvrTeams,
		})

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

		_, err := teamClient.Resource.Create(ctx, toCreate, 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, "the team must have a title")
	})

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

		toCreate := helper.LoadYAMLOrJSONFile("testdata/team-test-provisioned-v0.yaml")

		_, err := teamClient.Resource.Create(ctx, toCreate, 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, "provisioned teams are only allowed for service accounts")
	})

	t.Run("should not be able to set externalUID when not provisioned", func(t *testing.T) {
		ctx := context.Background()
		teamClient := helper.GetResourceClient(apis.ResourceClientArgs{
			User:      helper.Org1.Admin,
			Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
			GVR:       gvrTeams,
		})

		toCreate := helper.LoadYAMLOrJSONFile("testdata/team-test-external-uid-without-provisioned-v0.yaml")

		_, err := teamClient.Resource.Create(ctx, toCreate, 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, "externalUID is only allowed for provisioned teams")
	})

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

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

		created, err := teamClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/team-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, "Team with GenerateName", createdSpec["title"])
		require.Equal(t, false, createdSpec["provisioned"])

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

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

		fetchedSpec := fetched.Object["spec"].(map[string]interface{})
		require.Equal(t, "Team with GenerateName", fetchedSpec["title"])
		require.Equal(t, false, fetchedSpec["provisioned"])

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

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

	t.Run("should list teams correctly", func(t *testing.T) {
		ctx := context.Background()

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

		// For ensuring that it is able to list a team which has external_uid = null and is_provisioned = null
		// only matters when legacy storage is involved
		env := helper.GetEnv()
		res, err := env.SQLStore.GetSqlxSession().Exec(ctx, "INSERT INTO team (org_id, uid, name, email, is_provisioned, external_uid, created, updated) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
			helper.Org1.Admin.Identity.GetOrgID(), "t000000001", "List Team 1", "list-team-1@example.com", nil, nil, time.Now(), time.Now())
		require.NoError(t, err)
		require.NotNil(t, res)

		list, err := teamClient.Resource.List(ctx, metav1.ListOptions{})
		require.NoError(t, err)
		require.NotNil(t, list)

		// Cleanup
		_, err = env.SQLStore.GetSqlxSession().Exec(ctx, "DELETE FROM team WHERE uid = ?", "t000000001")
		require.NoError(t, err)
	})
}

func doTeamCRUDTestsUsingTheLegacyAPIs(t *testing.T, helper *apis.K8sTestHelper, mode rest.DualWriterMode) {
	t.Run("should create team using legacy APIs and get/update/delete it using the new APIs", func(t *testing.T) {
		ctx := context.Background()

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

		legacyTeamPayload := `{
			"name": "Test Team 2",
			"email": "testteam2@example.com"
		}`

		type legacyCreateResponse struct {
			UID string `json:"uid"`
			ID  int64  `json:"teamId"`
		}

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

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

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

		teamSpec := team.Object["spec"].(map[string]interface{})
		require.Equal(t, "Test Team 2", teamSpec["title"])
		require.Equal(t, "testteam2@example.com", teamSpec["email"])
		require.Equal(t, false, teamSpec["provisioned"])

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

		// Updating the team is not supported in Mode2 if the team has been created using the legacy APIs
		if mode < rest.Mode2 {
			team.Object["spec"].(map[string]interface{})["title"] = "Updated Test Team 2"
			team.Object["spec"].(map[string]interface{})["email"] = "updated@example.com"

			updatedTeam, err := teamClient.Resource.Update(ctx, team, metav1.UpdateOptions{})
			require.NoError(t, err)
			require.NotNil(t, updatedTeam)

			updatedSpec := updatedTeam.Object["spec"].(map[string]interface{})
			require.Equal(t, "Updated Test Team 2", updatedSpec["title"])
			require.Equal(t, "updated@example.com", updatedSpec["email"])
			require.Equal(t, false, updatedSpec["provisioned"])

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

			verifiedSpec := verifiedTeam.Object["spec"].(map[string]interface{})
			require.Equal(t, "Updated Test Team 2", verifiedSpec["title"])
			require.Equal(t, "updated@example.com", verifiedSpec["email"])
			require.Equal(t, false, verifiedSpec["provisioned"])
		}

		// Delete the team
		err = teamClient.Resource.Delete(ctx, rsp.Result.UID, metav1.DeleteOptions{})
		require.NoError(t, err)

		_, err = teamClient.Resource.Get(ctx, rsp.Result.UID, metav1.GetOptions{})
		require.Error(t, err)
		var statusErr *errors.StatusError
		require.ErrorAs(t, err, &statusErr)
		require.Equal(t, "Failure", statusErr.ErrStatus.Status)
		require.Contains(t, statusErr.ErrStatus.Message, "not found")
	})
}
