package gitlab

import (
	"bytes"
	"fmt"
	"net/http"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/google/go-querystring/query"
	"github.com/stretchr/testify/assert"
)

func TestListGroups(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodGet)
			fmt.Fprint(w, `[{"id":1},{"id":2}]`)
		})

	groups, _, err := client.Groups.ListGroups(&ListGroupsOptions{})
	require.NoError(t, err)

	want := []*Group{{ID: 1}, {ID: 2}}
	assert.Equal(t, want, groups)
}

func TestListGroups_Filtering(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)

		testParam(t, r, "active", "true")
		testParam(t, r, "archived", "false")
		testParam(t, r, "marked_for_deletion_on", "2023-10-01")

		fmt.Fprint(w, `[{"id":1}]`)
	})

	active := true
	archived := false
	deletionDate := ISOTime(time.Date(2023, time.October, 1, 0, 0, 0, 0, time.UTC))

	opt := &ListGroupsOptions{
		Active:              &active,
		Archived:            &archived,
		MarkedForDeletionOn: &deletionDate,
	}

	groups, _, err := client.Groups.ListGroups(opt)
	require.NoError(t, err)

	want := []*Group{{ID: 1}}
	assert.Equal(t, want, groups)
}

func TestListGroups_Visibility(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)

		testParam(t, r, "visibility", "public")

		fmt.Fprint(w, `[{"id":1}]`)
	})

	opt := &ListGroupsOptions{
		Visibility: Ptr(PublicVisibility),
	}

	groups, _, err := client.Groups.ListGroups(opt)

	require.NoError(t, err)

	want := []*Group{{ID: 1}}
	assert.Equal(t, want, groups)
}

func TestGetGroup(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/g",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodGet)
			fmt.Fprint(w, `
			{
			"id": 1,
			"name": "g",
			"default_branch": "branch",
			"shared_with_groups": [
				{
				"group_id": 1,
				"group_name": "whiskers delicious",
				"member_role_id": 2
				}
			]
			}
		`)
		})

	group, _, err := client.Groups.GetGroup("g", &GetGroupOptions{})
	require.NoError(t, err)

	// Create the group shares struct to test.
	shares := []SharedWithGroup{
		{
			GroupID:      1,
			GroupName:    "whiskers delicious",
			MemberRoleID: 2,
		},
	}
	want := &Group{ID: 1, Name: "g", DefaultBranch: "branch", SharedWithGroups: shares}

	assert.Equal(t, want, group)
}

func TestGetGroup_FullResponseFields(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN a GitLab API that returns a response containing every group field
	// the GitLab Groups API documents.
	mux.HandleFunc("/api/v4/groups/g",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodGet)
			fmt.Fprint(w, `{
				"id": 1,
				"name": "g",
				"organization_id": 7,
				"archived": true,
				"prevent_sharing_groups_outside_hierarchy": true,
				"enabled_git_access_protocol": "all",
				"duo_availability": "default_on",
				"experiment_features_enabled": true,
				"duo_features_enabled": true,
				"lock_duo_features_enabled": true,
				"math_rendering_limits_enabled": true,
				"lock_math_rendering_limits_enabled": true
			}`)
		})

	// WHEN GetGroup is called
	group, _, err := client.Groups.GetGroup("g", &GetGroupOptions{})
	require.NoError(t, err)

	// THEN every newly-exposed field is unmarshaled onto the Group struct.
	want := &Group{
		ID:                                   1,
		Name:                                 "g",
		OrganizationID:                       7,
		Archived:                             true,
		PreventSharingGroupsOutsideHierarchy: true,
		EnabledGitAccessProtocol:             EnabledGitAccessProtocolAll,
		DuoAvailability:                      DuoAvailabilityDefaultOn,
		ExperimentFeaturesEnabled:            true,
		DuoFeaturesEnabled:                   true,
		LockDuoFeaturesEnabled:               true,
		MathRenderingLimitsEnabled:           true,
		LockMathRenderingLimitsEnabled:       true,
	}
	assert.Equal(t, want, group)
}

func TestGetGroupWithFileTemplateId(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/g",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodGet)
			fmt.Fprint(w, `{"id": 1, "name": "g","file_template_project_id": 12345}`)
		})

	group, _, err := client.Groups.GetGroup("g", &GetGroupOptions{})
	require.NoError(t, err)

	want := &Group{ID: 1, Name: "g", FileTemplateProjectID: 12345}
	assert.Equal(t, want, group)
}

func TestCreateGroup(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodPost)
			fmt.Fprint(w, `{"id": 1, "name": "g", "path": "g"}`)
		})

	opt := &CreateGroupOptions{
		Name: Ptr("g"),
		Path: Ptr("g"),
	}

	group, _, err := client.Groups.CreateGroup(opt, nil)
	require.NoError(t, err)

	want := &Group{ID: 1, Name: "g", Path: "g"}
	assert.Equal(t, want, group)
}

