// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package restapi

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"reflect"
	"testing"

	"github.com/minio/console/models"
	iampolicy "github.com/minio/pkg/iam/policy"
	"github.com/stretchr/testify/assert"
)

// assigning mock at runtime instead of compile time
var minioListPoliciesMock func() (map[string]*iampolicy.Policy, error)

var (
	minioGetPolicyMock    func(name string) (*iampolicy.Policy, error)
	minioRemovePolicyMock func(name string) error
	minioAddPolicyMock    func(name string, policy *iampolicy.Policy) error
	minioSetPolicyMock    func(policyName, entityName string, isGroup bool) error
)

// mock function of listPolicies()
func (ac adminClientMock) listPolicies(ctx context.Context) (map[string]*iampolicy.Policy, error) {
	return minioListPoliciesMock()
}

// mock function of getPolicy()
func (ac adminClientMock) getPolicy(ctx context.Context, name string) (*iampolicy.Policy, error) {
	return minioGetPolicyMock(name)
}

// mock function of removePolicy()
func (ac adminClientMock) removePolicy(ctx context.Context, name string) error {
	return minioRemovePolicyMock(name)
}

// mock function of addPolicy()
func (ac adminClientMock) addPolicy(ctx context.Context, name string, policy *iampolicy.Policy) error {
	return minioAddPolicyMock(name, policy)
}

// mock function SetPolicy()
func (ac adminClientMock) setPolicy(ctx context.Context, policyName, entityName string, isGroup bool) error {
	return minioSetPolicyMock(policyName, entityName, isGroup)
}

func TestListPolicies(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	funcAssert := assert.New(t)
	adminClient := adminClientMock{}
	// mock function response from listPolicies()
	minioListPoliciesMock = func() (map[string]*iampolicy.Policy, error) {
		var readonly iampolicy.Policy
		var readwrite iampolicy.Policy
		var diagnostis iampolicy.Policy

		for _, p := range iampolicy.DefaultPolicies {
			switch p.Name {
			case "readonly":
				readonly = p.Definition
			case "readwrite":
				readwrite = p.Definition
			case "diagnostics":
				diagnostis = p.Definition
			}
		}

		return map[string]*iampolicy.Policy{
			"readonly":    &readonly,
			"readwrite":   &readwrite,
			"diagnostics": &diagnostis,
		}, nil
	}
	// Test-1 : listPolicies() Get response from minio client with three Canned Policies and return the same number on listPolicies()
	function := "listPolicies()"
	policiesList, err := listPolicies(ctx, adminClient)
	if err != nil {
		t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
	}
	// verify length of Policies is correct
	funcAssert.Equal(3, len(policiesList), fmt.Sprintf("Failed on %s: length of Policies's lists is not the same", function))
	// Test-2 : listPolicies() Return error and see that the error is handled correctly and returned
	minioListPoliciesMock = func() (map[string]*iampolicy.Policy, error) {
		return nil, errors.New("error")
	}
	_, err = listPolicies(ctx, adminClient)
	if funcAssert.Error(err) {
		funcAssert.Equal("error", err.Error())
	}
}

func TestRemovePolicy(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	funcAssert := assert.New(t)
	adminClient := adminClientMock{}
	// Test-1 : removePolicy() remove an existing policy
	policyToRemove := "console-policy"
	minioRemovePolicyMock = func(name string) error {
		return nil
	}
	function := "removePolicy()"
	if err := removePolicy(ctx, adminClient, policyToRemove); err != nil {
		t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
	}
	// Test-2 : removePolicy() Return error and see that the error is handled correctly and returned
	minioRemovePolicyMock = func(name string) error {
		return errors.New("error")
	}
	if err := removePolicy(ctx, adminClient, policyToRemove); funcAssert.Error(err) {
		funcAssert.Equal("error", err.Error())
	}
}

