package teamapi

import (
	"context"
	"net/http"
	"strconv"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/grafana/grafana/pkg/api/routing"
	"github.com/grafana/grafana/pkg/services/accesscontrol"
	"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
	"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
	"github.com/grafana/grafana/pkg/services/dashboards"
	"github.com/grafana/grafana/pkg/services/featuremgmt"
	"github.com/grafana/grafana/pkg/services/licensing"
	"github.com/grafana/grafana/pkg/services/org"
	"github.com/grafana/grafana/pkg/services/preference/preftest"
	"github.com/grafana/grafana/pkg/services/team"
	"github.com/grafana/grafana/pkg/services/team/teamtest"
	"github.com/grafana/grafana/pkg/services/user"
	"github.com/grafana/grafana/pkg/services/user/usertest"
	"github.com/grafana/grafana/pkg/setting"
	"github.com/grafana/grafana/pkg/web/webtest"
)

func SetupAPITestServer(t *testing.T, teamService team.Service, opts ...func(a *TeamAPI)) *webtest.Server {
	t.Helper()
	router := routing.NewRouteRegister()
	cfg := setting.NewCfg()
	cfg.LDAPAuthEnabled = true

	if teamService == nil {
		teamService = teamtest.NewFakeService()
	}

	a := ProvideTeamAPI(router,
		teamService,
		actest.FakeService{},
		acimpl.ProvideAccessControl(featuremgmt.WithFeatures()),
		&actest.FakePermissionsService{},
		&usertest.FakeUserService{},
		&licensing.OSSLicensingService{},
		cfg,
		preftest.NewPreferenceServiceFake(),
		dashboards.NewFakeDashboardService(t),
		featuremgmt.WithFeatures(),
	)
	for _, o := range opts {
		o(a)
	}

	server := webtest.NewServer(t, router)

	return server
}

func TestAddTeamMembersAPIEndpoint(t *testing.T) {
	server := SetupAPITestServer(t, &teamtest.FakeService{ExpectedTeamDTO: &team.TeamDTO{ID: 1, UID: "a00001"}})

	t.Run("should be able to add team member with correct permission", func(t *testing.T) {
		req := webtest.RequestWithSignedInUser(
			server.NewRequest(http.MethodPost, "/api/teams/1/members", strings.NewReader("{\"userId\": 1}")),
			authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsWrite, Scope: "teams:id:1"}}),
		)
		res, err := server.SendJSON(req)
		require.NoError(t, err)
		assert.Equal(t, http.StatusOK, res.StatusCode)
		require.NoError(t, res.Body.Close())
	})

	t.Run("should be able to add team member with correct permission by UID", func(t *testing.T) {
		req := webtest.RequestWithSignedInUser(
			server.NewRequest(http.MethodPost, "/api/teams/a00001/members", strings.NewReader("{\"userId\": 1}")),
			authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsWrite, Scope: "teams:id:1"}}),
		)
		res, err := server.SendJSON(req)
		require.NoError(t, err)
		assert.Equal(t, http.StatusOK, res.StatusCode)
		require.NoError(t, res.Body.Close())
	})

	t.Run("should not be able to add team member without correct permission", func(t *testing.T) {
		req := webtest.RequestWithSignedInUser(
			server.NewRequest(http.MethodPost, "/api/teams/1/members", strings.NewReader("{\"userId\": 1}")),
			authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsWrite, Scope: "teams:id:2"}}),
		)
		res, err := server.SendJSON(req)
		require.NoError(t, err)
		assert.Equal(t, http.StatusForbidden, res.StatusCode)
		require.NoError(t, res.Body.Close())
	})
}

