// This file is part of MinIO Console Server
// Copyright (c) 2020 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"
	"fmt"
	"testing"

	"errors"

	"github.com/minio/console/models"
	iampolicy "github.com/minio/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)
var minioRemovePolicyMock func(name string) error
var minioAddPolicyMock func(name string, policy *iampolicy.Policy) error
var 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 := context.Background()
	funcAssert := assert.New(t)
	adminClient := adminClientMock{}
	// mock function response from listPolicies()
	minioListPoliciesMock = func() (map[string]*iampolicy.Policy, error) {
		return map[string]*iampolicy.Policy{
			"readonly":    &iampolicy.ReadOnly,
			"readwrite":   &iampolicy.ReadWrite,
			"diagnostics": &iampolicy.AdminDiagnostics,
		}, 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 := context.Background()
	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 := context.Background()
	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 := context.Background()
	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())
	}
}
