// Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may not
// use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
// either express or implied. See the License for the specific language governing
// permissions and limitations under the License.

// Package processor contains the methods for update ssm agent.
// It also provides methods for sendReply and updateInstanceInfo
package processor

import (
	"fmt"
	"testing"

	"github.com/aws/amazon-ssm-agent/agent/appconfig"
	"github.com/aws/amazon-ssm-agent/agent/context"
	"github.com/aws/amazon-ssm-agent/agent/contracts"
	"github.com/aws/amazon-ssm-agent/agent/health"
	"github.com/aws/amazon-ssm-agent/agent/log"
	contextmocks "github.com/aws/amazon-ssm-agent/agent/mocks/context"
	logmocks "github.com/aws/amazon-ssm-agent/agent/mocks/log"
	"github.com/aws/amazon-ssm-agent/agent/ssm"
	ssm2 "github.com/aws/amazon-ssm-agent/agent/ssm/mocks/ssm"
	"github.com/aws/amazon-ssm-agent/agent/updateutil/updateconstants"
	"github.com/aws/amazon-ssm-agent/common/identity"
	identityMock "github.com/aws/amazon-ssm-agent/common/identity/mocks"
	ssmService "github.com/aws/aws-sdk-go/service/ssm"
	"github.com/stretchr/testify/assert"
)

type healthCheckTestCase struct {
	InputState   UpdateState
	IsSelfUpdate bool
	InputResult  contracts.ResultStatus
	ErrorCode    string
	Output       string
}

func TestHealthCheck(t *testing.T) {
	// generate test cases
	testCases := []healthCheckTestCase{
		{NotStarted, false, contracts.ResultStatusNotStarted, "", active},
		{Initialized, false, contracts.ResultStatusInProgress, "", updateInitialized},
		{Staged, false, contracts.ResultStatusInProgress, "", updateStaged},
		{Installed, false, contracts.ResultStatusInProgress, "", updateInProgress},
		{Completed, false, contracts.ResultStatusSuccess, "", updateSucceeded},
		{Completed, true, contracts.ResultStatusSuccess, "", updateSucceeded + "_SelfUpdate"},
		{Completed, false, contracts.ResultStatusFailed, "", updateFailed},
		{Rollback, false, contracts.ResultStatusNotStarted, "", rollingBack},
		{RolledBack, false, contracts.ResultStatusNotStarted, "", rollBackCompleted},
		{TestExecution, false, contracts.ResultStatusTestFailure, "SomeTestName", fmt.Sprintf("%v_%v", testFailed, "SomeTestName")},
		{UpdaterMetric, false, contracts.ResultStatus("some result"), "SomeMetric", fmt.Sprintf("%v_%v", updaterMetric, "SomeMetric")},
	}

	updateDetail := createUpdateDetail(Installed)

	// run tests
	for _, tst := range testCases {
		updateDetail.State = tst.InputState
		updateDetail.Result = tst.InputResult
		updateDetail.SelfUpdate = tst.IsSelfUpdate

		// call method
		result := PrepareHealthStatus(updateDetail, tst.ErrorCode, "")

		// check results
		assert.Equal(t, tst.Output, result, "Output was %s but expected to be %s", result, tst.Output)
	}
}

func TestUpdateHealthStatusWithNonAlarmingErrorCodes(t *testing.T) {
	// generate test cases
	testCases := map[updateconstants.ErrorCode]string{
		updateconstants.ErrorUnsupportedServiceManager: fmt.Sprintf("%v_%v-%v", updateFailed, updateconstants.ErrorUnsupportedServiceManager, noAlarm),
		updateconstants.ErrorEnvironmentIssue:          fmt.Sprintf("%v_%v", updateFailed, updateconstants.ErrorEnvironmentIssue),
	}

	dummyTargetVersion := "dummyTargetVersion"
	testCasesWithTargetVersion := map[updateconstants.ErrorCode]string{
		updateconstants.ErrorUnsupportedServiceManager: fmt.Sprintf("%v_%v-%v-%v", updateFailed, updateconstants.ErrorUnsupportedServiceManager, dummyTargetVersion, noAlarm),
		updateconstants.ErrorEnvironmentIssue:          fmt.Sprintf("%v_%v-%v", updateFailed, updateconstants.ErrorEnvironmentIssue, dummyTargetVersion),
	}

	updateDetail := createUpdateDetail(Installed)

	// run tests
	for errorCode, output := range testCases {
		updateDetail.Result = contracts.ResultStatusFailed
		updateDetail.State = Completed
		result := PrepareHealthStatus(updateDetail, string(errorCode), "")
		assert.Equal(t, output, result)
	}

	// run tests
	for errorCode, output := range testCasesWithTargetVersion {
		updateDetail.Result = contracts.ResultStatusFailed
		updateDetail.State = Completed
		result := PrepareHealthStatus(updateDetail, string(errorCode), dummyTargetVersion)
		assert.Equal(t, output, result)
	}
}

