//go:build integration

package integration_test

import (
	"context"
	_ "embed"
	"net/http"
	"path"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/text/language"

	"github.com/zitadel/zitadel/internal/api/scim/resources"
	"github.com/zitadel/zitadel/internal/api/scim/schemas"
	"github.com/zitadel/zitadel/internal/integration"
	"github.com/zitadel/zitadel/internal/integration/scim"
	"github.com/zitadel/zitadel/internal/test"
	"github.com/zitadel/zitadel/pkg/grpc/management"
	"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)

var (
	//go:embed testdata/users_replace_test_minimal_with_external_id.json
	minimalUserWithExternalIDJson []byte

	//go:embed testdata/users_replace_test_minimal.json
	minimalUserReplaceJson []byte

	//go:embed testdata/users_replace_test_full.json
	fullUserReplaceJson []byte
)

func TestReplaceUser(t *testing.T) {
	tests := []struct {
		name             string
		body             []byte
		ctx              context.Context
		createUserOrgID  string
		replaceUserOrgID string
		want             *resources.ScimUser
		wantErr          bool
		scimErrorType    string
		errorStatus      int
		zitadelErrID     string
	}{
		{
			name: "minimal user",
			body: minimalUserReplaceJson,
			want: &resources.ScimUser{
				UserName: "acmeUser1-minimal-replaced",
				Name: &resources.ScimUserName{
					FamilyName: "Ross-replaced",
					GivenName:  "Bethany-replaced",
				},
				Emails: []*resources.ScimEmail{
					{
						Value:   "user1-minimal-replaced@example.com",
						Primary: true,
					},
				},
			},
		},
		{
			name: "full user",
			body: fullUserReplaceJson,
			want: &resources.ScimUser{
				ExternalID: "701984-updated",
				UserName:   "bjensen-replaced-full@example.com",
				Name: &resources.ScimUserName{
					Formatted:       "Babs Jensen-updated", // display name takes precedence
					FamilyName:      "Jensen-updated",
					GivenName:       "Barbara-updated",
					MiddleName:      "Jane-updated",
					HonorificPrefix: "Ms.-updated",
					HonorificSuffix: "III",
				},
				DisplayName: "Babs Jensen-updated",
				NickName:    "Babs-updated",
				ProfileUrl:  test.Must(schemas.ParseHTTPURL("http://login.example.com/bjensen-updated")),
				Emails: []*resources.ScimEmail{
					{
						Value:   "bjensen-replaced-full@example.com",
						Primary: true,
					},
				},
				Addresses: []*resources.ScimAddress{
					{
						Type:          "work-updated",
						StreetAddress: "100 Universal City Plaza-updated",
						Locality:      "Hollywood-updated",
						Region:        "CA-updated",
						PostalCode:    "91608-updated",
						Country:       "USA-updated",
						Formatted:     "100 Universal City Plaza\nHollywood, CA 91608 USA-updated",
						Primary:       true,
					},
					{
						Type:          "home-updated",
						StreetAddress: "456 Hollywood Blvd-updated",
						Locality:      "Hollywood-updated",
						Region:        "CA-updated",
						PostalCode:    "91608-updated",
						Country:       "USA-updated",
						Formatted:     "456 Hollywood Blvd\nHollywood, CA 91608 USA-updated",
					},
				},
				PhoneNumbers: []*resources.ScimPhoneNumber{
					{
						Value:   "+4155555555558732833",
						Primary: true,
					},
				},
				Ims: []*resources.ScimIms{
					{
						Value: "someaimhandle-updated",
						Type:  "aim-updated",
					},
					{
						Value: "twitterhandle-updated",
						Type:  "X-updated",
					},
				},
				Photos: []*resources.ScimPhoto{
					{
						Value: *test.Must(schemas.ParseHTTPURL("https://photos.example.com/profilephoto/72930000000Ccne/F-updated")),
						Type:  "photo-updated",
					},
					{
						Value: *test.Must(schemas.ParseHTTPURL("https://photos.example.com/profilephoto/72930000000Ccne/T-updated")),
						Type:  "thumbnail-updated",
					},
				},
				Roles: []*resources.ScimRole{
					{
						Value:   "my-role-1-updated",
						Display: "Rolle 1-updated",
						Type:    "main-role-updated",
						Primary: true,
					},
					{
						Value:   "my-role-2-updated",
						Display: "Rolle 2-updated",
						Type:    "secondary-role-updated",
						Primary: false,
					},
				},
				Entitlements: []*resources.ScimEntitlement{
					{
						Value:   "my-entitlement-1-updated",
						Display: "Entitlement 1-updated",
						Type:    "main-entitlement-updated",
						Primary: true,
					},
					{
						Value:   "my-entitlement-2-updated",
						Display: "Entitlement 2-updated",
						Type:    "secondary-entitlement-updated",
						Primary: false,
					},
				},
				Title:             "Tour Guide-updated",
				PreferredLanguage: language.MustParse("en-CH"),
				Locale:            "en-CH",
				Timezone:          "Europe/Zurich",
				Active:            schemas.NewRelaxedBool(false),
			},
		},
		{
			name:          "password complexity violation",
			wantErr:       true,
			scimErrorType: "invalidValue",
			body:          invalidPasswordUserJson,
		},
		{
			name:          "invalid profile url",
			wantErr:       true,
			scimErrorType: "invalidValue",
			zitadelErrID:  "SCIM-htturl1",
			body:          invalidProfileUrlUserJson,
		},
		{
			name:          "invalid time zone",
			wantErr:       true,
			scimErrorType: "invalidValue",
			body:          invalidTimeZoneUserJson,
		},
		{
			name:          "invalid locale",
			wantErr:       true,
			scimErrorType: "invalidValue",
			body:          invalidLocaleUserJson,
		},
		{
			name:        "not authenticated",
			body:        minimalUserJson,
			ctx:         context.Background(),
			wantErr:     true,
			errorStatus: http.StatusUnauthorized,
		},
		{
			name:        "no permissions",
			body:        minimalUserJson,
			ctx:         Instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
			wantErr:     true,
			errorStatus: http.StatusNotFound,
		},
		{
			name:             "another org",
			body:             minimalUserJson,
			replaceUserOrgID: SecondaryOrganization.OrganizationId,
			wantErr:          true,
			errorStatus:      http.StatusNotFound,
		},
		{
			name:             "another org with permissions",
			body:             minimalUserJson,
			replaceUserOrgID: SecondaryOrganization.OrganizationId,
			ctx:              Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner),
			wantErr:          true,
			errorStatus:      http.StatusNotFound,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// use iam owner => we don't want to test permissions of the create endpoint.
			createdUser, err := Instance.Client.SCIM.Users.Create(Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), Instance.DefaultOrg.Id, fullUserJson)
			require.NoError(t, err)

			defer func() {
				_, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID})
				assert.NoError(t, err)
			}()

			ctx := tt.ctx
			if ctx == nil {
				ctx = CTX
			}

			replaceUserOrgID := tt.replaceUserOrgID
			if replaceUserOrgID == "" {
				replaceUserOrgID = Instance.DefaultOrg.Id
			}

			replacedUser, err := Instance.Client.SCIM.Users.Replace(ctx, replaceUserOrgID, createdUser.ID, tt.body)
			if (err != nil) != tt.wantErr {
				t.Errorf("ReplaceUser() error = %v, wantErr %v", err, tt.wantErr)
			}

			if err != nil {
				statusCode := tt.errorStatus
				if statusCode == 0 {
					statusCode = http.StatusBadRequest
				}

				scimErr := scim.RequireScimError(t, statusCode, err)
				assert.Equal(t, tt.scimErrorType, scimErr.Error.ScimType)
				if tt.zitadelErrID != "" {
					assert.Equal(t, tt.zitadelErrID, scimErr.Error.ZitadelDetail.ID)
				}

				return
			}

			assert.NotEmpty(t, replacedUser.ID)
			assert.EqualValues(t, []schemas.ScimSchemaType{"urn:ietf:params:scim:schemas:core:2.0:User"}, replacedUser.Resource.Schemas)
			assert.Equal(t, schemas.ScimResourceTypeSingular("User"), replacedUser.Resource.Meta.ResourceType)
			assert.Equal(t, "http://"+Instance.Host()+path.Join(schemas.HandlerPrefix, Instance.DefaultOrg.Id, "Users", createdUser.ID), replacedUser.Resource.Meta.Location)
			assert.Nil(t, createdUser.Password)

			if !test.PartiallyDeepEqual(tt.want, replacedUser) {
				t.Errorf("ReplaceUser() got = %#v, want %#v", replacedUser, tt.want)
			}

			retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
			require.EventuallyWithT(t, func(ttt *assert.CollectT) {
				// ensure the user is really stored and not just returned to the caller
				fetchedUser, err := Instance.Client.SCIM.Users.Get(CTX, Instance.DefaultOrg.Id, replacedUser.ID)
				require.NoError(ttt, err)
				if !test.PartiallyDeepEqual(tt.want, fetchedUser) {
					ttt.Errorf("GetUser() got = %#v, want %#v", fetchedUser, tt.want)
				}
			}, retryDuration, tick)
		})
	}

}