func TestGetTeamMembersAPIEndpoint(t *testing.T) {
	server := SetupAPITestServer(t, &teamtest.FakeService{ExpectedIsMember: true, ExpectedTeamDTO: &team.TeamDTO{ID: 1, UID: "a00001"}})

	t.Run("should be able to get team members with correct permission", func(t *testing.T) {
		req := webtest.RequestWithSignedInUser(
			server.NewGetRequest("/api/teams/1/members"),
			authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:id:1"}}),
		)
		res, err := server.SendJSON(req)
		require.NoError(t, err)
		assert.Equal(t, http.StatusOK, res.StatusCode)
		require.NoError(t, res.Body.Close())
	})

	t.Run("should be able to get team members with correct permission by UID", func(t *testing.T) {
		req := webtest.RequestWithSignedInUser(
			server.NewGetRequest("/api/teams/a00001/members"),
			authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:id:1"}}),
		)
		res, err := server.SendJSON(req)
		require.NoError(t, err)
		assert.Equal(t, http.StatusOK, res.StatusCode)
		require.NoError(t, res.Body.Close())
	})

	t.Run("should not be able to get team members without correct permission", func(t *testing.T) {
		req := webtest.RequestWithSignedInUser(
			server.NewGetRequest("/api/teams/1/members"),
			authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsRead, Scope: "teams:id:2"}}),
		)
		res, err := server.SendJSON(req)
		require.NoError(t, err)
		assert.Equal(t, http.StatusForbidden, res.StatusCode)
		require.NoError(t, res.Body.Close())
	})
}

func TestUpdateTeamMembersAPIEndpoint(t *testing.T) {
	server := SetupAPITestServer(t, &teamtest.FakeService{ExpectedIsMember: true, ExpectedTeamDTO: &team.TeamDTO{ID: 1, UID: "a00001"}})

	t.Run("should be able to update team member with correct permission", func(t *testing.T) {
		req := webtest.RequestWithSignedInUser(
			server.NewRequest(http.MethodPut, "/api/teams/1/members/1", strings.NewReader("{\"permission\": 1}")),
			authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsWrite, Scope: "teams:id:1"}}),
		)
		res, err := server.SendJSON(req)
		require.NoError(t, err)
		assert.Equal(t, http.StatusOK, res.StatusCode)
		require.NoError(t, res.Body.Close())
	})

	t.Run("should be able to update team member with correct permission by team UID", func(t *testing.T) {
		req := webtest.RequestWithSignedInUser(
			server.NewRequest(http.MethodPut, "/api/teams/a00001/members/1", strings.NewReader("{\"permission\": 1}")),
			authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsWrite, Scope: "teams:id:1"}}),
		)
		res, err := server.SendJSON(req)
		require.NoError(t, err)
		assert.Equal(t, http.StatusOK, res.StatusCode)
		require.NoError(t, res.Body.Close())
	})

	t.Run("should not be able to update team member without correct permission", func(t *testing.T) {
		req := webtest.RequestWithSignedInUser(
			server.NewRequest(http.MethodPut, "/api/teams/1/members/1", strings.NewReader("{\"permission\": 1}")),
			authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsWrite, Scope: "teams:id:2"}}),
		)
		res, err := server.SendJSON(req)
		require.NoError(t, err)
		assert.Equal(t, http.StatusForbidden, res.StatusCode)
		require.NoError(t, res.Body.Close())
	})
}

func TestUpdateTeamMembersFromProvisionedTeamWhenGroupSyncIsEnabled(t *testing.T) {
	server := SetupAPITestServer(t, &teamtest.FakeService{
		ExpectedIsMember: true,
		ExpectedTeamDTO:  &team.TeamDTO{ID: 1, UID: "a00001", IsProvisioned: true},
	}, func(tapi *TeamAPI) {
		tapi.cfg.Raw.Section("auth.scim").Key("group_sync_enabled").SetValue("true")
	})

	t.Run("should not be able to update team member from a provisioned team if team sync is enabled", func(t *testing.T) {
		req := webtest.RequestWithSignedInUser(
			server.NewRequest(http.MethodPut, "/api/teams/1/members/1", strings.NewReader("{\"permission\": 1}")),
			authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsWrite, Scope: "teams:id:1"}}),
		)
		res, err := server.SendJSON(req)
		require.NoError(t, err)
		assert.Equal(t, http.StatusBadRequest, res.StatusCode)
		require.NoError(t, res.Body.Close())
	})

	t.Run("should not be able to update team member from a provisioned team by team UID if team sync is enabled", func(t *testing.T) {
		req := webtest.RequestWithSignedInUser(
			server.NewRequest(http.MethodPut, "/api/teams/a00001/members/1", strings.NewReader("{\"permission\": 1}")),
			authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsWrite, Scope: "teams:id:1"}}),
		)
		res, err := server.SendJSON(req)
		require.NoError(t, err)
		assert.Equal(t, http.StatusBadRequest, res.StatusCode)
		require.NoError(t, res.Body.Close())
	})
}