func TestAddPolicy(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	funcAssert := assert.New(t)
	adminClient := adminClientMock{}
	policyName := "new-policy"
	policyDefinition := "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"s3:GetBucketLocation\",\"s3:GetObject\",\"s3:ListAllMyBuckets\"],\"Resource\":[\"arn:aws:s3:::*\"]}]}"
	minioAddPolicyMock = func(name string, policy *iampolicy.Policy) error {
		return nil
	}
	minioGetPolicyMock = func(name string) (*iampolicy.Policy, error) {
		policy := "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"s3:GetBucketLocation\",\"s3:GetObject\",\"s3:ListAllMyBuckets\"],\"Resource\":[\"arn:aws:s3:::*\"]}]}"
		iamp, err := iampolicy.ParseConfig(bytes.NewReader([]byte(policy)))
		if err != nil {
			return nil, err
		}
		return iamp, nil
	}
	assertPolicy := models.Policy{
		Name:   "new-policy",
		Policy: "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"s3:GetBucketLocation\",\"s3:GetObject\",\"s3:ListAllMyBuckets\"],\"Resource\":[\"arn:aws:s3:::*\"]}]}",
	}
	// Test-1 : addPolicy() adds a new policy
	function := "addPolicy()"
	policy, err := addPolicy(ctx, adminClient, policyName, policyDefinition)
	if err != nil {
		t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
	} else {
		funcAssert.Equal(policy.Name, assertPolicy.Name)

		var expectedPolicy iampolicy.Policy
		var actualPolicy iampolicy.Policy
		err1 := json.Unmarshal([]byte(policy.Policy), &expectedPolicy)
		funcAssert.NoError(err1)
		err2 := json.Unmarshal([]byte(assertPolicy.Policy), &actualPolicy)
		funcAssert.NoError(err2)
		funcAssert.Equal(expectedPolicy, actualPolicy)
	}
	// Test-2 : addPolicy() got an error while adding policy
	minioAddPolicyMock = func(name string, policy *iampolicy.Policy) error {
		return errors.New("error")
	}
	if _, err := addPolicy(ctx, adminClient, policyName, policyDefinition); funcAssert.Error(err) {
		funcAssert.Equal("error", err.Error())
	}
	// Test-3 : addPolicy() got an error while retrieving policy
	minioAddPolicyMock = func(name string, policy *iampolicy.Policy) error {
		return nil
	}
	minioGetPolicyMock = func(name string) (*iampolicy.Policy, error) {
		return nil, errors.New("error")
	}
	if _, err := addPolicy(ctx, adminClient, policyName, policyDefinition); funcAssert.Error(err) {
		funcAssert.Equal("error", err.Error())
	}
}

func TestSetPolicy(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	funcAssert := assert.New(t)
	adminClient := adminClientMock{}
	policyName := "readOnly"
	entityName := "alevsk"
	entityObject := models.PolicyEntityUser
	minioSetPolicyMock = func(policyName, entityName string, isGroup bool) error {
		return nil
	}
	// Test-1 : SetPolicy() set policy to user
	function := "SetPolicy()"
	err := SetPolicy(ctx, adminClient, policyName, entityName, entityObject)
	if err != nil {
		t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
	}
	// Test-2 : SetPolicy() set policy to group
	entityObject = models.PolicyEntityGroup
	err = SetPolicy(ctx, adminClient, policyName, entityName, entityObject)
	if err != nil {
		t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
	}
	// Test-3 : SetPolicy() set policy to user and get error
	entityObject = models.PolicyEntityUser
	minioSetPolicyMock = func(policyName, entityName string, isGroup bool) error {
		return errors.New("error")
	}
	if err := SetPolicy(ctx, adminClient, policyName, entityName, entityObject); funcAssert.Error(err) {
		funcAssert.Equal("error", err.Error())
	}
	// Test-4 : SetPolicy() set policy to group and get error
	entityObject = models.PolicyEntityGroup
	minioSetPolicyMock = func(policyName, entityName string, isGroup bool) error {
		return errors.New("error")
	}
	if err := SetPolicy(ctx, adminClient, policyName, entityName, entityObject); funcAssert.Error(err) {
		funcAssert.Equal("error", err.Error())
	}
}