func TestHealthCheckWithUpdateFailed(t *testing.T) {
	// generate test cases
	testCases := []healthCheckTestCase{
		{NotStarted, false, contracts.ResultStatusFailed, "ErrorNotStarted", fmt.Sprintf("%v_%v", updateFailed, "ErrorNotStarted")},
		{Initialized, false, contracts.ResultStatusFailed, "ErrorInitialized", fmt.Sprintf("%v_%v", updateFailed, "ErrorInitialized")},
		{Staged, false, contracts.ResultStatusFailed, "ErrorStaged", fmt.Sprintf("%v_%v", updateFailed, "ErrorStaged")},
		{Installed, false, contracts.ResultStatusFailed, "ErrorInstalled", fmt.Sprintf("%v_%v", updateFailed, "ErrorInstalled")},
		{Completed, false, contracts.ResultStatusFailed, "ErrorCompleted", fmt.Sprintf("%v_%v", updateFailed, "ErrorCompleted")},
		{Rollback, false, contracts.ResultStatusFailed, "ErrorRollback", fmt.Sprintf("%v_%v", updateFailed, "ErrorRollback")},
		{RolledBack, false, contracts.ResultStatusFailed, "ErrorRolledBack", fmt.Sprintf("%v_%v", updateFailed, "ErrorRolledBack")},
		{RolledBack, true, contracts.ResultStatusFailed, "ErrorRolledBack", fmt.Sprintf("%v_%v_%v", updateFailed, "SelfUpdate", "ErrorRolledBack")},
	}

	updateDetail := createUpdateDetail(Installed)

	for _, tst := range testCases {
		updateDetail.Result = tst.InputResult
		updateDetail.State = Completed
		updateDetail.SelfUpdate = tst.IsSelfUpdate

		// call method
		result := PrepareHealthStatus(updateDetail, tst.ErrorCode, "")

		// check results
		assert.Equal(t, tst.Output, result)
	}
}

func TestUpdateHealthCheck(t *testing.T) {
	var logger = logmocks.NewMockLog()
	updateDetail := createUpdateDetail(Installed)

	// Initialize the appconfigMock with HealthFrequencyMinutes as every five minute
	appconfigMock := &appconfig.SsmagentConfig{
		Ssm: appconfig.SsmCfg{
			HealthFrequencyMinutes: appconfig.DefaultSsmHealthFrequencyMinutes,
		},
	}
	mockContext := contextmocks.NewMockDefault()
	mockContext.On("AppConfig").Return(&appconfigMock)

	service := &svcManager{context: mockContext}

	mockEC2Identity := &identityMock.IAgentIdentityInner{}
	newEC2Identity = func(log log.T) identity.IAgentIdentityInner {
		return mockEC2Identity
	}

	availabilityZone := "us-east-1a"
	availabilityZoneId := "use1-az2"
	mockEC2Identity.On("IsIdentityEnvironment").Return(true)
	mockEC2Identity.On("AvailabilityZone").Return(availabilityZone, nil)
	mockEC2Identity.On("AvailabilityZoneId").Return(availabilityZoneId, nil)

	mockECSIdentity := &identityMock.IAgentIdentityInner{}
	newECSIdentity = func(log log.T) identity.IAgentIdentityInner {
		return mockECSIdentity
	}
	mockECSIdentity.On("IsIdentityEnvironment").Return(false)

	mockOnPremIdentity := &identityMock.IAgentIdentityInner{}
	newOnPremIdentity = func(log log.T, config *appconfig.SsmagentConfig) identity.IAgentIdentityInner {
		return mockOnPremIdentity
	}
	mockOnPremIdentity.On("IsIdentityEnvironment").Return(false)

	ssmConnectionChannel := ""

	mockObj := ssm2.NewMockDefault()
	mockObj.On(
		"UpdateInstanceInformation",
		logger,
		updateDetail.SourceVersion,
		fmt.Sprintf("%v-%v", updateInProgress, updateDetail.TargetVersion),
		health.AgentName,
		availabilityZone,
		availabilityZoneId,
		ssmConnectionChannel).Return(&ssmService.UpdateInstanceInformationOutput{}, nil)

	// setup
	newSsmSvc = func(context context.T) ssm.Service {
		return mockObj
	}

	// action
	err := service.UpdateHealthCheck(logger, updateDetail, "")

	// assert
	mockObj.AssertExpectations(t)
	assert.NoError(t, err)
}

func TestUpdateHealthCheckFailCreatingService(t *testing.T) {
	// setup
	// fail to create a new ssm service
	ssmSvc = nil
	updateDetail := createUpdateDetail(Installed)
	service := &svcManager{}
	// action
	var logger = logmocks.NewMockLog()
	err := service.UpdateHealthCheck(logger, updateDetail, "")

	// assert
	assert.Error(t, err)
}

func createUpdateDetail(state UpdateState) *UpdateDetail {
	updateDetail := &UpdateDetail{}
	updateDetail.Result = contracts.ResultStatusInProgress
	updateDetail.SourceVersion = "5.0.0.0"
	updateDetail.TargetVersion = "6.0.0.0"
	updateDetail.State = state
	updateDetail.UpdateRoot = "testdata"
	updateDetail.MessageID = "message id"

	return updateDetail
}