func TestCreateGroup_FullAttributes(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN every non-deprecated, non-Avatar attribute on CreateGroupOptions.
	// Building once and reusing for both the request-body assertion and the
	// actual call guarantees the test stays in sync with the struct.
	opt := &CreateGroupOptions{
		Name:                            Ptr("g"),
		Path:                            Ptr("g"),
		DefaultBranch:                   Ptr("main"),
		Description:                     Ptr("a description"),
		MembershipLock:                  Ptr(true),
		Visibility:                      Ptr(PrivateVisibility),
		ShareWithGroupLock:              Ptr(true),
		RequireTwoFactorAuth:            Ptr(true),
		TwoFactorGracePeriod:            Ptr(int64(48)),
		ProjectCreationLevel:            Ptr(MaintainerProjectCreation),
		AutoDevopsEnabled:               Ptr(true),
		SubGroupCreationLevel:           Ptr(MaintainerSubGroupCreationLevelValue),
		EmailsEnabled:                   Ptr(true),
		MentionsDisabled:                Ptr(true),
		LFSEnabled:                      Ptr(true),
		DefaultBranchProtectionDefaults: &DefaultBranchProtectionDefaultsOptions{CodeOwnerApprovalRequired: Ptr(true)},
		RequestAccessEnabled:            Ptr(true),
		ParentID:                        Ptr(int64(2)),
		SharedRunnersMinutesLimit:       Ptr(int64(100)),
		ExtraSharedRunnersMinutesLimit:  Ptr(int64(50)),
		WikiAccessLevel:                 Ptr(EnabledAccessControl),
		EnabledGitAccessProtocol:        Ptr(EnabledGitAccessProtocolAll),
		OrganizationID:                  Ptr(int64(7)),
		DuoAvailability:                 Ptr(DuoAvailabilityDefaultOn),
		ExperimentFeaturesEnabled:       Ptr(true),

		UniqueProjectDownloadLimit:                  Ptr(int64(10)),
		UniqueProjectDownloadLimitIntervalInSeconds: Ptr(int64(60)),
		UniqueProjectDownloadLimitAllowlist:         &[]string{"alice"},
		UniqueProjectDownloadLimitAlertlist:         &[]int64{42},
		AutoBanUserOnExcessiveProjectsDownload:      Ptr(true),

		MathRenderingLimitsEnabled: Ptr(true),

		WebBasedCommitSigningEnabled: Ptr(true),
		AllowPersonalSnippets:        Ptr(true),
	}

	// AND a GitLab API stub that asserts the request body carries that full
	// payload verbatim.
	mux.HandleFunc("/api/v4/groups",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodPost)
			testBodyJSON(t, r, opt)
			fmt.Fprint(w, `{"id": 1, "name": "g", "path": "g"}`)
		})

	// WHEN CreateGroup is called with that fully populated options struct.
	group, _, err := client.Groups.CreateGroup(opt, nil)
	require.NoError(t, err)

	// THEN the request body validation passes and a group is returned.
	want := &Group{ID: 1, Name: "g", Path: "g"}
	assert.Equal(t, want, group)
}

func TestCreateGroupWithDefaultBranch(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodPost)
			fmt.Fprint(w, `{"id": 1, "name": "g", "path": "g", "default_branch": "branch"}`)
		})

	opt := &CreateGroupOptions{
		Name:          Ptr("g"),
		Path:          Ptr("g"),
		DefaultBranch: Ptr("branch"),
	}

	group, _, err := client.Groups.CreateGroup(opt, nil)
	require.NoError(t, err)

	want := &Group{ID: 1, Name: "g", Path: "g", DefaultBranch: "branch"}
	assert.Equal(t, want, group)
}

func TestCreateGroupDefaultBranchSettings(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodPost)
			testBodyJSON(t, r, &CreateGroupOptions{
				Name: Ptr("g"),
				Path: Ptr("g"),
				DefaultBranchProtectionDefaults: &DefaultBranchProtectionDefaultsOptions{
					AllowedToPush: &[]*GroupAccessLevel{
						{
							AccessLevel: Ptr(MaintainerPermissions),
						},
					},
					AllowedToMerge: &[]*GroupAccessLevel{
						{
							AccessLevel: Ptr(MaintainerPermissions),
						},
					},
					CodeOwnerApprovalRequired: Ptr(true),
				},
			})

			fmt.Fprint(w, `
			{
				"id": 1,
				"name": "g",
				"path": "g",
				"default_branch_protection_defaults": {
					"allowed_to_push": [
						{
							"access_level": 40
						}
					],
					"allow_force_push": false,
					"allowed_to_merge": [
						{
							"access_level": 40
						}
					],
					"code_owner_approval_required": true
				}
			}
			`)
		})

	opt := &CreateGroupOptions{
		Name: Ptr("g"),
		Path: Ptr("g"),
		DefaultBranchProtectionDefaults: &DefaultBranchProtectionDefaultsOptions{
			AllowedToPush: &[]*GroupAccessLevel{
				{
					AccessLevel: Ptr(AccessLevelValue(40)),
				},
			},
			AllowedToMerge: &[]*GroupAccessLevel{
				{
					AccessLevel: Ptr(AccessLevelValue(40)),
				},
			},
			CodeOwnerApprovalRequired: Ptr(true),
		},
	}

	group, _, err := client.Groups.CreateGroup(opt, nil)
	require.NoError(t, err)

	// Create the group that we want to get back
	want := &Group{
		ID:   1,
		Name: "g",
		Path: "g",
		DefaultBranchProtectionDefaults: &BranchProtectionDefaults{
			AllowedToMerge: []*GroupAccessLevel{
				{
					AccessLevel: Ptr(MaintainerPermissions),
				},
			},
			AllowedToPush: []*GroupAccessLevel{
				{
					AccessLevel: Ptr(MaintainerPermissions),
				},
			},
			CodeOwnerApprovalRequired: true,
		},
	}

	assert.Equal(t, want, group)
}

func TestTransferGroup(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1/projects/2",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodPost)
			fmt.Fprintf(w, `{"id": 1}`)
		})

	group, _, err := client.Groups.TransferGroup(1, 2)
	require.NoError(t, err)

	want := &Group{ID: 1}
	assert.Equal(t, want, group)
}

func TestTransferSubGroup(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1/transfer",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodPost)
			fmt.Fprintf(w, `{"id": 1, "parent_id": 2}`)
		})

	opt := &TransferSubGroupOptions{
		GroupID: Ptr(int64(2)),
	}

	group, _, err := client.Groups.TransferSubGroup(1, opt)
	require.NoError(t, err)

	want := &Group{ID: 1, ParentID: 2}
	assert.Equal(t, want, group)
}

func TestDeleteGroup(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodDelete)
			w.WriteHeader(http.StatusAccepted)
		})

	resp, err := client.Groups.DeleteGroup(1, nil)
	require.NoError(t, err)

	require.NotNil(t, resp)
	assert.Equal(t, http.StatusAccepted, resp.StatusCode)
}

func TestDeleteGroup_WithPermanentDelete(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodDelete)
			w.WriteHeader(http.StatusAccepted)
			testParam(t, r, "permanently_remove", "true")
			testParam(t, r, "full_path", "testPath")
		})

	resp, err := client.Groups.DeleteGroup(1, &DeleteGroupOptions{
		PermanentlyRemove: Ptr(true),
		FullPath:          Ptr("testPath"),
	})
	require.NoError(t, err)

	require.NotNil(t, resp)
	assert.Equal(t, http.StatusAccepted, resp.StatusCode)
}

