// Copyright IBM Corp. 2020, 2026
// SPDX-License-Identifier: BUSL-1.1

package iam_test

import (
	"context"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/hashicorp/boundary/internal/auth/oidc"
	"github.com/hashicorp/boundary/internal/db"
	"github.com/hashicorp/boundary/internal/errors"
	"github.com/hashicorp/boundary/internal/iam"
	"github.com/hashicorp/boundary/internal/iam/store"
	"github.com/hashicorp/boundary/internal/kms"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/testing/protocmp"
)

// This is in an iam_test package because managed groups are an abstract type so
// we need to reach into an implementation for testing, which itself reaches
// into IAM. So this avoids a dependency loop.

func TestNewManagedGroupRole(t *testing.T) {
	t.Parallel()
	ctx := context.Background()
	conn, _ := db.TestSetup(t, "postgres")
	wrapper := db.TestWrapper(t)
	repo := iam.TestRepo(t, conn, wrapper)
	org, _ := iam.TestScopes(t, repo)
	orgRole := iam.TestRole(t, conn, org.PublicId)
	const managedGroupId = "mgoidc_1234567890"

	type args struct {
		roleId         string
		ManagedGroupId string
		opt            []iam.Option
	}
	tests := []struct {
		name      string
		args      args
		want      *iam.ManagedGroupRole
		wantErr   bool
		wantIsErr errors.Code
	}{
		{
			name: "valid-org",
			args: args{
				roleId:         orgRole.PublicId,
				ManagedGroupId: managedGroupId,
			},
			want: func() *iam.ManagedGroupRole {
				r := iam.AllocManagedGroupRole()
				r.RoleId = orgRole.PublicId
				r.PrincipalId = managedGroupId
				return &r
			}(),
		},
		{
			name: "empty-role-id",
			args: args{
				roleId:         "",
				ManagedGroupId: managedGroupId,
			},
			want:      nil,
			wantErr:   true,
			wantIsErr: errors.InvalidParameter,
		},
		{
			name: "empty-ManagedGroup-id",
			args: args{
				roleId:         orgRole.PublicId,
				ManagedGroupId: "",
			},
			want:      nil,
			wantErr:   true,
			wantIsErr: errors.InvalidParameter,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert, require := assert.New(t), require.New(t)
			got, err := iam.NewManagedGroupRole(ctx, tt.args.roleId, tt.args.ManagedGroupId, tt.args.opt...)
			if tt.wantErr {
				require.Error(err)
				assert.True(errors.Match(errors.T(tt.wantIsErr), err))
				return
			}
			require.NoError(err)
			assert.Equal(tt.want, got)
		})
	}
}