func TestReplaceUser_removeOldMetadata(t *testing.T) {
	// ensure old metadata is removed correctly
	createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson)
	require.NoError(t, err)

	_, err = Instance.Client.SCIM.Users.Replace(CTX, Instance.DefaultOrg.Id, createdUser.ID, minimalUserJson)
	require.NoError(t, err)

	retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
	require.EventuallyWithT(t, func(tt *assert.CollectT) {
		md, err := Instance.Client.Mgmt.ListUserMetadata(CTX, &management.ListUserMetadataRequest{
			Id: createdUser.ID,
		})
		require.NoError(tt, err)
		require.Equal(tt, 0, len(md.Result))
	}, retryDuration, tick)

	_, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID})
	require.NoError(t, err)
}

func TestReplaceUser_scopedExternalID(t *testing.T) {
	// create user without provisioning domain set
	createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson)
	require.NoError(t, err)

	// set provisioning domain of service user
	setProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID, "fooBazz")

	// replace the user with provisioning domain set
	_, err = Instance.Client.SCIM.Users.Replace(CTX, Instance.DefaultOrg.Id, createdUser.ID, minimalUserWithExternalIDJson)
	require.NoError(t, err)

	retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
	require.EventuallyWithT(t, func(tt *assert.CollectT) {
		md, err := Instance.Client.Mgmt.ListUserMetadata(CTX, &management.ListUserMetadataRequest{
			Id: createdUser.ID,
		})
		require.NoError(tt, err)

		mdMap := make(map[string]string)
		for i := range md.Result {
			mdMap[md.Result[i].Key] = string(md.Result[i].Value)
		}

		// both external IDs should be present on the user
		test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:externalId", "701984")
		test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:fooBazz:externalId", "replaced-external-id")
	}, retryDuration, tick)

	_, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID})
	require.NoError(t, err)

	removeProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID)
}