func TestSearchGroup(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodGet)
			fmt.Fprint(w, `[{"id": 1, "name": "Foobar Group"}]`)
		})

	groups, _, err := client.Groups.SearchGroup("foobar")
	require.NoError(t, err)

	want := []*Group{{ID: 1, Name: "Foobar Group"}}
	assert.Equal(t, want, groups)
}

func TestUpdateGroup(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodPut)
			fmt.Fprint(w, `{"id": 1}`)
		})

	group, _, err := client.Groups.UpdateGroup(1, &UpdateGroupOptions{})
	require.NoError(t, err)

	want := &Group{ID: 1}
	assert.Equal(t, want, group)
}

func TestUpdateGroupWithDefaultBranch(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodPut)
			fmt.Fprint(w, `{"id": 1, "default_branch": "branch"}`)
		})

	opt := &UpdateGroupOptions{
		DefaultBranch: Ptr("branch"),
	}

	group, _, err := client.Groups.UpdateGroup(1, opt)
	require.NoError(t, err)

	want := &Group{ID: 1, DefaultBranch: "branch"}
	assert.Equal(t, want, group)
}

func TestListGroupProjects(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/22/projects",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodGet)
			fmt.Fprint(w, `[{"id":1},{"id":2}]`)
		})

	projects, _, err := client.Groups.ListGroupProjects(22,
		&ListGroupProjectsOptions{})
	require.NoError(t, err)

	want := []*Project{{ID: 1}, {ID: 2}}
	assert.Equal(t, want, projects)
}

func TestListGroupProjectsWithActive(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/22/projects",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodGet)
			testURL(t, r, "/api/v4/groups/22/projects?active=true")
			fmt.Fprint(w, `[{"id":1},{"id":2}]`)
		})

	projects, _, err := client.Groups.ListGroupProjects(22,
		&ListGroupProjectsOptions{Active: Ptr(true)})
	require.NoError(t, err)

	want := []*Project{{ID: 1}, {ID: 2}}
	assert.Equal(t, want, projects)
}

func TestListSubGroups(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1/subgroups",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodGet)
			fmt.Fprint(w, `[{"id": 1}, {"id": 2}]`)
		})

	groups, _, err := client.Groups.ListSubGroups(1, &ListSubGroupsOptions{})
	require.NoError(t, err)

	want := []*Group{{ID: 1}, {ID: 2}}
	assert.Equal(t, want, groups)
}

func TestListGroupLDAPLinks(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1/ldap_group_links",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodGet)
			fmt.Fprint(w, `[
	{
		"cn":"gitlab_group_example_30",
		"group_access":30,
		"provider":"example_ldap_provider",
		"member_role_id":2
	},
	{
		"cn":"gitlab_group_example_40",
		"group_access":40,
		"provider":"example_ldap_provider"
	}
]`)
		})

	links, resp, err := client.Groups.ListGroupLDAPLinks(1)
	assert.NoError(t, err)
	assert.NotNil(t, resp)

	want := []*LDAPGroupLink{
		{
			CN:           "gitlab_group_example_30",
			GroupAccess:  30,
			Provider:     "example_ldap_provider",
			MemberRoleID: 2,
		},
		{
			CN:          "gitlab_group_example_40",
			GroupAccess: 40,
			Provider:    "example_ldap_provider",
		},
	}
	assert.Equal(t, want, links)
}

func TestAddGroupLDAPLink(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1/ldap_group_links",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodPost)
			fmt.Fprint(w, `
{
	"cn":"gitlab_group_example_30",
	"group_access":30,
	"provider":"example_ldap_provider",
	"member_role_id":2
}`)
		})

	opt := &AddGroupLDAPLinkOptions{
		CN:           Ptr("gitlab_group_example_30"),
		GroupAccess:  Ptr(AccessLevelValue(30)),
		Provider:     Ptr("example_ldap_provider"),
		MemberRoleID: Ptr(int64(2)),
	}

	link, resp, err := client.Groups.AddGroupLDAPLink(1, opt)
	assert.NoError(t, err)
	assert.NotNil(t, resp)

	want := &LDAPGroupLink{
		CN:           "gitlab_group_example_30",
		GroupAccess:  30,
		Provider:     "example_ldap_provider",
		MemberRoleID: 2,
	}
	assert.Equal(t, want, link)
}

func TestAddGroupLDAPLinkFilter(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1/ldap_group_links",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodPost)
			fmt.Fprint(w, `
{
	"filter":"(memberOf=example_group_dn)",
	"group_access":30,
	"provider":"example_ldap_provider"
}`)
		})

	opt := &AddGroupLDAPLinkOptions{
		Filter:      Ptr("(memberOf=example_group_dn)"),
		GroupAccess: Ptr(AccessLevelValue(30)),
		Provider:    Ptr("example_ldap_provider"),
	}

	link, resp, err := client.Groups.AddGroupLDAPLink(1, opt)
	assert.NoError(t, err)
	assert.NotNil(t, resp)

	want := &LDAPGroupLink{
		Filter:      "(memberOf=example_group_dn)",
		GroupAccess: 30,
		Provider:    "example_ldap_provider",
	}
	assert.Equal(t, want, link)
}

func TestDeleteGroupLDAPLink(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)
	mux.HandleFunc("/api/v4/groups/1/ldap_group_links/gitlab_group_example_30", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodDelete)
		w.WriteHeader(http.StatusAccepted)
	})
	resp, err := client.Groups.DeleteGroupLDAPLink(1, "gitlab_group_example_30")
	assert.NoError(t, err)
	assert.NotNil(t, resp)
}

func TestDeleteGroupLDAPLinkWithCNOrFilter(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)
	mux.HandleFunc("/api/v4/groups/1/ldap_group_links", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodDelete)
		w.WriteHeader(http.StatusAccepted)
	})
	opt := &DeleteGroupLDAPLinkWithCNOrFilterOptions{
		CN:       Ptr("gitlab_group_example_30"),
		Provider: Ptr("example_ldap_provider"),
	}

	resp, err := client.Groups.DeleteGroupLDAPLinkWithCNOrFilter(1, opt)
	assert.NoError(t, err)
	assert.NotNil(t, resp)
}