func TestUpdateTeamMembersFromProvisionedTeamWhenGroupSyncIsDisabled(t *testing.T) {
	t.Run("should be able to delete team member from a provisioned team when SCIM group sync is disabled", func(t *testing.T) {
		server := SetupAPITestServer(t, nil, func(hs *TeamAPI) {
			hs.teamService = &teamtest.FakeService{
				ExpectedIsMember: true,
				ExpectedTeamDTO:  &team.TeamDTO{ID: 1, UID: "a00001", IsProvisioned: true},
			}
			hs.teamPermissionsService = &actest.FakePermissionsService{}
		})

		req := webtest.RequestWithSignedInUser(
			server.NewRequest(http.MethodDelete, "/api/teams/1/members/1", nil),
			authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsWrite, Scope: "teams:id:1"}}),
		)
		res, err := server.SendJSON(req)
		require.NoError(t, err)
		assert.Equal(t, http.StatusOK, res.StatusCode)
		require.NoError(t, res.Body.Close())
	})
}

func TestDeleteTeamMembersAPIEndpoint(t *testing.T) {
	server := SetupAPITestServer(t, nil, func(hs *TeamAPI) {
		hs.teamService = &teamtest.FakeService{
			ExpectedIsMember: true,
			ExpectedTeamDTO:  &team.TeamDTO{ID: 1, UID: "a00001"},
		}
		hs.teamPermissionsService = &actest.FakePermissionsService{}
	})

	t.Run("should be able to delete team member with correct permission", func(t *testing.T) {
		req := webtest.RequestWithSignedInUser(
			server.NewRequest(http.MethodDelete, "/api/teams/1/members/1", nil),
			authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsWrite, Scope: "teams:id:1"}}),
		)
		res, err := server.SendJSON(req)
		require.NoError(t, err)
		assert.Equal(t, http.StatusOK, res.StatusCode)
		require.NoError(t, res.Body.Close())
	})
	t.Run("should not be able to delete member without correct permission", func(t *testing.T) {
		req := webtest.RequestWithSignedInUser(
			server.NewRequest(http.MethodDelete, "/api/teams/1/members/1", nil),
			authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsWrite, Scope: "teams:id:2"}}),
		)
		res, err := server.SendJSON(req)
		require.NoError(t, err)
		assert.Equal(t, http.StatusForbidden, res.StatusCode)
		require.NoError(t, res.Body.Close())
	})
}

func TestDeleteTeamMembersFromProvisionedTeam(t *testing.T) {
	server := SetupAPITestServer(t, nil, func(hs *TeamAPI) {
		hs.teamService = &teamtest.FakeService{
			ExpectedIsMember: true,
			ExpectedTeamDTO:  &team.TeamDTO{ID: 1, UID: "a00001", IsProvisioned: true},
		}
		hs.teamPermissionsService = &actest.FakePermissionsService{}
	}, func(hs *TeamAPI) {
		hs.cfg.Raw.Section("auth.scim").Key("group_sync_enabled").SetValue("true")
	})

	t.Run("should not be able to delete team member from a provisioned team", func(t *testing.T) {
		req := webtest.RequestWithSignedInUser(
			server.NewRequest(http.MethodDelete, "/api/teams/1/members/1", nil),
			authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsWrite, Scope: "teams:id:1"}}),
		)
		res, err := server.SendJSON(req)
		require.NoError(t, err)
		assert.Equal(t, http.StatusBadRequest, res.StatusCode)
		require.NoError(t, res.Body.Close())
	})
}

