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

package iam

import (
	"context"
	"testing"

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

func Test_NewGroupMember(t *testing.T) {
	t.Parallel()
	ctx := context.Background()
	conn, _ := db.TestSetup(t, "postgres")
	wrapper := db.TestWrapper(t)
	repo := TestRepo(t, conn, wrapper)
	org, proj := TestScopes(t, repo)
	org2, _ := TestScopes(t, repo)

	orgGroup := TestGroup(t, conn, org.PublicId)
	projGroup := TestGroup(t, conn, proj.PublicId)
	user := TestUser(t, repo, org.PublicId)
	user2 := TestUser(t, repo, org2.PublicId)

	type args struct {
		groupId string
		userId  string
		opt     []Option
	}
	tests := []struct {
		name      string
		args      args
		want      *GroupMemberUser
		wantErr   bool
		wantIsErr errors.Code
	}{
		{
			name: "valid-org",
			args: args{
				groupId: orgGroup.PublicId,
				userId:  user.PublicId,
			},
			want: func() *GroupMemberUser {
				gm := allocGroupMember()
				gm.GroupId = orgGroup.PublicId
				gm.MemberId = user.PublicId
				return &gm
			}(),
		},
		{
			name: "valid-proj",
			args: args{
				groupId: projGroup.PublicId,
				userId:  user.PublicId,
			},
			want: func() *GroupMemberUser {
				gm := allocGroupMember()
				gm.GroupId = projGroup.PublicId
				gm.MemberId = user.PublicId
				return &gm
			}(),
		},
		{
			name: "cross-org",
			args: args{
				groupId: orgGroup.PublicId,
				userId:  user2.PublicId,
			},
			want: func() *GroupMemberUser {
				gm := allocGroupMember()
				gm.GroupId = orgGroup.PublicId
				gm.MemberId = user2.PublicId
				return &gm
			}(),
		},
		{
			name: "missing-group",
			args: args{
				userId: user.PublicId,
			},
			want:      nil,
			wantErr:   true,
			wantIsErr: errors.InvalidParameter,
		},
		{
			name: "missing-user",
			args: args{
				groupId: projGroup.PublicId,
			},
			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 := NewGroupMemberUser(ctx, tt.args.groupId, tt.args.userId, 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 Test_GroupMemberCreate(t *testing.T) {
	t.Parallel()
	ctx := context.Background()
	conn, _ := db.TestSetup(t, "postgres")
	wrapper := db.TestWrapper(t)
	repo := TestRepo(t, conn, wrapper)
	org, proj := TestScopes(t, repo)
	type args struct {
		gm *GroupMemberUser
	}
	tests := []struct {
		name       string
		args       args
		wantDup    bool
		wantErr    bool
		wantErrMsg string
		wantIsErr  errors.Code
	}{
		{
			name: "valid-with-org",
			args: args{
				gm: func() *GroupMemberUser {
					g := TestGroup(t, conn, org.PublicId)
					u := TestUser(t, repo, org.PublicId)
					gm, err := NewGroupMemberUser(ctx, g.PublicId, u.PublicId)
					require.NoError(t, err)
					return gm
				}(),
			},
			wantErr: false,
		},
		{
			name: "valid-with-proj",
			args: args{
				gm: func() *GroupMemberUser {
					g := TestGroup(t, conn, proj.PublicId)
					u := TestUser(t, repo, org.PublicId)
					gm, err := NewGroupMemberUser(ctx, g.PublicId, u.PublicId)
					require.NoError(t, err)
					return gm
				}(),
			},
			wantErr: false,
		},
		{
			name: "bad-group-id",
			args: args{
				gm: func() *GroupMemberUser {
					id := testId(t)
					u := TestUser(t, repo, org.PublicId)
					gm, err := NewGroupMemberUser(ctx, id, u.PublicId)
					require.NoError(t, err)
					return gm
				}(),
			},
			wantErr:    true,
			wantErrMsg: `db.Create: insert or update on table "iam_group_member_user" violates foreign key constraint`,
		},
		{
			name: "bad-user-id",
			args: args{
				gm: func() *GroupMemberUser {
					id := testId(t)
					g := TestGroup(t, conn, proj.PublicId)
					gm, err := NewGroupMemberUser(ctx, g.PublicId, id)
					require.NoError(t, err)
					return gm
				}(),
			},
			wantErr:    true,
			wantErrMsg: `integrity violation: error #1003`,
		},
		{
			name: "missing-group-id",
			args: args{
				gm: func() *GroupMemberUser {
					u := TestUser(t, repo, org.PublicId)
					return &GroupMemberUser{
						GroupMemberUser: &store.GroupMemberUser{
							GroupId:  "",
							MemberId: u.PublicId,
						},
					}
				}(),
			},
			wantErr:   true,
			wantIsErr: errors.InvalidParameter,
		},
		{
			name: "missing-user-id",
			args: args{
				gm: func() *GroupMemberUser {
					g := TestGroup(t, conn, org.PublicId)
					return &GroupMemberUser{
						GroupMemberUser: &store.GroupMemberUser{
							GroupId:  g.PublicId,
							MemberId: "",
						},
					}
				}(),
			},
			wantErr:   true,
			wantIsErr: errors.InvalidParameter,
		},
		{
			name: "dup-at-org",
			args: args{
				gm: func() *GroupMemberUser {
					g := TestGroup(t, conn, org.PublicId)
					u := TestUser(t, repo, org.PublicId)
					gm, err := NewGroupMemberUser(ctx, g.PublicId, u.PublicId)
					require.NoError(t, err)
					return gm
				}(),
			},
			wantDup:    true,
			wantErr:    true,
			wantErrMsg: `db.Create: duplicate key value violates unique constraint "iam_group_member_user_pkey"`,
		},
	}

	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 {
				gm := tt.args.gm.Clone().(*GroupMemberUser)
				err := w.Create(context.Background(), gm)
				require.NoError(err)
			}
			gm := tt.args.gm.Clone().(*GroupMemberUser)
			err := w.Create(context.Background(), gm)
			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 := allocGroupMember()
			err = w.LookupWhere(context.Background(), &found, "group_id = ? and member_id = ?", []any{gm.GroupId, gm.MemberId})
			require.NoError(err)
			assert.Empty(cmp.Diff(gm, &found, protocmp.Transform()))
		})
	}
}

func Test_GroupMemberUpdate(t *testing.T) {
	t.Parallel()
	conn, _ := db.TestSetup(t, "postgres")
	wrapper := db.TestWrapper(t)
	repo := TestRepo(t, conn, wrapper)
	org, _ := TestScopes(t, repo)
	rw := db.New(conn)

	t.Run("updates not allowed", func(t *testing.T) {
		assert, require := assert.New(t), require.New(t)
		g := TestGroup(t, conn, org.PublicId)
		u := TestUser(t, repo, org.PublicId)
		u2 := TestUser(t, repo, org.PublicId)
		gm := TestGroupMember(t, conn, g.PublicId, u.PublicId)
		updateGrpMember := gm.Clone().(*GroupMemberUser)
		updateGrpMember.MemberId = u2.PublicId
		updatedRows, err := rw.Update(context.Background(), updateGrpMember, []string{"MemberId"}, nil)
		require.Error(err)
		assert.Equal(0, updatedRows)
	})
}

func Test_GroupMemberDelete(t *testing.T) {
	t.Parallel()
	conn, _ := db.TestSetup(t, "postgres")
	wrapper := db.TestWrapper(t)
	repo := TestRepo(t, conn, wrapper)
	rw := db.New(conn)
	id := testId(t)
	org, _ := TestScopes(t, repo)
	u := TestUser(t, repo, org.PublicId)
	g := TestGroup(t, conn, org.PublicId)

	tests := []struct {
		name            string
		gm              *GroupMemberUser
		wantRowsDeleted int
		wantErr         bool
		wantErrMsg      string
	}{
		{
			name:            "valid",
			gm:              TestGroupMember(t, conn, g.PublicId, u.PublicId),
			wantErr:         false,
			wantRowsDeleted: 1,
		},
		{
			name:            "bad-id",
			gm:              func() *GroupMemberUser { gm := allocGroupMember(); gm.MemberId = id; gm.GroupId = id; return &gm }(),
			wantErr:         false,
			wantRowsDeleted: 0,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert, require := assert.New(t), require.New(t)
			deleteGroup := allocGroupMember()
			deleteGroup.GroupId = tt.gm.GetGroupId()
			deleteGroup.MemberId = tt.gm.GetMemberId()
			deletedRows, err := rw.Delete(context.Background(), &deleteGroup)
			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 := allocGroupMember()
			err = rw.LookupWhere(context.Background(), &found, "group_id = ? and member_id = ?", []any{tt.gm.GetGroupId(), tt.gm.GetMemberId()})
			require.Error(err)
			assert.True(errors.IsNotFoundError(err))
		})
	}
}

func TestGroupMember_Clone(t *testing.T) {
	t.Parallel()
	conn, _ := db.TestSetup(t, "postgres")
	wrapper := db.TestWrapper(t)
	repo := TestRepo(t, conn, wrapper)
	org, proj := TestScopes(t, repo)
	user := TestUser(t, repo, org.PublicId)
	t.Run("valid", func(t *testing.T) {
		assert := assert.New(t)
		group := TestGroup(t, conn, org.PublicId)
		gm := TestGroupMember(t, conn, group.PublicId, user.PublicId)
		cp := gm.Clone()
		assert.True(proto.Equal(cp.(*GroupMemberUser).GroupMemberUser, gm.GroupMemberUser))
	})
	t.Run("not-equal", func(t *testing.T) {
		assert := assert.New(t)
		g := TestGroup(t, conn, org.PublicId)
		g2 := TestGroup(t, conn, proj.PublicId)
		gm := TestGroupMember(t, conn, g.PublicId, user.PublicId)
		gm2 := TestGroupMember(t, conn, g2.PublicId, user.PublicId)
		cp := gm.Clone()
		assert.True(!proto.Equal(cp.(*GroupMemberUser).GroupMemberUser, gm2.GroupMemberUser))
	})
}

func TestGroupMember_SetTableName(t *testing.T) {
	defaultTableName := groupMemberViewDefaultTableName
	tests := []struct {
		name        string
		initialName string
		setNameTo   string
		want        string
	}{
		{
			name:        "new-name",
			initialName: "",
			setNameTo:   "new-name",
			want:        "new-name",
		},
		{
			name:        "reset to default",
			initialName: "initial",
			setNameTo:   "",
			want:        defaultTableName,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert, require := assert.New(t), require.New(t)
			def := &GroupMember{
				GroupMemberView: &store.GroupMemberView{},
			}
			require.Equal(defaultTableName, def.TableName())
			s := &GroupMember{
				GroupMemberView: &store.GroupMemberView{},
				tableName:       tt.initialName,
			}
			s.SetTableName(tt.setNameTo)
			assert.Equal(tt.want, s.TableName())
		})
	}
}

func TestGroupMemberUser_SetTableName(t *testing.T) {
	defaultTableName := groupMemberUserDefaultTable
	tests := []struct {
		name        string
		initialName string
		setNameTo   string
		want        string
	}{
		{
			name:        "new-name",
			initialName: "",
			setNameTo:   "new-name",
			want:        "new-name",
		},
		{
			name:        "reset to default",
			initialName: "initial",
			setNameTo:   "",
			want:        defaultTableName,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert, require := assert.New(t), require.New(t)
			def := allocGroupMember()
			require.Equal(defaultTableName, def.TableName())
			s := &GroupMemberUser{
				GroupMemberUser: &store.GroupMemberUser{},
				tableName:       tt.initialName,
			}
			s.SetTableName(tt.setNameTo)
			assert.Equal(tt.want, s.TableName())
		})
	}
}