func TestListGroupSAMLLinks(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1/saml_group_links",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodGet)
			fmt.Fprint(w, `[
	{
		"access_level":30,
		"name":"gitlab_group_example_developer"
	},
	{
		"access_level":40,
		"name":"gitlab_group_example_maintainer"
	}
]`)
		})

	links, _, err := client.Groups.ListGroupSAMLLinks(1)
	require.NoError(t, err)

	want := []*SAMLGroupLink{
		{
			AccessLevel: DeveloperPermissions,
			Name:        "gitlab_group_example_developer",
		},
		{
			AccessLevel: MaintainerPermissions,
			Name:        "gitlab_group_example_maintainer",
		},
	}
	assert.Equal(t, want, links)
}

func TestListGroupSAMLLinksCustomRole(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1/saml_group_links",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodGet)
			fmt.Fprint(w, `[
	{
		"access_level":30,
		"name":"gitlab_group_example_developer",
		"member_role_id":123
	}
]`)
		})

	links, _, err := client.Groups.ListGroupSAMLLinks(1)
	require.NoError(t, err)

	want := []*SAMLGroupLink{
		{
			AccessLevel:  DeveloperPermissions,
			Name:         "gitlab_group_example_developer",
			MemberRoleID: 123,
		},
	}
	assert.Equal(t, want, links)
}

func TestGetGroupSAMLLink(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1/saml_group_links/gitlab_group_example_developer",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodGet)
			fmt.Fprint(w, `
{
	"access_level":30,
	"name":"gitlab_group_example_developer"
}`)
		})

	links, _, err := client.Groups.GetGroupSAMLLink(1, "gitlab_group_example_developer")
	require.NoError(t, err)

	want := &SAMLGroupLink{
		AccessLevel: DeveloperPermissions,
		Name:        "gitlab_group_example_developer",
	}
	assert.Equal(t, want, links)
}

func TestGetGroupSAMLLinkCustomRole(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1/saml_group_links/gitlab_group_example_developer",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodGet)
			fmt.Fprint(w, `
{
	"access_level":30,
	"name":"gitlab_group_example_developer",
	"member_role_id":123
}`)
		})

	links, _, err := client.Groups.GetGroupSAMLLink(1, "gitlab_group_example_developer")
	require.NoError(t, err)

	want := &SAMLGroupLink{
		AccessLevel:  DeveloperPermissions,
		Name:         "gitlab_group_example_developer",
		MemberRoleID: 123,
	}
	assert.Equal(t, want, links)
}

func TestAddGroupSAMLLink(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1/saml_group_links",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodPost)
			fmt.Fprint(w, `
{
	"access_level":30,
	"name":"gitlab_group_example_developer"
}`)
		})

	opt := &AddGroupSAMLLinkOptions{
		SAMLGroupName: Ptr("gitlab_group_example_developer"),
		AccessLevel:   Ptr(DeveloperPermissions),
	}

	link, _, err := client.Groups.AddGroupSAMLLink(1, opt)
	require.NoError(t, err)

	want := &SAMLGroupLink{
		AccessLevel: DeveloperPermissions,
		Name:        "gitlab_group_example_developer",
	}
	assert.Equal(t, want, link)
}

func TestAddGroupSAMLLinkCustomRole(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1/saml_group_links",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodPost)
			fmt.Fprint(w, `
{
	"access_level":30,
	"name":"gitlab_group_example_developer",
	"member_role_id":123
}`)
		})

	opt := &AddGroupSAMLLinkOptions{
		SAMLGroupName: Ptr("gitlab_group_example_developer"),
		AccessLevel:   Ptr(DeveloperPermissions),
		MemberRoleID:  Ptr(int64(123)),
	}

	link, _, err := client.Groups.AddGroupSAMLLink(1, opt)
	require.NoError(t, err)

	want := &SAMLGroupLink{
		AccessLevel:  DeveloperPermissions,
		Name:         "gitlab_group_example_developer",
		MemberRoleID: 123,
	}
	assert.Equal(t, want, link)
}

func TestAddGroupSAMLLinkWithProvider(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1/saml_group_links",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodPost)
			fmt.Fprint(w, `
{
    "access_level":30,
    "name":"gitlab_group_example_developer",
    "provider":"example_saml_provider"
}`)
		})

	opt := &AddGroupSAMLLinkOptions{
		SAMLGroupName: Ptr("gitlab_group_example_developer"),
		AccessLevel:   Ptr(DeveloperPermissions),
		Provider:      Ptr("example_saml_provider"),
	}

	link, _, err := client.Groups.AddGroupSAMLLink(1, opt)
	require.NoError(t, err)

	want := &SAMLGroupLink{
		AccessLevel: DeveloperPermissions,
		Name:        "gitlab_group_example_developer",
		Provider:    "example_saml_provider",
	}
	assert.Equal(t, want, link)
}

func TestGroupsService_ListGroupSharedProjects(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1/projects/shared", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, "GET")
		testURL(t, r, "/api/v4/groups/1/projects/shared?page=2&per_page=3")
		fmt.Fprint(w, `[
			{
				"id": 1,
				"description": "Foobar project",
				"name": "Foobar",
				"name_with_namespace": "Group / Foobar",
				"path": "foobar",
				"path_with_namespace": "group/foobar",
				"created_at": "2020-01-01T00:00:00.000Z"
			},
			{
				"id": 2,
				"description": "Shared project",
				"name": "Shared",
				"name_with_namespace": "Group / Shared",
				"path": "shared",
				"path_with_namespace": "group/shared",
				"created_at": "2020-01-01T00:00:00.000Z"
			}
		]`)
	})

	opt := &ListGroupSharedProjectsOptions{ListOptions: ListOptions{Page: 2, PerPage: 3}}
	projects, _, err := client.Groups.ListGroupSharedProjects(1, opt)
	require.NoError(t, err)

	want := []*Project{
		{
			ID:                1,
			Description:       "Foobar project",
			Name:              "Foobar",
			NameWithNamespace: "Group / Foobar",
			Path:              "foobar",
			PathWithNamespace: "group/foobar",
			CreatedAt:         Ptr(time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)),
		},
		{
			ID:                2,
			Description:       "Shared project",
			Name:              "Shared",
			NameWithNamespace: "Group / Shared",
			Path:              "shared",
			PathWithNamespace: "group/shared",
			CreatedAt:         Ptr(time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)),
		},
	}
	assert.Equal(t, want, projects)
}