func Test_getTeamMembershipUpdates(t *testing.T) {
	type testCase struct {
		description     string
		input           team.SetTeamMembershipsCommand
		currentMembers  []*team.TeamMemberDTO
		expectedUpdates []accesscontrol.SetResourcePermissionCommand
		expectErr       bool
	}

	testCases := []testCase{
		{
			description: "should correctly list members and admins for a team with no current members or admins",
			input: team.SetTeamMembershipsCommand{
				Members: []string{"user1", "user2"},
				Admins:  []string{"user3"},
			},
			expectedUpdates: []accesscontrol.SetResourcePermissionCommand{
				{UserID: 1, Permission: team.PermissionTypeMember.String()},
				{UserID: 2, Permission: team.PermissionTypeMember.String()},
				{UserID: 3, Permission: team.PermissionTypeAdmin.String()},
			},
		},
		{
			description: "should correctly diff the member updates for a team with existing members and admins",
			input: team.SetTeamMembershipsCommand{
				Members: []string{"user1", "user2"},
				Admins:  []string{"user3"},
			},
			currentMembers: []*team.TeamMemberDTO{
				{Email: "user1", Permission: team.PermissionTypeMember},
				{Email: "user3", Permission: team.PermissionTypeAdmin},
			},
			expectedUpdates: []accesscontrol.SetResourcePermissionCommand{
				{UserID: 2, Permission: team.PermissionTypeMember.String()},
			},
		},
		{
			description: "should correctly update membership type for the existing members and admins",
			input: team.SetTeamMembershipsCommand{
				Members: []string{"user1", "user2"},
				Admins:  []string{"user3"},
			},
			currentMembers: []*team.TeamMemberDTO{
				{Email: "user1", Permission: team.PermissionTypeMember},
				{Email: "user2", Permission: team.PermissionTypeAdmin},
				{Email: "user3", Permission: team.PermissionTypeMember},
			},
			expectedUpdates: []accesscontrol.SetResourcePermissionCommand{
				{UserID: 2, Permission: team.PermissionTypeMember.String()},
				{UserID: 3, Permission: team.PermissionTypeAdmin.String()},
			},
		},
		{
			description: "should correctly remove current members and admins that are not in the new list",
			input: team.SetTeamMembershipsCommand{
				Members: []string{"user1"},
				Admins:  []string{"user3"},
			},
			currentMembers: []*team.TeamMemberDTO{
				{Email: "user1", UserID: 1, Permission: team.PermissionTypeMember},
				{Email: "user2", UserID: 2, Permission: team.PermissionTypeMember},
				{Email: "user3", UserID: 3, Permission: team.PermissionTypeAdmin},
				{Email: "user4", UserID: 4, Permission: team.PermissionTypeAdmin},
			},
			expectedUpdates: []accesscontrol.SetResourcePermissionCommand{
				{UserID: 2, Permission: ""},
				{UserID: 4, Permission: ""},
			},
		},
		{
			description: "should error if a non-existent user is provided",
			input: team.SetTeamMembershipsCommand{
				Members: []string{"non-existent-user", "user2"},
			},
			expectErr: true,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.description, func(t *testing.T) {
			cfg := setting.NewCfg()
			userService := &usertest.FakeUserService{
				GetByEmailFn: func(ctx context.Context, query *user.GetUserByEmailQuery) (*user.User, error) {
					id, err := strconv.Atoi(strings.TrimPrefix(query.Email, "user"))
					if err != nil {
						return nil, err
					}
					user := &user.User{
						ID:    int64(id),
						Email: query.Email,
					}
					return user, nil
				},
			}
			teamSvc := teamtest.NewFakeService()
			teamSvc.ExpectedMembers = tc.currentMembers
			tapi := ProvideTeamAPI(routing.NewRouteRegister(),
				teamSvc,
				actest.FakeService{},
				acimpl.ProvideAccessControl(featuremgmt.WithFeatures()),
				&actest.FakePermissionsService{},
				userService,
				&licensing.OSSLicensingService{},
				cfg,
				preftest.NewPreferenceServiceFake(),
				dashboards.NewFakeDashboardService(t),
				featuremgmt.WithFeatures(),
			)

			user := &user.SignedInUser{UserID: 1, OrgID: 1, OrgRole: org.RoleAdmin, Permissions: map[int64]map[string][]string{1: {accesscontrol.ActionOrgUsersRead: {"users:id:*"}}}}
			updates, err := tapi.getTeamMembershipUpdates(context.Background(), 1, 1, tc.input, user)
			if tc.expectErr {
				require.Error(t, err)
				return
			}
			require.NoError(t, err)
			require.ElementsMatch(t, tc.expectedUpdates, updates)
		})
	}
}

func authedUserWithPermissions(userID, orgID int64, permissions []accesscontrol.Permission) *user.SignedInUser {
	return &user.SignedInUser{UserID: userID, OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByActionContext(context.Background(), permissions)}}
}