func TestManagedGroupRole_Create(t *testing.T) {
	t.Parallel()
	conn, _ := db.TestSetup(t, "postgres")

	wrap := db.TestWrapper(t)
	kmsCache := kms.TestKms(t, conn, wrap)

	repo := iam.TestRepo(t, conn, wrap)
	org, _ := iam.TestScopes(t, repo)
	org2, _ := iam.TestScopes(t, repo)

	ctx := context.Background()
	databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase)
	require.NoError(t, err)

	authMethod := oidc.TestAuthMethod(
		t, conn, databaseWrapper, org.GetPublicId(), oidc.ActivePrivateState,
		"alice-rp", "fido",
		oidc.WithSigningAlgs(oidc.RS256),
		oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]),
		oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]),
	)

	mg := oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter)

	type args struct {
		role *iam.ManagedGroupRole
	}
	tests := []struct {
		name       string
		args       args
		wantDup    bool
		wantErr    bool
		wantErrMsg string
		wantIsErr  errors.Code
	}{
		{
			name: "valid-with-org",
			args: args{
				role: func() *iam.ManagedGroupRole {
					role := iam.TestRole(t, conn, org.PublicId)
					principalRole, err := iam.NewManagedGroupRole(ctx, role.PublicId, mg.PublicId)
					require.NoError(t, err)
					return principalRole
				}(),
			},
			wantErr: false,
		},
		{
			name: "cross-org",
			args: args{
				role: func() *iam.ManagedGroupRole {
					role := iam.TestRole(t, conn, org2.PublicId)
					principalRole, err := iam.NewManagedGroupRole(ctx, role.PublicId, mg.PublicId)
					require.NoError(t, err)
					return principalRole
				}(),
			},
		},
		{
			name: "bad-role-id",
			args: args{
				role: func() *iam.ManagedGroupRole {
					id := testId(t)
					principalRole, err := iam.NewManagedGroupRole(ctx, id, mg.PublicId)
					require.NoError(t, err)
					return principalRole
				}(),
			},
			wantErr:    true,
			wantErrMsg: "integrity violation: error #1003",
		},
		{
			name: "bad-principal-id",
			args: args{
				role: func() *iam.ManagedGroupRole {
					id := testId(t)
					role := iam.TestRole(t, conn, org.PublicId)
					principalRole, err := iam.NewManagedGroupRole(ctx, role.PublicId, id)
					require.NoError(t, err)
					return principalRole
				}(),
			},
			wantErr:    true,
			wantErrMsg: "integrity violation: error #1003",
		},
		{
			name: "missing-role-id",
			args: args{
				role: func() *iam.ManagedGroupRole {
					return &iam.ManagedGroupRole{
						ManagedGroupRole: &store.ManagedGroupRole{
							RoleId:      "",
							PrincipalId: mg.PublicId,
						},
					}
				}(),
			},
			wantErr:    true,
			wantErrMsg: "iam.(ManagedGroupRole).VetForWrite: missing role id: parameter violation: error #100",
			wantIsErr:  errors.InvalidParameter,
		},
		{
			name: "missing-principal-id",
			args: args{
				role: func() *iam.ManagedGroupRole {
					role := iam.TestRole(t, conn, org.PublicId)
					return &iam.ManagedGroupRole{
						ManagedGroupRole: &store.ManagedGroupRole{
							RoleId:      role.PublicId,
							PrincipalId: "",
						},
					}
				}(),
			},
			wantErr:    true,
			wantErrMsg: "iam.(ManagedGroupRole).VetForWrite: missing managed group id: parameter violation: error #100",
			wantIsErr:  errors.InvalidParameter,
		},
		{
			name: "dup-at-org",
			args: args{
				role: func() *iam.ManagedGroupRole {
					role := iam.TestRole(t, conn, org.PublicId)
					principalRole, err := iam.NewManagedGroupRole(ctx, role.PublicId, mg.PublicId)
					require.NoError(t, err)
					return principalRole
				}(),
			},
			wantDup:    true,
			wantErr:    true,
			wantErrMsg: `db.Create: duplicate key value violates unique constraint "iam_managed_group_role_pkey": unique constraint violation`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert, require := assert.New(t), require.New(t)
			w := db.New(conn)
			if tt.wantDup {
				r := tt.args.role.Clone().(*iam.ManagedGroupRole)
				err := w.Create(context.Background(), r)
				require.NoError(err)
			}
			r := tt.args.role.Clone().(*iam.ManagedGroupRole)
			err := w.Create(context.Background(), r)
			if tt.wantErr {
				require.Error(err)
				assert.Contains(err.Error(), tt.wantErrMsg)
				assert.True(errors.Match(errors.T(tt.wantIsErr), err))
				return
			}
			assert.NoError(err)

			found := iam.AllocManagedGroupRole()
			err = w.LookupWhere(context.Background(), &found, "role_id = ? and principal_id = ?", []any{r.RoleId, r.PrincipalId})
			require.NoError(err)
			assert.Empty(cmp.Diff(r, &found, protocmp.Transform()))
		})
	}
}

func TestManagedGroupRole_Update(t *testing.T) {
	t.Parallel()
	conn, _ := db.TestSetup(t, "postgres")
	rw := db.New(conn)

	wrap := db.TestWrapper(t)
	kmsCache := kms.TestKms(t, conn, wrap)

	repo := iam.TestRepo(t, conn, wrap)
	org, _ := iam.TestScopes(t, repo)

	ctx := context.Background()
	databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase)
	require.NoError(t, err)

	authMethod := oidc.TestAuthMethod(
		t, conn, databaseWrapper, org.GetPublicId(), oidc.ActivePrivateState,
		"alice-rp", "fido",
		oidc.WithSigningAlgs(oidc.RS256),
		oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]),
		oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]),
	)

	t.Run("updates not allowed", func(t *testing.T) {
		assert, require := assert.New(t), require.New(t)
		r := iam.TestRole(t, conn, org.PublicId)
		mg := oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter)
		mg2 := oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter)
		mgr := iam.TestManagedGroupRole(t, conn, r.PublicId, mg.PublicId)
		updateRole := mgr.Clone().(*iam.ManagedGroupRole)
		updateRole.PrincipalId = mg2.PublicId
		updatedRows, err := rw.Update(context.Background(), updateRole, []string{"PrincipalId"}, nil)
		require.Error(err)
		assert.Equal(0, updatedRows)
	})
}