func TestRestoreGroup(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)
	mux.HandleFunc("/api/v4/groups/1/restore",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodPost)
			fmt.Fprint(w, `{"id": 1, "name": "g"}`)
		})

	group, _, err := client.Groups.RestoreGroup(1)
	require.NoError(t, err)
	want := &Group{ID: 1, Name: "g"}
	assert.Equal(t, want, group)
}

func TestShareGroupWithGroup(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1/share",
		func(w http.ResponseWriter, r *http.Request) {
			testBodyJSON(t, r, map[string]any{
				"group_id":       float64(1),
				"group_access":   float64(30),
				"member_role_id": float64(1),
			})

			// Return group details
			testMethod(t, r, http.MethodPost)
			fmt.Fprint(w, `{"id": 1, "name": "g"}`)
		})

	group, _, err := client.Groups.ShareGroupWithGroup(1, &ShareGroupWithGroupOptions{
		GroupID:      Ptr(int64(1)),
		GroupAccess:  Ptr(DeveloperPermissions),
		MemberRoleID: Ptr(int64(1)),
	})
	require.NoError(t, err)

	want := &Group{ID: 1, Name: "g"}
	assert.Equal(t, want, group)
}

func TestUnshareGroupFromGroup(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)
	mux.HandleFunc("/api/v4/groups/1/share/2",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodDelete)
			w.WriteHeader(http.StatusNoContent)
		})

	r, err := client.Groups.UnshareGroupFromGroup(1, 2)
	require.NoError(t, err)
	require.NotNil(t, r)
	assert.Equal(t, http.StatusNoContent, r.StatusCode)
}

func TestUpdateGroupWithIPRestrictionRanges(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)
	const ipRange = "192.168.0.0/24"

	mux.HandleFunc("/api/v4/groups/1",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodPut)
			testBodyJSON(t, r, map[string]any{
				"ip_restriction_ranges": ipRange,
			})

			fmt.Fprintf(w, `{"id": 1, "ip_restriction_ranges" : "%v"}`, ipRange)
		})

	group, _, err := client.Groups.UpdateGroup(1, &UpdateGroupOptions{
		IPRestrictionRanges: Ptr(ipRange),
	})
	require.NoError(t, err)

	want := &Group{ID: 1, IPRestrictionRanges: ipRange}
	assert.Equal(t, want, group)
}

func TestGetGroupWithEmailsEnabled(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodGet)

			// Modified from https://docs.gitlab.com/api/groups/#details-of-a-group
			fmt.Fprint(w, `
			{
				"id": 1,
				"name": "test",
				"path": "test",
				"emails_enabled": true,
				"description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
				"visibility": "public",
				"avatar_url": null,
				"web_url": "https://gitlab.example.com/groups/test",
				"request_access_enabled": false,
				"repository_storage": "default",
				"full_name": "test",
				"full_path": "test",
				"runners_token": "ba324ca7b1c77fc20bb9",
				"file_template_project_id": 1,
				"parent_id": null,
				"enabled_git_access_protocol": "all",
				"created_at": "2020-01-15T12:36:29.590Z",
				"prevent_sharing_groups_outside_hierarchy": false,
				"ip_restriction_ranges": null,
				"math_rendering_limits_enabled": true,
				"lock_math_rendering_limits_enabled": false
			  }`)
		})

	group, _, err := client.Groups.GetGroup(1, &GetGroupOptions{})
	require.NoError(t, err)

	assert.True(t, group.EmailsEnabled)
}

func TestCreateGroupWithEmailsEnabled(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodPost)
			testBodyJSON(t, r, map[string]bool{"emails_enabled": true})
			// Response is tested via the "GET" test, only test the actual request here.
			fmt.Fprint(w, `
			{}`)
		})

	_, _, err := client.Groups.CreateGroup(&CreateGroupOptions{EmailsEnabled: Ptr(true)})
	require.NoError(t, err)
}

func TestUpdateGroupWithEmailsEnabled(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodPut)
			testBodyJSON(t, r, map[string]bool{"emails_enabled": true})
			// Response is tested via the "GET" test, only test the actual request here.
			fmt.Fprint(w, `
			{}`)
		})

	_, _, err := client.Groups.UpdateGroup(1, &UpdateGroupOptions{EmailsEnabled: Ptr(true)})
	require.NoError(t, err)
}