func Test_SetPolicyMultiple(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	adminClient := adminClientMock{}

	type args struct {
		policyName    string
		users         []models.IamEntity
		groups        []models.IamEntity
		setPolicyFunc func(policyName, entityName string, isGroup bool) error
	}
	tests := []struct {
		name          string
		args          args
		errorExpected error
	}{
		{
			name: "Set policy to multiple users and groups",
			args: args{
				policyName: "readonly",
				users:      []models.IamEntity{"user1", "user2"},
				groups:     []models.IamEntity{"group1", "group2"},
				setPolicyFunc: func(policyName, entityName string, isGroup bool) error {
					return nil
				},
			},
			errorExpected: nil,
		},
		{
			name: "Return error on set policy function",
			args: args{
				policyName: "readonly",
				users:      []models.IamEntity{"user1", "user2"},
				groups:     []models.IamEntity{"group1", "group2"},
				setPolicyFunc: func(policyName, entityName string, isGroup bool) error {
					return errors.New("error set")
				},
			},
			errorExpected: errors.New("error set"),
		},
		{
			// Description: Empty lists of users and groups are acceptable
			name: "Empty lists of users and groups",
			args: args{
				policyName: "readonly",
				users:      []models.IamEntity{},
				groups:     []models.IamEntity{},
				setPolicyFunc: func(policyName, entityName string, isGroup bool) error {
					return nil
				},
			},
			errorExpected: nil,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			minioSetPolicyMock = tt.args.setPolicyFunc
			got := setPolicyMultipleEntities(ctx, adminClient, tt.args.policyName, tt.args.users, tt.args.groups)
			if !reflect.DeepEqual(got, tt.errorExpected) {
				ji, _ := json.Marshal(got)
				vi, _ := json.Marshal(tt.errorExpected)
				t.Errorf("got %s want %s", ji, vi)
			}
		})
	}
}

func Test_policyMatchesBucket(t *testing.T) {
	type args struct {
		ctx    context.Context
		policy *models.Policy
		bucket string
	}
	tests := []struct {
		name string
		args args
		want bool
	}{
		{
			name: "Test1",
			args: args{ctx: context.Background(), policy: &models.Policy{Name: "consoleAdmin", Policy: `{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "admin:*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:*"
            ],
            "Resource": [
                "arn:aws:s3:::*"
            ]
        }
    ]
}`}, bucket: "test1"},
			want: true,
		},
		{
			name: "Test2",
			args: args{ctx: context.Background(), policy: &models.Policy{Name: "consoleAdmin", Policy: `{
				"Version": "2012-10-17",
				"Statement": [
			{
				"Effect": "Allow",
				"Action": [
				"s3:*"
			],
				"Resource": [
				"arn:aws:s3:::bucket1"
			]
			}
			]
			}`}, bucket: "test1"},
			want: false,
		},
		{
			name: "Test3",
			args: args{ctx: context.Background(), policy: &models.Policy{Name: "consoleAdmin", Policy: `{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "s3:ListStorageLensConfigurations",
                "s3:GetAccessPoint",
                "s3:PutAccountPublicAccessBlock",
                "s3:GetAccountPublicAccessBlock",
                "s3:ListAllMyBuckets",
                "s3:ListAccessPoints",
                "s3:ListJobs",
                "s3:PutStorageLensConfiguration",
                "s3:CreateJob"
            ],
            "Resource": "*"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::test",
                "arn:aws:s3:::test/*",
                "arn:aws:s3:::lkasdkljasd090901",
                "arn:aws:s3:::lkasdkljasd090901/*"
                ]
        }
    ]
	}`}, bucket: "test1"},
			want: false,
		},
		{
			name: "Test4",
			args: args{ctx: context.Background(), policy: &models.Policy{Name: "consoleAdmin", Policy: `{
				"Version": "2012-10-17",
				"Statement": [
			{
				"Effect": "Allow",
				"Action": [
				"s3:*"
			],
				"Resource": [
				"arn:aws:s3:::bucket1"
			]
			}
			]
			}`}, bucket: "bucket1"},
			want: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := policyMatchesBucket(tt.args.ctx, tt.args.policy, tt.args.bucket); got != tt.want {
				t.Errorf("policyMatchesBucket() = %v, want %v", got, tt.want)
			}
		})
	}
}
