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

package scopeids_test

import (
	"context"
	"testing"

	"github.com/hashicorp/boundary/internal/authtoken"
	"github.com/hashicorp/boundary/internal/daemon/controller/auth"
	"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
	"github.com/hashicorp/boundary/internal/daemon/controller/handlers/groups"
	"github.com/hashicorp/boundary/internal/daemon/controller/handlers/sessions"
	"github.com/hashicorp/boundary/internal/db"
	pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
	authpb "github.com/hashicorp/boundary/internal/gen/controller/auth"
	"github.com/hashicorp/boundary/internal/iam"
	"github.com/hashicorp/boundary/internal/kms"
	"github.com/hashicorp/boundary/internal/server"
	"github.com/hashicorp/boundary/internal/session"
	"github.com/hashicorp/boundary/internal/types/scope"
	"github.com/stretchr/testify/require"
)

// This test validates that we can perform recursive listing when there are no
// permissions on the parent scope against which the query is being run, but
// there are on child scopes. We use groups because groups are (one type of
// resource) valid in projects and we want to validate that the initial bugged
// behavior that used role permissions instead of the resource type under test
// is fixed.
func TestListingScopeIds(t *testing.T) {
	ctx := context.Background()
	conn, _ := db.TestSetup(t, "postgres")
	rw := db.New(conn)
	wrap := db.TestWrapper(t)
	kms := kms.TestKms(t, conn, wrap)
	iamRepo := iam.TestRepo(t, conn, wrap)
	iamRepoFn := func() (*iam.Repository, error) {
		return iamRepo, nil
	}
	authTokenRepoFn := func() (*authtoken.Repository, error) {
		return authtoken.NewRepository(ctx, rw, rw, kms)
	}
	serversRepoFn := func() (*server.Repository, error) {
		return server.NewRepository(ctx, rw, rw, kms)
	}
	s, err := groups.NewService(ctx, iamRepoFn, 1000)
	require.NoError(t, err)

	sessionsRepoFn := func(opt ...session.Option) (*session.Repository, error) {
		return session.NewRepository(ctx, rw, rw, kms, opt...)
	}
	sess, err := sessions.NewService(ctx, sessionsRepoFn, iamRepoFn, 1000)
	require.NoError(t, err)

	tcs := []struct {
		name         string
		globalGrants []string
		orgGrants    []string
		projGrants   []string
		globalGroups int
		orgGroups    int
		projGroups   int
		users        []string
		wantErr      error
		expCount     int
		recurseFrom  string
	}{
		{
			name:        "no perms, start at global",
			recurseFrom: "global",
			wantErr:     handlers.ForbiddenError(),
		},
		{
			name:        "no perms, start at org",
			recurseFrom: "org",
			wantErr:     handlers.ForbiddenError(),
		},
		{
			name:        "no perms, start at project",
			recurseFrom: "project",
			wantErr:     handlers.ForbiddenError(),
		},
		{
			name:         "perms on global, no groups",
			globalGrants: []string{"ids=*;type=group;actions=list,no-op"},
			projGrants:   []string{"ids=*;type=group;actions=read"},
			recurseFrom:  "global",
		},
		{
			name:        "perms on org, no groups",
			orgGrants:   []string{"ids=*;type=group;actions=list,no-op"},
			projGrants:  []string{"ids=*;type=group;actions=read"},
			recurseFrom: "org",
		},
		{
			name:        "perms on project, no groups",
			projGrants:  []string{"ids=*;type=group;actions=list,no-op"},
			recurseFrom: "project",
		},
		{
			name:         "perms on global, none in org",
			globalGrants: []string{"ids=*;type=group;actions=list,no-op"},
			projGrants:   []string{"ids=*;type=group;actions=read"},
			globalGroups: 1,
			orgGroups:    3,
			projGroups:   5,
			expCount:     6,
			recurseFrom:  "global",
		},
		{
			name:         "perms on global, none in org, start at org",
			globalGrants: []string{"ids=*;type=group;actions=list,no-op"},
			projGrants:   []string{"ids=*;type=group;actions=read"},
			globalGroups: 1,
			orgGroups:    3,
			projGroups:   5,
			expCount:     5,
			recurseFrom:  "org",
		},
		{
			name:         "perms on global, with org",
			globalGrants: []string{"ids=*;type=group;actions=list,no-op"},
			orgGrants:    []string{"ids=*;type=group;actions=read"},
			projGrants:   []string{"ids=*;type=group;actions=read"},
			globalGroups: 1,
			orgGroups:    3,
			projGroups:   5,
			expCount:     9,
			recurseFrom:  "global",
		},
		{
			name:         "perms on global, with org, start at org",
			globalGrants: []string{"ids=*;type=group;actions=list,no-op"},
			orgGrants:    []string{"ids=*;type=group;actions=read"},
			projGrants:   []string{"ids=*;type=group;actions=read"},
			globalGroups: 1,
			orgGroups:    3,
			projGroups:   5,
			expCount:     8,
			recurseFrom:  "org",
		},
		{
			name:         "perms on global, start at project",
			globalGrants: []string{"ids=*;type=group;actions=list,no-op"},
			orgGrants:    []string{"ids=*;type=group;actions=read"},
			projGrants:   []string{"ids=*;type=group;actions=read"},
			globalGroups: 1,
			orgGroups:    3,
			projGroups:   5,
			expCount:     5,
			recurseFrom:  "project",
		},
		{
			name:         "perms on org, start at global, no read on org",
			orgGrants:    []string{"ids=*;type=group;actions=list"},
			projGrants:   []string{"ids=*;type=group;actions=read"},
			globalGroups: 1,
			orgGroups:    3,
			projGroups:   5,
			expCount:     5,
			recurseFrom:  "global",
		},
		{
			name:         "perms on org, start at global, read on org",
			orgGrants:    []string{"ids=*;type=group;actions=list,no-op"},
			projGrants:   []string{"ids=*;type=group;actions=read"},
			globalGroups: 1,
			orgGroups:    3,
			projGroups:   5,
			expCount:     8,
			recurseFrom:  "global",
		},
		{
			name:         "perms on org, start at org",
			orgGrants:    []string{"ids=*;type=group;actions=list,no-op"},
			projGrants:   []string{"ids=*;type=group;actions=read"},
			globalGroups: 1,
			orgGroups:    3,
			projGroups:   5,
			expCount:     8,
			recurseFrom:  "org",
		},
		{
			name:         "perms on org, start at project",
			orgGrants:    []string{"ids=*;type=group;actions=list,no-op"},
			projGrants:   []string{"ids=*;type=group;actions=read"},
			globalGroups: 1,
			orgGroups:    3,
			projGroups:   5,
			expCount:     5,
			recurseFrom:  "project",
		},
		{
			name:         "perms on proj",
			projGrants:   []string{"ids=*;type=group;actions=list,no-op"},
			globalGroups: 1,
			orgGroups:    3,
			projGroups:   5,
			expCount:     5,
			recurseFrom:  "project",
		},
		{
			name:         "perms on proj, start at org",
			projGrants:   []string{"ids=*;type=group;actions=list,no-op"},
			globalGroups: 1,
			orgGroups:    3,
			projGroups:   5,
			expCount:     5,
			recurseFrom:  "org",
		},
		{
			name:         "perms on proj, start at global",
			projGrants:   []string{"ids=*;type=group;actions=list,no-op"},
			globalGroups: 1,
			orgGroups:    3,
			projGroups:   5,
			expCount:     5,
			recurseFrom:  "global",
		},
	}
	// Each test starts with a new set of scopes and new users/roles, which
	// ensures we don't clash and doesn't require us to deal with cleanup. Plus
	// it means subsequent tests are ensuring that only the scopes that the
	// current user should see are seen.
	for _, tc := range tcs {
		t.Run(tc.name, func(t *testing.T) {
			require := require.New(t)
			o, p := iam.TestScopes(t, iamRepo)
			at := authtoken.TestAuthToken(t, conn, kms, o.GetPublicId())
			ctx := auth.NewVerifierContext(context.Background(),
				iamRepoFn,
				authTokenRepoFn,
				serversRepoFn,
				kms,
				&authpb.RequestInfo{
					Token:       at.GetToken(),
					TokenFormat: uint32(auth.AuthTokenTypeBearer),
					PublicId:    at.GetPublicId(),
				})

			// Clean up scopes between tests
			defer func() {
				_, err := iamRepo.DeleteScope(ctx, o.GetPublicId())
				require.NoError(err)
			}()

			for i := 0; i < tc.globalGroups; i++ {
				g := iam.TestGroup(t, conn, scope.Global.String())
				defer func() {
					_, err := iamRepo.DeleteGroup(ctx, g.GetPublicId())
					require.NoError(err)
				}()
			}
			for i := 0; i < tc.orgGroups; i++ {
				iam.TestGroup(t, conn, o.GetPublicId())
			}
			for i := 0; i < tc.projGroups; i++ {
				iam.TestGroup(t, conn, p.GetPublicId())
			}

			for i, grants := range [][]string{tc.globalGrants, tc.orgGrants, tc.projGrants} {
				if len(grants) > 0 {
					pubId := scope.Global.String()
					switch i {
					case 1:
						pubId = o.GetPublicId()
					case 2:
						pubId = p.GetPublicId()
					}
					role := iam.TestRole(t, conn, pubId)
					// Clean up global between tests
					if pubId == scope.Global.String() {
						defer func() {
							_, err := iamRepo.DeleteRole(ctx, role.GetPublicId())
							require.NoError(err)
						}()
					}
					for _, grant := range grants {
						iam.TestRoleGrant(t, conn, role.GetPublicId(), grant)
					}
					iam.TestUserRole(t, conn, role.GetPublicId(), at.GetIamUserId())
				}
			}
			var startScope string
			switch tc.recurseFrom {
			case "global":
				startScope = scope.Global.String()
			case "org":
				startScope = o.GetPublicId()
			case "project":
				startScope = p.GetPublicId()
			default:
				t.Fatal("unknown start scope")
			}
			out, err := s.ListGroups(ctx, &pbs.ListGroupsRequest{
				Recursive: true,
				ScopeId:   startScope,
			})
			if tc.wantErr != nil {
				require.Error(err)
				require.Nil(out)
				require.Equal(tc.wantErr.Error(), err.Error())
			} else {
				require.NoError(err)
				require.NotNil(out)
				require.Len(out.Items, tc.expCount)
			}

			sessOut, err := sess.ListSessions(ctx, &pbs.ListSessionsRequest{
				Recursive: true,
				ScopeId:   startScope,
			})
			require.NoError(err)
			require.NotNil(sessOut)
		})
	}
}