func TestGetGroupPushRules(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1/push_rule", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, `{
			"id": 1,
			"commit_message_regex": "Fixes \\d+\\..*",
			"commit_message_negative_regex": "ssh\\:\\/\\/",
			"branch_name_regex": "(feat|fix)\\/*",
			"deny_delete_tag": false,
			"member_check": false,
			"prevent_secrets": false,
			"author_email_regex": "@company.com$",
			"file_name_regex": "(jar|exe)$",
			"max_file_size": 5,
			"commit_committer_check": false,
			"commit_committer_name_check": false,
			"reject_unsigned_commits": false,
			"reject_non_dco_commits": false
		  }`)
	})

	rule, _, err := client.Groups.GetGroupPushRules(1)
	require.NoError(t, err)

	want := &GroupPushRules{
		ID:                         1,
		CommitMessageRegex:         "Fixes \\d+\\..*",
		CommitMessageNegativeRegex: "ssh\\:\\/\\/",
		BranchNameRegex:            "(feat|fix)\\/*",
		DenyDeleteTag:              false,
		MemberCheck:                false,
		PreventSecrets:             false,
		AuthorEmailRegex:           "@company.com$",
		FileNameRegex:              "(jar|exe)$",
		MaxFileSize:                5,
		CommitCommitterCheck:       false,
		CommitCommitterNameCheck:   false,
		RejectUnsignedCommits:      false,
		RejectNonDCOCommits:        false,
	}

	assert.Equal(t, want, rule)
}

func TestAddGroupPushRules(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1/push_rule", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodPost)
		fmt.Fprint(w, `{
			"id": 1,
			"commit_message_regex": "Fixes \\d+\\..*",
			"commit_message_negative_regex": "ssh\\:\\/\\/",
			"branch_name_regex": "(feat|fix)\\/*",
			"deny_delete_tag": false,
			"member_check": false,
			"prevent_secrets": false,
			"author_email_regex": "@company.com$",
			"file_name_regex": "(jar|exe)$",
			"max_file_size": 5,
			"commit_committer_check": false,
			"commit_committer_name_check": false,
			"reject_unsigned_commits": false,
			"reject_non_dco_commits": false
		  }`)
	})

	opt := &AddGroupPushRuleOptions{
		CommitMessageRegex:         Ptr("Fixes \\d+\\..*"),
		CommitMessageNegativeRegex: Ptr("ssh\\:\\/\\/"),
		BranchNameRegex:            Ptr("(feat|fix)\\/*"),
		DenyDeleteTag:              Ptr(false),
		MemberCheck:                Ptr(false),
		PreventSecrets:             Ptr(false),
		AuthorEmailRegex:           Ptr("@company.com$"),
		FileNameRegex:              Ptr("(jar|exe)$"),
		MaxFileSize:                Ptr(int64(5)),
		CommitCommitterCheck:       Ptr(false),
		CommitCommitterNameCheck:   Ptr(false),
		RejectUnsignedCommits:      Ptr(false),
		RejectNonDCOCommits:        Ptr(false),
	}

	rule, _, err := client.Groups.AddGroupPushRule(1, opt)
	require.NoError(t, err)

	want := &GroupPushRules{
		ID:                         1,
		CommitMessageRegex:         "Fixes \\d+\\..*",
		CommitMessageNegativeRegex: "ssh\\:\\/\\/",
		BranchNameRegex:            "(feat|fix)\\/*",
		DenyDeleteTag:              false,
		MemberCheck:                false,
		PreventSecrets:             false,
		AuthorEmailRegex:           "@company.com$",
		FileNameRegex:              "(jar|exe)$",
		MaxFileSize:                5,
		CommitCommitterCheck:       false,
		CommitCommitterNameCheck:   false,
		RejectUnsignedCommits:      false,
		RejectNonDCOCommits:        false,
	}

	assert.Equal(t, want, rule)
}

func TestEditGroupPushRules(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1/push_rule", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodPut)
		fmt.Fprint(w, `{
			"id": 1,
			"commit_message_regex": "Fixes \\d+\\..*",
			"commit_message_negative_regex": "ssh\\:\\/\\/",
			"branch_name_regex": "(feat|fix)\\/*",
			"deny_delete_tag": false,
			"member_check": false,
			"prevent_secrets": false,
			"author_email_regex": "@company.com$",
			"file_name_regex": "(jar|exe)$",
			"max_file_size": 5,
			"commit_committer_check": false,
			"commit_committer_name_check": false,
			"reject_unsigned_commits": false,
			"reject_non_dco_commits": false
		  }`)
	})

	opt := &EditGroupPushRuleOptions{
		CommitMessageRegex:         Ptr("Fixes \\d+\\..*"),
		CommitMessageNegativeRegex: Ptr("ssh\\:\\/\\/"),
		BranchNameRegex:            Ptr("(feat|fix)\\/*"),
		DenyDeleteTag:              Ptr(false),
		MemberCheck:                Ptr(false),
		PreventSecrets:             Ptr(false),
		AuthorEmailRegex:           Ptr("@company.com$"),
		FileNameRegex:              Ptr("(jar|exe)$"),
		MaxFileSize:                Ptr(int64(5)),
		CommitCommitterCheck:       Ptr(false),
		CommitCommitterNameCheck:   Ptr(false),
		RejectUnsignedCommits:      Ptr(false),
		RejectNonDCOCommits:        Ptr(false),
	}

	rule, _, err := client.Groups.EditGroupPushRule(1, opt)
	require.NoError(t, err)

	want := &GroupPushRules{
		ID:                         1,
		CommitMessageRegex:         "Fixes \\d+\\..*",
		CommitMessageNegativeRegex: "ssh\\:\\/\\/",
		BranchNameRegex:            "(feat|fix)\\/*",
		DenyDeleteTag:              false,
		MemberCheck:                false,
		PreventSecrets:             false,
		AuthorEmailRegex:           "@company.com$",
		FileNameRegex:              "(jar|exe)$",
		MaxFileSize:                5,
		CommitCommitterCheck:       false,
		CommitCommitterNameCheck:   false,
		RejectUnsignedCommits:      false,
		RejectNonDCOCommits:        false,
	}

	assert.Equal(t, want, rule)
}

func TestUpdateGroupWithAllowedEmailDomainsList(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)
	const domain = "example.com"

	mux.HandleFunc("/api/v4/groups/1",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodPut)
			testBodyJSON(t, r, map[string]string{"allowed_email_domains_list": domain})

			fmt.Fprintf(w, `{"id": 1, "allowed_email_domains_list" : "%v"}`, domain)
		})

	group, _, err := client.Groups.UpdateGroup(1, &UpdateGroupOptions{
		AllowedEmailDomainsList: Ptr(domain),
	})
	require.NoError(t, err)

	want := &Group{ID: 1, AllowedEmailDomainsList: domain}
	assert.Equal(t, want, group)
}

func TestCreateGroupDefaultBranchSettingsWithAvatar(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodPost)

			testFormBody(t, r, "name", "g")
			testFormBody(t, r, "path", "g")
			testFormBody(t, r, "default_branch_protection_defaults[allowed_to_push][][access_level]", "40")
			testFormBody(t, r, "default_branch_protection_defaults[allowed_to_merge][][access_level]", "40")
			testFormBody(
				t,
				r,
				"default_branch_protection_defaults[code_owner_approval_required]",
				"true",
			)

			fmt.Fprint(w, `
			{
				"id": 1,
				"name": "g",
				"path": "g",
				"default_branch_protection_defaults": {
					"allowed_to_push": [
						{
							"access_level": 40
						}
					],
					"allow_force_push": false,
					"allowed_to_merge": [
						{
							"access_level": 40
						}
					],
					"code_owner_approval_required": true
				},
				"avatar_url":"http://localhost/uploads/-/system/group/avatar/999/avatar.png"
			}
			`)
		})

	avatar := new(bytes.Buffer)
	groupAvatar := &GroupAvatar{
		Image:    avatar,
		Filename: "avatar.png",
	}
	opt := &CreateGroupOptions{
		Name:   Ptr("g"),
		Path:   Ptr("g"),
		Avatar: groupAvatar,
		DefaultBranchProtectionDefaults: &DefaultBranchProtectionDefaultsOptions{
			AllowedToPush: &[]*GroupAccessLevel{
				{
					AccessLevel: Ptr(AccessLevelValue(40)),
				},
			},
			AllowedToMerge: &[]*GroupAccessLevel{
				{
					AccessLevel: Ptr(AccessLevelValue(40)),
				},
			},
			CodeOwnerApprovalRequired: Ptr(true),
		},
	}

	var _ query.Encoder = &DefaultBranchProtectionDefaultsOptions{}

	group, _, err := client.Groups.CreateGroup(opt, nil)
	require.NoError(t, err)

	// Create the group that we want to get back
	want := &Group{
		ID:        1,
		Name:      "g",
		Path:      "g",
		AvatarURL: "http://localhost/uploads/-/system/group/avatar/999/avatar.png",
		DefaultBranchProtectionDefaults: &BranchProtectionDefaults{
			AllowedToMerge: []*GroupAccessLevel{
				{
					AccessLevel: Ptr(MaintainerPermissions),
				},
			},
			AllowedToPush: []*GroupAccessLevel{
				{
					AccessLevel: Ptr(MaintainerPermissions),
				},
			},
			CodeOwnerApprovalRequired: true,
		},
	}

	assert.Equal(t, want, group)
}

// Test to ensure an error occurs when providing an avatar and multiple
// allowed to push / allowed to merge access levels during group creation
func TestCreateGroupMultipleDefaultBranchSettingsWithAvatarShouldError(t *testing.T) {
	t.Parallel()
	_, client := setup(t)

	avatar := new(bytes.Buffer)
	groupAvatar := &GroupAvatar{
		Image:    avatar,
		Filename: "avatar.png",
	}
	opt := &CreateGroupOptions{
		Name:   Ptr("g"),
		Path:   Ptr("g"),
		Avatar: groupAvatar,
		DefaultBranchProtectionDefaults: &DefaultBranchProtectionDefaultsOptions{
			AllowedToPush: &[]*GroupAccessLevel{
				{
					AccessLevel: Ptr(AccessLevelValue(40)),
				},
				{
					AccessLevel: Ptr(AccessLevelValue(30)),
				},
			},
			AllowedToMerge: &[]*GroupAccessLevel{
				{
					AccessLevel: Ptr(AccessLevelValue(40)),
				},
				{
					AccessLevel: Ptr(AccessLevelValue(30)),
				},
			},
		},
	}

	var _ query.Encoder = &DefaultBranchProtectionDefaultsOptions{}

	group, _, err := client.Groups.CreateGroup(opt, nil)
	assert.Error(t, err)
	assert.Equal(t, "multiple access levels for allowed_to_merge or allowed_to_push are not permitted when an Avatar is also specified as it will result in unexpected behavior", err.Error())
	assert.Nil(t, group)
}

// Test to ensure an error occurs when providing an avatar and multiple
// allowed to push / allowed to merge access levels during group update
func TestUpdateGroupMultipleDefaultBranchSettingsWithAvatarShouldError(t *testing.T) {
	t.Parallel()
	_, client := setup(t)

	avatar := new(bytes.Buffer)
	groupAvatar := &GroupAvatar{
		Image:    avatar,
		Filename: "avatar.png",
	}
	opt := &UpdateGroupOptions{
		Name:   Ptr("g"),
		Path:   Ptr("g"),
		Avatar: groupAvatar,
		DefaultBranchProtectionDefaults: &DefaultBranchProtectionDefaultsOptions{
			AllowedToPush: &[]*GroupAccessLevel{
				{
					AccessLevel: Ptr(AccessLevelValue(40)),
				},
				{
					AccessLevel: Ptr(AccessLevelValue(30)),
				},
			},
			AllowedToMerge: &[]*GroupAccessLevel{
				{
					AccessLevel: Ptr(AccessLevelValue(40)),
				},
				{
					AccessLevel: Ptr(AccessLevelValue(30)),
				},
			},
		},
	}

	var _ query.Encoder = &DefaultBranchProtectionDefaultsOptions{}

	group, _, err := client.Groups.UpdateGroup(1, opt)
	assert.Error(t, err)
	assert.Equal(t, "multiple access levels for allowed_to_merge or allowed_to_push are not permitted when an Avatar is also specified as it will result in unexpected behavior", err.Error())
	assert.Nil(t, group)
}

func TestCreateGroupWithAvatar(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodPost)

			testFormBody(t, r, "name", "g")
			testFormBody(t, r, "path", "g")

			fmt.Fprint(w, `
			{
				"id": 1,
				"name": "g",
				"path": "g",
				"avatar_url":"http://localhost/uploads/-/system/group/avatar/999/avatar.png"
			}
			`)
		})

	avatar := new(bytes.Buffer)
	groupAvatar := &GroupAvatar{
		Image:    avatar,
		Filename: "avatar.png",
	}
	opt := &CreateGroupOptions{
		Name:   Ptr("g"),
		Path:   Ptr("g"),
		Avatar: groupAvatar,
	}

	group, _, err := client.Groups.CreateGroup(opt, nil)
	require.NoError(t, err)

	// Create the group that we want to get back
	want := &Group{
		ID:        1,
		Name:      "g",
		Path:      "g",
		AvatarURL: "http://localhost/uploads/-/system/group/avatar/999/avatar.png",
	}

	assert.Equal(t, want, group)
}

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

	// Test GetGroup with merge settings
	t.Run("GetGroup includes merge settings", func(t *testing.T) {
		t.Parallel()

		mux, client := setup(t) // Create a new mux for each subtest

		mux.HandleFunc("/api/v4/groups/1", func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodGet)
			fmt.Fprint(w, `{
				"id": 1,
				"name": "Test Group",
				"path": "test-group",
				"only_allow_merge_if_pipeline_succeeds": true,
				"allow_merge_on_skipped_pipeline": false,
				"only_allow_merge_if_all_discussions_are_resolved": true
			}`)
		})

		group, _, err := client.Groups.GetGroup(1, nil)
		require.NoError(t, err)

		assert.True(t, group.OnlyAllowMergeIfPipelineSucceeds)
		assert.False(t, group.AllowMergeOnSkippedPipeline)
		assert.True(t, group.OnlyAllowMergeIfAllDiscussionsAreResolved)
	})

	// Test UpdateGroup with merge settings
	t.Run("UpdateGroup with merge settings", func(t *testing.T) {
		t.Parallel()

		mux, client := setup(t) // Create a new mux for each subtest

		mux.HandleFunc("/api/v4/groups/1", func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodPut)

			testBodyJSON(t, r, map[string]bool{
				"only_allow_merge_if_pipeline_succeeds":            false,
				"allow_merge_on_skipped_pipeline":                  true,
				"only_allow_merge_if_all_discussions_are_resolved": false,
			})

			fmt.Fprint(w, `{
				"id": 1,
				"name": "Updated Group",
				"path": "updated-group",
				"only_allow_merge_if_pipeline_succeeds": false,
				"allow_merge_on_skipped_pipeline": true,
				"only_allow_merge_if_all_discussions_are_resolved": false
			}`)
		})

		opt := &UpdateGroupOptions{
			OnlyAllowMergeIfPipelineSucceeds:          Ptr(false),
			AllowMergeOnSkippedPipeline:               Ptr(true),
			OnlyAllowMergeIfAllDiscussionsAreResolved: Ptr(false),
		}

		group, _, err := client.Groups.UpdateGroup(1, opt)
		require.NoError(t, err)

		assert.False(t, group.OnlyAllowMergeIfPipelineSucceeds)
		assert.True(t, group.AllowMergeOnSkippedPipeline)
		assert.False(t, group.OnlyAllowMergeIfAllDiscussionsAreResolved)
	})
}

func TestArchiveGroup(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)
	mux.HandleFunc("/api/v4/groups/1/archive",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodPost)
			fmt.Fprint(w, `{"id": 1, "name": "g"}`)
		})

	_, err := client.Groups.ArchiveGroup(1)
	require.NoError(t, err)
}

func TestUnarchiveGroup(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)
	mux.HandleFunc("/api/v4/groups/1/unarchive",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodPost)
			fmt.Fprint(w, `{"id": 1, "name": "g"}`)
		})

	_, err := client.Groups.UnarchiveGroup(1)
	require.NoError(t, err)
}

func TestListSAMLUsers(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN the SAML users endpoint returns one user and the request carries
	// the filter query parameters.
	mux.HandleFunc("/api/v4/groups/1/saml_users",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodGet)
			testParam(t, r, "username", "alice")
			testParam(t, r, "active", "true")
			fmt.Fprint(w, `[{"id": 7, "username": "alice"}]`)
		})

	// WHEN ListSAMLUsers is called with filters.
	users, _, err := client.Groups.ListSAMLUsers(1, &ListSAMLUsersOptions{
		Username: Ptr("alice"),
		Active:   Ptr(true),
	})
	require.NoError(t, err)

	// THEN the returned slice contains the parsed user.
	want := []*User{{ID: 7, Username: "alice"}}
	assert.Equal(t, want, users)
}

func TestListGroupsSharedWith(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN the shared-groups endpoint returns one group and the request
	// carries the search filter.
	mux.HandleFunc("/api/v4/groups/1/groups/shared",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodGet)
			testParam(t, r, "search", "foo")
			fmt.Fprint(w, `[{"id": 2, "name": "shared"}]`)
		})

	// WHEN ListGroupsSharedWith is called.
	groups, _, err := client.Groups.ListGroupsSharedWith(1, &ListGroupsSharedWithOptions{
		Search: Ptr("foo"),
	})
	require.NoError(t, err)

	// THEN the response is unmarshaled into Group slice.
	want := []*Group{{ID: 2, Name: "shared"}}
	assert.Equal(t, want, groups)
}

func TestListInvitedGroups(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN the invited-groups endpoint returns one group and the request
	// carries the relation filter.
	mux.HandleFunc("/api/v4/groups/1/invited_groups",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodGet)
			testParam(t, r, "relation", "direct,inherited")
			fmt.Fprint(w, `[{"id": 3, "name": "invited"}]`)
		})

	// WHEN ListInvitedGroups is called.
	groups, _, err := client.Groups.ListInvitedGroups(1, &ListInvitedGroupsOptions{
		Relation: &[]string{"direct", "inherited"},
	})
	require.NoError(t, err)

	// THEN the response is unmarshaled into Group slice.
	want := []*Group{{ID: 3, Name: "invited"}}
	assert.Equal(t, want, groups)
}

func TestListTransferLocations(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN the transfer-locations endpoint returns one parent candidate.
	mux.HandleFunc("/api/v4/groups/1/transfer_locations",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodGet)
			testParam(t, r, "search", "parent")
			fmt.Fprint(w, `[{"id": 9, "name": "Parent", "full_path": "parent", "web_url": "https://gitlab.example.com/groups/parent"}]`)
		})

	// WHEN ListTransferLocations is called.
	locations, _, err := client.Groups.ListTransferLocations(1, &ListTransferLocationsOptions{
		Search: Ptr("parent"),
	})
	require.NoError(t, err)

	// THEN the response is unmarshaled into TransferLocation slice.
	want := []*TransferLocation{{
		ID:       9,
		Name:     "Parent",
		FullPath: "parent",
		WebURL:   "https://gitlab.example.com/groups/parent",
	}}
	assert.Equal(t, want, locations)
}

func TestSyncGroupWithLDAP(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	// GIVEN the LDAP sync endpoint responds with 202 Accepted.
	mux.HandleFunc("/api/v4/groups/1/ldap_sync",
		func(w http.ResponseWriter, r *http.Request) {
			testMethod(t, r, http.MethodPost)
			w.WriteHeader(http.StatusAccepted)
		})

	// WHEN SyncGroupWithLDAP is called.
	resp, err := client.Groups.SyncGroupWithLDAP(1)
	require.NoError(t, err)

	// THEN the response carries the 202 status.
	require.NotNil(t, resp)
	assert.Equal(t, http.StatusAccepted, resp.StatusCode)
}
