package utils

import (
	"context"
	"fmt"
	"testing"

	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	"k8s.io/apiserver/pkg/authorization/authorizer"

	"github.com/grafana/authlib/authn"
	"github.com/grafana/grafana/pkg/apimachinery/identity"
)

type expect struct {
	decision authorizer.Decision
	reason   string
	err      string
}

type testCase struct {
	name       string
	user       identity.Requester
	attrs      authorizer.Attributes
	expect     expect
	breakpoint bool
}

func TestAuthorizer_Authorize(t *testing.T) {
	userABC := &identity.StaticRequester{
		UserUID: "abc",
		OrgRole: identity.RoleViewer,
		AccessTokenClaims: &authn.Claims[authn.AccessTokenClaims]{
			Rest: authn.AccessTokenClaims{
				DelegatedPermissions: []string{"group/stars:*", "group/preferences:*", "group/ns:*"},
			},
		},
	}

	tests := []struct {
		name     string
		teams    func(t *testing.T) TeamService
		resource map[string][]ResourceOwner
		check    []testCase
	}{
		{
			name: "stars",
			resource: map[string][]ResourceOwner{
				"stars": {UserResourceOwner},
			},
			check: []testCase{{
				name: "matches user",
				user: userABC,
				attrs: authorizer.AttributesRecord{
					Verb:            "get",
					APIGroup:        "group",
					Resource:        "stars",
					Name:            "user-abc", // note this matches in input user name
					ResourceRequest: true,
				},
				expect: expect{
					decision: authorizer.DecisionAllow,
				},
			}, {
				name: "different user",
				user: userABC,
				attrs: authorizer.AttributesRecord{
					Verb:            "get",
					APIGroup:        "group",
					Resource:        "stars",
					Name:            "user-xyz", // not abc
					ResourceRequest: true,
				},
				expect: expect{
					decision: authorizer.DecisionDeny,
					reason:   "your are not the owner of the resource",
				},
			}},
		}, {
			name: "fast path",
			resource: map[string][]ResourceOwner{
				"stars":       {UserResourceOwner},
				"preferences": {TeamResourceOwner},
			},
			check: []testCase{{
				name:  "missing user",
				attrs: authorizer.AttributesRecord{},
				expect: expect{
					decision: authorizer.DecisionDeny,
					err:      "a Requester was not found in the context",
				},
			}, {
				name: "not a resource",
				user: &identity.StaticRequester{},
				attrs: authorizer.AttributesRecord{
					ResourceRequest: false,
				},
				expect: expect{
					decision: authorizer.DecisionNoOpinion,
				},
			}, {
				name: "unknown resource",
				user: &identity.StaticRequester{},
				attrs: authorizer.AttributesRecord{
					Resource:        "xxxx",
					ResourceRequest: true,
				},
				expect: expect{
					decision: authorizer.DecisionDeny,
					reason:   "missing resource name",
				},
			}, {
				name: "missing service permissions",
				user: &identity.StaticRequester{
					UserUID: "abc",
					AccessTokenClaims: &authn.Claims[authn.AccessTokenClaims]{
						Rest: authn.AccessTokenClaims{
							DelegatedPermissions: []string{""},
						},
					},
				},
				attrs: authorizer.AttributesRecord{
					Resource:        "stars",
					ResourceRequest: true,
				},
				expect: expect{
					decision: authorizer.DecisionDeny,
					reason:   "calling service lacks required permissions",
				},
			}, {
				name: "wrong owner type",
				user: userABC,
				attrs: authorizer.AttributesRecord{
					APIGroup:        "group",
					Resource:        "stars",
					ResourceRequest: true,
					Verb:            "create",   // missing name
					Name:            "team-xxx", // not supported
				},
				expect: expect{
					decision: authorizer.DecisionDeny,
					reason:   "unsupported owner type",
				},
			}, {
				name: "unknown resource",
				user: userABC,
				attrs: authorizer.AttributesRecord{
					APIGroup:        "group",
					Resource:        "stars",
					ResourceRequest: true,
					Verb:            "create", // missing name
				},
				expect: expect{
					decision: authorizer.DecisionDeny,
					reason:   "mutating request without a name",
				},
			}, {
				name: "list request",
				user: userABC,
				attrs: authorizer.AttributesRecord{
					APIGroup:        "group",
					Resource:        "stars",
					ResourceRequest: true,
					Verb:            "list", // no name
				},
				expect: expect{
					decision: authorizer.DecisionAllow,
				},
			}, {
				name: "teams request (but not configured)",
				user: userABC,
				attrs: authorizer.AttributesRecord{
					APIGroup:        "group",
					Resource:        "preferences",
					ResourceRequest: true,
					Verb:            "get",
					Name:            "team-XYZ",
				},
				expect: expect{
					decision: authorizer.DecisionDeny,
					reason:   "team checker not configured",
				},
			}},
		}, {
			name: "unknown owner",
			resource: map[string][]ResourceOwner{
				"stars": {UnknownResourceOwner},
			},
			check: []testCase{{
				name: "get",
				user: userABC,
				attrs: authorizer.AttributesRecord{
					APIGroup:        "group",
					Resource:        "stars",
					Name:            "something-not-an-owner",
					ResourceRequest: true,
					Verb:            "get",
				},
				expect: expect{
					decision: authorizer.DecisionAllow,
				},
			}},
		}, {
			name: "namespace",
			resource: map[string][]ResourceOwner{
				"ns": {NamespaceResourceOwner},
			},
			check: []testCase{{
				name: "readonly",
				user: userABC,
				attrs: authorizer.AttributesRecord{
					APIGroup:        "group",
					Resource:        "ns",
					ResourceRequest: true,
					Verb:            "get",
					Name:            "namespace",
				},
				expect: expect{
					decision: authorizer.DecisionAllow,
				},
			}, {
				name: "mutating",
				user: userABC,
				attrs: authorizer.AttributesRecord{
					APIGroup:        "group",
					Resource:        "ns",
					ResourceRequest: true,
					Verb:            "create",
					Name:            "namespace",
				},
				expect: expect{
					decision: authorizer.DecisionDeny,
					reason:   "must be an org admin to edit",
				},
			}, {
				name: "org admin",
				user: &identity.StaticRequester{
					UserUID: "abc",
					OrgRole: identity.RoleAdmin,
					AccessTokenClaims: &authn.Claims[authn.AccessTokenClaims]{
						Rest: authn.AccessTokenClaims{
							DelegatedPermissions: []string{"group/ns:create"},
						},
					},
				},
				attrs: authorizer.AttributesRecord{
					APIGroup:        "group",
					Resource:        "ns",
					ResourceRequest: true,
					Verb:            "create",
					Name:            "namespace",
				},
				expect: expect{
					decision: authorizer.DecisionAllow,
				},
			}},
		}, {
			name: "preferences teams",
			teams: func(t *testing.T) TeamService {
				teams := NewMockTeamService(t)
				teams.On("InTeam", mock.Anything, userABC, "xyz", false).Return(true, nil)
				teams.On("InTeam", mock.Anything, userABC, "456", false).Return(false, nil)
				teams.On("InTeam", mock.Anything, userABC, "XXX", false).Return(true, fmt.Errorf("error from team"))
				return teams
			},
			resource: map[string][]ResourceOwner{
				"preferences": {
					TeamResourceOwner,
				},
			},
			check: []testCase{{
				name: "user in team",
				user: userABC,
				attrs: authorizer.AttributesRecord{
					Verb:            "get",
					APIGroup:        "group",
					Resource:        "preferences",
					Name:            "team-xyz",
					ResourceRequest: true,
				},
				expect: expect{
					decision: authorizer.DecisionAllow,
				},
			}, {
				name: "user not in team",
				user: userABC,
				attrs: authorizer.AttributesRecord{
					Verb:            "get",
					APIGroup:        "group",
					Resource:        "preferences",
					Name:            "team-456",
					ResourceRequest: true,
				},
				expect: expect{
					decision: authorizer.DecisionDeny,
					reason:   "you are not a member of the referenced team",
				},
			}, {
				name: "team error",
				user: userABC,
				attrs: authorizer.AttributesRecord{
					Verb:            "get",
					APIGroup:        "group",
					Resource:        "preferences",
					Name:            "team-XXX",
					ResourceRequest: true,
				},
				expect: expect{
					decision: authorizer.DecisionDeny,
					reason:   "error fetching teams",
					err:      "error from team",
				},
			}},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			authz := &AuthorizeFromName{
				Resource: tt.resource,
			}
			if tt.teams != nil {
				authz.Teams = tt.teams(t)
			}
			for _, check := range tt.check {
				t.Run(check.name, func(t *testing.T) {
					ctx := context.Background()
					if check.user != nil {
						ctx = identity.WithRequester(ctx, check.user)
					}
					e := check.expect
					if check.breakpoint {
						require.True(t, true) // Can set breakpoint in IDE here
					}
					d, r, err := authz.Authorize(ctx, check.attrs)
					if e.err != "" {
						require.ErrorContains(t, err, e.err)
						return
					}
					require.NoError(t, err)
					require.Equal(t, e.decision, d)
					if e.reason != "" {
						require.Equal(t, e.reason, r)
					}
				})
			}
		})
	}
}