func TestManagedGroupRole_Delete(t *testing.T) {
	t.Parallel()
	conn, _ := db.TestSetup(t, "postgres")
	rw := db.New(conn)

	wrap := db.TestWrapper(t)
	kmsCache := kms.TestKms(t, conn, wrap)

	repo := iam.TestRepo(t, conn, wrap)
	org, _ := iam.TestScopes(t, repo)

	ctx := context.Background()
	databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase)
	require.NoError(t, err)

	authMethod := oidc.TestAuthMethod(
		t, conn, databaseWrapper, org.GetPublicId(), oidc.ActivePrivateState,
		"alice-rp", "fido",
		oidc.WithSigningAlgs(oidc.RS256),
		oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]),
		oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]),
	)

	id := testId(t)
	mg := oidc.TestManagedGroup(t, conn, authMethod, oidc.TestFakeManagedGroupFilter)
	r := iam.TestRole(t, conn, org.PublicId)

	tests := []struct {
		name            string
		role            *iam.ManagedGroupRole
		wantRowsDeleted int
		wantErr         bool
		wantErrMsg      string
	}{
		{
			name:            "valid",
			role:            iam.TestManagedGroupRole(t, conn, r.PublicId, mg.PublicId),
			wantErr:         false,
			wantRowsDeleted: 1,
		},
		{
			name: "bad-id",
			role: func() *iam.ManagedGroupRole {
				r := iam.AllocManagedGroupRole()
				r.PrincipalId = id
				r.RoleId = id
				return &r
			}(),
			wantErr:         false,
			wantRowsDeleted: 0,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert, require := assert.New(t), require.New(t)
			deleteRole := iam.AllocManagedGroupRole()
			deleteRole.RoleId = tt.role.GetRoleId()
			deleteRole.PrincipalId = tt.role.GetPrincipalId()
			deletedRows, err := rw.Delete(context.Background(), &deleteRole)
			if tt.wantErr {
				require.Error(err)
				return
			}
			require.NoError(err)
			if tt.wantRowsDeleted == 0 {
				assert.Equal(tt.wantRowsDeleted, deletedRows)
				return
			}
			assert.Equal(tt.wantRowsDeleted, deletedRows)
			found := iam.AllocManagedGroupRole()
			err = rw.LookupWhere(context.Background(), &found, "role_id = ? and principal_id = ?", []any{tt.role.GetRoleId(), tt.role.GetPrincipalId()})
			require.Error(err)
			assert.True(errors.IsNotFoundError(err))
		})
	}
}

func TestManagedGroupRole_Clone(t *testing.T) {
	t.Parallel()
	ctx := context.Background()
	t.Run("valid", func(t *testing.T) {
		assert, require := assert.New(t), require.New(t)
		mgr, err := iam.NewManagedGroupRole(ctx, "r_abc", "mgoidc_abc")
		require.NoError(err)
		cp := mgr.Clone()
		assert.True(proto.Equal(cp.(*iam.ManagedGroupRole).ManagedGroupRole, mgr.ManagedGroupRole))
	})
	t.Run("not-equal", func(t *testing.T) {
		assert, require := assert.New(t), require.New(t)
		mgr, err := iam.NewManagedGroupRole(ctx, "r_abc", "mgoidc_abc")
		require.NoError(err)
		mgr2, err := iam.NewManagedGroupRole(ctx, "r_xyz", "mgoidc_xyz")
		require.NoError(err)
		cp := mgr.Clone()

		assert.True(!proto.Equal(cp.(*iam.ManagedGroupRole).ManagedGroupRole, mgr2.ManagedGroupRole))
	})
}
