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/apis/meta/v1/unstructured"

	"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 TestIntegrationTeamBindings(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 binding 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{
					"teambindings.iam.grafana.app": {
						DualWriterMode: mode,
					},
				},
				EnableFeatureToggles: []string{
					featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs,
					featuremgmt.FlagKubernetesAuthnMutation,
				},
			})

			ctx := context.Background()

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

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

			// Create a user
			userClient := helper.GetResourceClient(apis.ResourceClientArgs{
				User:      helper.Org1.Admin,
				Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
				GVR:       gvrUsers,
			})

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

			doTeamBindingCRUDTestsUsingTheNewAPIs(t, helper, team, user)

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

func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHelper, team *unstructured.Unstructured, user *unstructured.Unstructured) {
	t.Run("should create/update/get/delete team binding using the new APIs", func(t *testing.T) {
		ctx := context.Background()

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

		// Create the team binding
		toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
		toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName()
		toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName()
		created, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{})
		require.NoError(t, err)
		require.NotNil(t, created)

		createdSpec := created.Object["spec"].(map[string]interface{})
		require.Equal(t, user.GetName(), createdSpec["subject"].(map[string]interface{})["name"])
		require.Equal(t, team.GetName(), createdSpec["teamRef"].(map[string]interface{})["name"])
		require.Equal(t, "admin", createdSpec["permission"])
		require.Equal(t, false, createdSpec["external"])

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

		// Get the team binding
		fetched, err := teamBindingClient.Resource.Get(ctx, createdUID, metav1.GetOptions{})
		require.NoError(t, err)
		require.NotNil(t, fetched)

		fetchedSpec := fetched.Object["spec"].(map[string]interface{})
		require.Equal(t, user.GetName(), fetchedSpec["subject"].(map[string]interface{})["name"])
		require.Equal(t, team.GetName(), fetchedSpec["teamRef"].(map[string]interface{})["name"])
		require.Equal(t, "admin", fetchedSpec["permission"])
		require.Equal(t, false, fetchedSpec["external"])
		require.Equal(t, createdUID, fetched.GetName())

		// Update the team binding
		toUpdate := toCreate.DeepCopy()
		toUpdate.Object["spec"].(map[string]interface{})["permission"] = "member"
		updated, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{})
		require.NoError(t, err)
		require.NotNil(t, updated)

		updatedSpec := updated.Object["spec"].(map[string]interface{})
		require.Equal(t, createdUID, updated.GetName())
		require.Equal(t, user.GetName(), updatedSpec["subject"].(map[string]interface{})["name"])
		require.Equal(t, team.GetName(), updatedSpec["teamRef"].(map[string]interface{})["name"])
		require.Equal(t, "member", updatedSpec["permission"])
		require.Equal(t, false, updatedSpec["external"])

		// Get the team binding
		fetched, err = teamBindingClient.Resource.Get(ctx, createdUID, metav1.GetOptions{})
		require.NoError(t, err)
		require.NotNil(t, fetched)

		fetchedSpec = fetched.Object["spec"].(map[string]interface{})
		require.Equal(t, user.GetName(), fetchedSpec["subject"].(map[string]interface{})["name"])
		require.Equal(t, team.GetName(), fetchedSpec["teamRef"].(map[string]interface{})["name"])
		require.Equal(t, "member", fetchedSpec["permission"])
		require.Equal(t, false, fetchedSpec["external"])
		require.Equal(t, createdUID, fetched.GetName())

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

		// Verify the team binding is deleted
		_, err = teamBindingClient.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)
		require.Contains(t, statusErr.ErrStatus.Message, "not found")
	})

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

				toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
				toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName()
				toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName()
				_, err := teamBindingClient.Resource.Create(ctx, toCreate, 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 binding without a subject", func(t *testing.T) {
		ctx := context.Background()
		teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{
			User:      helper.Org1.Admin,
			Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
			GVR:       gvrTeamBindings,
		})

		toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
		toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = ""
		toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName()

		_, err := teamBindingClient.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, "subject is required")
	})

	t.Run("should not be able to create team binding without a teamRef", func(t *testing.T) {
		ctx := context.Background()
		teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{
			User:      helper.Org1.Admin,
			Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
			GVR:       gvrTeamBindings,
		})

		toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
		toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName()
		toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = ""

		_, err := teamBindingClient.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, "teamRef is required")
	})

	t.Run("should not be able to create team binding with invalid permission", func(t *testing.T) {
		ctx := context.Background()
		teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{
			User:      helper.Org1.Admin,
			Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
			GVR:       gvrTeamBindings,
		})

		toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
		toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName()
		toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName()
		toCreate.Object["spec"].(map[string]interface{})["permission"] = "invalid"

		_, err := teamBindingClient.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, "invalid permission")
	})

	t.Run("should not be able to update team binding with insufficient permissions", func(t *testing.T) {
		for _, u := range []apis.User{
			helper.Org1.Editor,
			helper.Org1.Viewer,
		} {
			t.Run(fmt.Sprintf("with basic role_%s", u.Identity.GetOrgRole()), func(t *testing.T) {
				ctx := context.Background()
				teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{
					User:      u,
					Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
					GVR:       gvrTeamBindings,
				})

				toUpdate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
				toUpdate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName()
				toUpdate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName()
				toUpdate.Object["spec"].(map[string]interface{})["permission"] = "member"
				_, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{})
				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 update team binding if the team binding does not exist", func(t *testing.T) {
		ctx := context.Background()
		teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{
			User:      helper.Org1.Admin,
			Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
			GVR:       gvrTeamBindings,
		})

		toUpdate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
		toUpdate.Object["metadata"].(map[string]interface{})["name"] = "invalid-team-binding-name"
		toUpdate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName()
		toUpdate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName()
		_, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{})
		require.Error(t, err)
		var statusErr *errors.StatusError
		require.ErrorAs(t, err, &statusErr)
		require.Equal(t, int32(404), statusErr.ErrStatus.Code)
		require.Contains(t, statusErr.ErrStatus.Message, "not found")
	})

	t.Run("should not be able to update team binding with teamRef change", func(t *testing.T) {
		ctx := context.Background()
		teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{
			User:      helper.Org1.Admin,
			Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
			GVR:       gvrTeamBindings,
		})

		// Create the team binding if it doesn't already exist
		toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
		toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName()
		toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName()
		_, _ = teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{})

		toUpdate := toCreate.DeepCopy()
		toUpdate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = "test-team-2"
		_, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{})
		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, "teamRef is immutable")
	})

	t.Run("should not be able to update team binding with subject change", func(t *testing.T) {
		ctx := context.Background()
		teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{
			User:      helper.Org1.Admin,
			Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
			GVR:       gvrTeamBindings,
		})

		// Create the team binding if it doesn't already exist
		toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
		toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName()
		toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName()
		_, _ = teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{})

		toUpdate := toCreate.DeepCopy()
		toUpdate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = "test-user-2"

		_, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{})
		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, "subject is immutable")
	})

	t.Run("should not be able to update team binding with external change", func(t *testing.T) {
		ctx := context.Background()
		teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{
			User:      helper.Org1.Admin,
			Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
			GVR:       gvrTeamBindings,
		})

		// Create the team binding if it doesn't already exist
		toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
		toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName()
		toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName()
		_, _ = teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{})

		toUpdate := toCreate.DeepCopy()
		toUpdate.Object["spec"].(map[string]interface{})["external"] = true
		_, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{})
		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, "external is immutable")
	})

	t.Run("should not be able to update team binding with invalid permission", func(t *testing.T) {
		ctx := context.Background()
		teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{
			User:      helper.Org1.Admin,
			Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
			GVR:       gvrTeamBindings,
		})

		// Create the team binding if it doesn't already exist
		toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
		toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName()
		toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName()
		_, _ = teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{})

		toUpdate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
		toUpdate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName()
		toUpdate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName()
		toUpdate.Object["spec"].(map[string]interface{})["permission"] = "invalid"
		_, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{})
		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 permission")
	})
}

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

		// Create a team using legacy API
		legacyTeamPayload := `{
			"name": "Test Team Legacy",
			"email": "testteamlegacy@example.com"
		}`

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

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

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

		// Create a user using legacy API
		legacyUserPayload := `{
			"name": "Test User 2",
			"email": "testuser2@example.com",
			"login": "testuser2",
			"password": "password123"
		}`

		type legacyUserResponse struct {
			UID string `json:"uid"`
			ID  int64  `json:"id"`
		}

		userRsp := apis.DoRequest(helper, apis.RequestParams{
			User:   helper.Org1.Admin,
			Method: "POST",
			Path:   "/api/admin/users",
			Body:   []byte(legacyUserPayload),
		}, &legacyUserResponse{})

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

		// Create team binding using legacy API
		legacyTeamBindingPayload := `{
			"userId": ` + fmt.Sprintf("%d", userRsp.Result.ID) + `,
			"teamId": ` + fmt.Sprintf("%d", teamRsp.Result.ID) + `,
			"permission": "member"
		}`

		type legacyTeamBindingPostResponse struct {
			Message string `json:"message"`
		}

		teamBindingRsp := apis.DoRequest(helper, apis.RequestParams{
			User:   helper.Org1.Admin,
			Method: "POST",
			Path:   "/api/teams/" + teamRsp.Result.UID + "/members",
			Body:   []byte(legacyTeamBindingPayload),
		}, &legacyTeamBindingPostResponse{})

		require.NotNil(t, teamBindingRsp)
		require.Equal(t, 200, teamBindingRsp.Response.StatusCode)

		type legacyTeamBindingGetResponse struct {
			UID    string `json:"uid"`
			UserID int64  `json:"userId"`
			TeamID int64  `json:"teamId"`
		}

		// Get the binding UID using the legacy API
		legacyTeamBindingUIDRsp := apis.DoRequest(helper, apis.RequestParams{
			User:   helper.Org1.Admin,
			Method: "GET",
			Path:   "/api/teams/" + teamRsp.Result.UID + "/members",
		}, &[]legacyTeamBindingGetResponse{})

		require.NotNil(t, legacyTeamBindingUIDRsp)
		require.Equal(t, 200, legacyTeamBindingUIDRsp.Response.StatusCode)

		teamBindingName := ""
		for _, binding := range *legacyTeamBindingUIDRsp.Result {
			if binding.UserID == userRsp.Result.ID && binding.TeamID == teamRsp.Result.ID {
				teamBindingName = binding.UID
				break
			}
		}
		require.NotEmpty(t, teamBindingName)

		// Get team binding using new API
		teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{
			User:      helper.Org1.Admin,
			Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
			GVR:       gvrTeamBindings,
		})

		teamBinding, err := teamBindingClient.Resource.Get(ctx, teamBindingName, metav1.GetOptions{})
		require.NoError(t, err)
		require.NotNil(t, teamBinding)

		teamBindingSpec := teamBinding.Object["spec"].(map[string]interface{})
		require.Equal(t, "member", teamBindingSpec["permission"])
		require.Equal(t, userRsp.Result.UID, teamBindingSpec["subject"].(map[string]interface{})["name"])
		require.Equal(t, teamRsp.Result.UID, teamBindingSpec["teamRef"].(map[string]interface{})["name"])
		require.Equal(t, teamBindingName, teamBinding.GetName())
	})
}
