// 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/
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 configurepackage

import (
	"testing"

	"github.com/aws/amazon-ssm-agent/agent/context"
	contextmocks "github.com/aws/amazon-ssm-agent/agent/mocks/context"
	"github.com/aws/amazon-ssm-agent/agent/mocks/log"
	"github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/localpackages"
	repository_mock "github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/localpackages/mock"
	"github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/trace"
	"github.com/stretchr/testify/mock"
)

var contextMock context.T = contextmocks.NewMockDefault()

func TestInstallNew(t *testing.T) {
	installerMock := installerSuccessMock("SsmTest", "0.0.1")
	repoMock := &repository_mock.MockedRepository{}
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Installing).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Installed).Return(nil)
	tracer := trace.NewTracer(log.NewMockLog())
	tracer.BeginSection("test segment root")
	output := &trace.PluginOutputTrace{Tracer: tracer}

	executeConfigurePackage(tracer, contextMock, repoMock, installerMock, nil, false, localpackages.New, output)

	installerMock.AssertExpectations(t)
	repoMock.AssertExpectations(t)
}

func TestUpgrade(t *testing.T) {
	uninstallerMock := uninstallerSuccessMock("SsmTest", "0.0.1")
	installerMock := installerSuccessMock("SsmTest", "0.0.2")
	repoMock := &repository_mock.MockedRepository{}
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Upgrading).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.2", localpackages.Installing).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.2", localpackages.Installed).Return(nil)
	repoMock.On("RemovePackage", mock.Anything, "SsmTest", "0.0.1").Return(nil)
	tracer := trace.NewTracer(log.NewMockLog())
	tracer.BeginSection("test segment root")
	output := &trace.PluginOutputTrace{Tracer: tracer}

	executeConfigurePackage(tracer, contextMock, repoMock, installerMock, uninstallerMock, false, localpackages.Installed, output)

	installerMock.AssertExpectations(t)
	uninstallerMock.AssertExpectations(t)
	repoMock.AssertExpectations(t)
}

func TestUpgradeFailedUninstall(t *testing.T) {
	uninstallerMock := uninstallerFailedMock("SsmTest", "0.0.1")
	installerMock := installerSuccessMock("SsmTest", "0.0.2")
	repoMock := &repository_mock.MockedRepository{}
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Upgrading).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.2", localpackages.Installing).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.2", localpackages.Installed).Return(nil)
	repoMock.On("RemovePackage", mock.Anything, "SsmTest", "0.0.1").Return(nil)
	tracer := trace.NewTracer(log.NewMockLog())
	tracer.BeginSection("test segment root")
	output := &trace.PluginOutputTrace{Tracer: tracer}

	executeConfigurePackage(tracer, contextMock, repoMock, installerMock, uninstallerMock, false, localpackages.Installed, output)

	installerMock.AssertExpectations(t)
	uninstallerMock.AssertExpectations(t)
	repoMock.AssertExpectations(t)
}

func TestUninstall(t *testing.T) {
	uninstallerMock := uninstallerSuccessMock("SsmTest", "0.0.1")
	repoMock := &repository_mock.MockedRepository{}
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Uninstalling).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.None).Return(nil)
	repoMock.On("RemovePackage", mock.Anything, "SsmTest", "0.0.1").Return(nil)
	tracer := trace.NewTracer(log.NewMockLog())
	tracer.BeginSection("test segment root")
	output := &trace.PluginOutputTrace{Tracer: tracer}

	executeConfigurePackage(tracer, contextMock, repoMock, nil, uninstallerMock, false, localpackages.Installed, output)

	uninstallerMock.AssertExpectations(t)
	repoMock.AssertExpectations(t)
}

func TestInstall_FailedInstall(t *testing.T) {
	installerMock := installerFailedMock("SsmTest", "0.0.1")
	repoMock := &repository_mock.MockedRepository{}
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Installing).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Failed).Return(nil)
	tracer := trace.NewTracer(log.NewMockLog())
	tracer.BeginSection("test segment root")
	output := &trace.PluginOutputTrace{Tracer: tracer}

	executeConfigurePackage(tracer, contextMock, repoMock, installerMock, nil, false, localpackages.New, output)

	installerMock.AssertExpectations(t)
	repoMock.AssertExpectations(t)
}

func TestInstall_FailedValidate(t *testing.T) {
	installerMock := installerInvalidMock("SsmTest", "0.0.1")
	repoMock := &repository_mock.MockedRepository{}
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Installing).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Failed).Return(nil)
	tracer := trace.NewTracer(log.NewMockLog())
	tracer.BeginSection("test segment root")
	output := &trace.PluginOutputTrace{Tracer: tracer}

	executeConfigurePackage(tracer, contextMock, repoMock, installerMock, nil, false, localpackages.New, output)

	installerMock.AssertExpectations(t)
	repoMock.AssertExpectations(t)
}

func TestUninstall_Failed(t *testing.T) {
	uninstallerMock := uninstallerFailedMock("SsmTest", "0.0.1")
	repoMock := &repository_mock.MockedRepository{}
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Uninstalling).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Failed).Return(nil)
	tracer := trace.NewTracer(log.NewMockLog())
	tracer.BeginSection("test segment root")
	output := &trace.PluginOutputTrace{Tracer: tracer}

	executeConfigurePackage(tracer, contextMock, repoMock, nil, uninstallerMock, false, localpackages.Installed, output)

	uninstallerMock.AssertExpectations(t)
	repoMock.AssertExpectations(t)
}

func TestRollback(t *testing.T) {
	uninstallerMock := uninstallerSuccessWithRollbackMock("SsmTest", "0.0.1")
	installerMock := installerFailedWithRollbackMock("SsmTest", "0.0.2")
	repoMock := &repository_mock.MockedRepository{}
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Upgrading).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.2", localpackages.Installing).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.2", localpackages.RollbackUninstall).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.RollbackInstall).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Installed).Return(nil)
	repoMock.On("RemovePackage", mock.Anything, "SsmTest", "0.0.2").Return(nil)
	tracer := trace.NewTracer(log.NewMockLog())
	tracer.BeginSection("test segment root")
	output := &trace.PluginOutputTrace{Tracer: tracer}

	executeConfigurePackage(tracer, contextMock, repoMock, installerMock, uninstallerMock, false, localpackages.Installed, output)

	installerMock.AssertExpectations(t)
	uninstallerMock.AssertExpectations(t)
	repoMock.AssertExpectations(t)
}

func TestRollbackFailed(t *testing.T) {
	uninstallerMock := uninstallerSuccessWithFailedRollbackMock("SsmTest", "0.0.1")
	installerMock := installerFailedWithRollbackMock("SsmTest", "0.0.2")
	repoMock := &repository_mock.MockedRepository{}
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Upgrading).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.2", localpackages.Installing).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.2", localpackages.RollbackUninstall).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.RollbackInstall).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Failed).Return(nil)
	tracer := trace.NewTracer(log.NewMockLog())
	tracer.BeginSection("test segment root")
	output := &trace.PluginOutputTrace{Tracer: tracer}

	executeConfigurePackage(tracer, contextMock, repoMock, installerMock, uninstallerMock, false, localpackages.Installed, output)

	installerMock.AssertExpectations(t)
	uninstallerMock.AssertExpectations(t)
	repoMock.AssertExpectations(t)
}

func TestUninstallReboot(t *testing.T) {
	uninstallerMock := uninstallerRebootMock("SsmTest", "0.0.1")
	repoMock := &repository_mock.MockedRepository{}
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Uninstalling).Return(nil)
	tracer := trace.NewTracer(log.NewMockLog())
	tracer.BeginSection("test segment root")
	output := &trace.PluginOutputTrace{Tracer: tracer}

	executeConfigurePackage(tracer, contextMock, repoMock, nil, uninstallerMock, false, localpackages.Installed, output)

	uninstallerMock.AssertExpectations(t)
	repoMock.AssertExpectations(t)
}

func TestUninstallAfterReboot(t *testing.T) {
	uninstallerMock := uninstallerSuccessMock("SsmTest", "0.0.1")
	repoMock := &repository_mock.MockedRepository{}
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Uninstalling).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.None).Return(nil)
	repoMock.On("RemovePackage", mock.Anything, "SsmTest", "0.0.1").Return(nil)
	tracer := trace.NewTracer(log.NewMockLog())
	tracer.BeginSection("test segment root")
	output := &trace.PluginOutputTrace{Tracer: tracer}

	executeConfigurePackage(tracer, contextMock, repoMock, nil, uninstallerMock, false, localpackages.Uninstalling, output)

	uninstallerMock.AssertExpectations(t)
	repoMock.AssertExpectations(t)
}

func TestInstallReboot(t *testing.T) {
	installerMock := installerRebootMock("SsmTest", "0.0.1")
	repoMock := &repository_mock.MockedRepository{}
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Installing).Return(nil)
	tracer := trace.NewTracer(log.NewMockLog())
	tracer.BeginSection("test segment root")
	output := &trace.PluginOutputTrace{Tracer: tracer}

	executeConfigurePackage(tracer, contextMock, repoMock, installerMock, nil, false, localpackages.New, output)

	installerMock.AssertExpectations(t)
	repoMock.AssertExpectations(t)
}

func TestInstallAfterReboot(t *testing.T) {
	installerMock := installerSuccessMock("SsmTest", "0.0.1")
	repoMock := &repository_mock.MockedRepository{}
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Installing).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Installed).Return(nil)
	tracer := trace.NewTracer(log.NewMockLog())
	tracer.BeginSection("test segment root")
	output := &trace.PluginOutputTrace{Tracer: tracer}

	executeConfigurePackage(tracer, contextMock, repoMock, installerMock, nil, false, localpackages.Installing, output)

	installerMock.AssertExpectations(t)
	repoMock.AssertExpectations(t)
}

func TestUpgradeAfterUninstallReboot(t *testing.T) {
	uninstallerMock := uninstallerSuccessMock("SsmTest", "0.0.1")
	installerMock := installerSuccessMock("SsmTest", "0.0.2")
	repoMock := &repository_mock.MockedRepository{}
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Upgrading).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.2", localpackages.Installing).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.2", localpackages.Installed).Return(nil)
	repoMock.On("RemovePackage", mock.Anything, "SsmTest", "0.0.1").Return(nil)
	tracer := trace.NewTracer(log.NewMockLog())
	tracer.BeginSection("test segment root")
	output := &trace.PluginOutputTrace{Tracer: tracer}

	executeConfigurePackage(tracer, contextMock, repoMock, installerMock, uninstallerMock, false, localpackages.Uninstalling, output)

	installerMock.AssertExpectations(t)
	uninstallerMock.AssertExpectations(t)
	repoMock.AssertExpectations(t)
}

func TestUpgradeAfterInstallReboot(t *testing.T) {
	uninstallerMock := installerNameVersionOnlyMock("SsmTest", "0.0.1")
	installerMock := installerSuccessMock("SsmTest", "0.0.2")
	repoMock := &repository_mock.MockedRepository{}
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.2", localpackages.Installing).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.2", localpackages.Installed).Return(nil)
	repoMock.On("RemovePackage", mock.Anything, "SsmTest", "0.0.1").Return(nil)
	tracer := trace.NewTracer(log.NewMockLog())
	tracer.BeginSection("test segment root")
	output := &trace.PluginOutputTrace{Tracer: tracer}

	executeConfigurePackage(tracer, contextMock, repoMock, installerMock, uninstallerMock, false, localpackages.Installing, output)

	installerMock.AssertExpectations(t)
	uninstallerMock.AssertExpectations(t)
	repoMock.AssertExpectations(t)
}

func TestRollbackAfterUninstallReboot(t *testing.T) {
	uninstallerMock := installerSuccessMock("SsmTest", "0.0.1")
	installerMock := uninstallerSuccessMock("SsmTest", "0.0.2")
	repoMock := &repository_mock.MockedRepository{}
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.2", localpackages.RollbackUninstall).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.RollbackInstall).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Installed).Return(nil)
	repoMock.On("RemovePackage", mock.Anything, "SsmTest", "0.0.2").Return(nil)
	tracer := trace.NewTracer(log.NewMockLog())
	tracer.BeginSection("test segment root")
	output := &trace.PluginOutputTrace{Tracer: tracer}

	executeConfigurePackage(tracer, contextMock, repoMock, installerMock, uninstallerMock, false, localpackages.RollbackUninstall, output)

	installerMock.AssertExpectations(t)
	uninstallerMock.AssertExpectations(t)
	repoMock.AssertExpectations(t)
}

func TestRollbackAfterInstallReboot(t *testing.T) {
	uninstallerMock := installerSuccessMock("SsmTest", "0.0.1")
	installerMock := installerNameVersionOnlyMock("SsmTest", "0.0.2")
	repoMock := &repository_mock.MockedRepository{}
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.RollbackInstall).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Installed).Return(nil)
	repoMock.On("RemovePackage", mock.Anything, "SsmTest", "0.0.2").Return(nil)
	tracer := trace.NewTracer(log.NewMockLog())
	tracer.BeginSection("test segment root")
	output := &trace.PluginOutputTrace{Tracer: tracer}

	executeConfigurePackage(tracer, contextMock, repoMock, installerMock, uninstallerMock, false, localpackages.RollbackInstall, output)

	installerMock.AssertExpectations(t)
	uninstallerMock.AssertExpectations(t)
	repoMock.AssertExpectations(t)
}

func TestUpdate(t *testing.T) {
	installerMock := trueUpdateInstallerMock("SsmTest", "0.0.2")
	uninstallerMock := installerNameVersionOnlyMock("SsmTest", "0.0.1")
	repoMock := &repository_mock.MockedRepository{}
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.2", localpackages.Updating).Return(nil)
	repoMock.On("RemovePackage", mock.Anything, "SsmTest", mock.Anything).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.2", localpackages.Installed).Return(nil)
	tracer := trace.NewTracer(log.NewMockLog())
	tracer.BeginSection("test segment root")
	output := &trace.PluginOutputTrace{Tracer: tracer}

	executeConfigurePackage(tracer, contextMock, repoMock, installerMock, uninstallerMock, true, localpackages.Updating, output)

	uninstallerMock.AssertExpectations(t)
	installerMock.AssertExpectations(t)
	repoMock.AssertExpectations(t)
}

func TestUpdate_UpdateFailed_RollbackSucceeds(t *testing.T) {
	installerMock := trueUpdateInstallerMockRollbackSucceeds("SsmTest", "0.0.2")
	uninstallerMock := installerSuccessMock("SsmTest", "0.0.1")
	repoMock := &repository_mock.MockedRepository{}
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.2", localpackages.Updating).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.2", localpackages.RollbackUninstall).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.RollbackInstall).Return(nil)
	repoMock.On("RemovePackage", mock.Anything, "SsmTest", mock.Anything).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Installed).Return(nil)
	tracer := trace.NewTracer(log.NewMockLog())
	tracer.BeginSection("test segment root")
	output := &trace.PluginOutputTrace{Tracer: tracer}

	executeConfigurePackage(tracer, contextMock, repoMock, installerMock, uninstallerMock, true, localpackages.Updating, output)

	uninstallerMock.AssertExpectations(t)
	installerMock.AssertExpectations(t)
	repoMock.AssertExpectations(t)
}

func TestUpdate_UpdateFailed_RollbackUninstallFailed_DirectlyReinstallSucceeds(t *testing.T) {
	installerMock := trueUpdateInstallerMockRollbackUninstallFails("SsmTest", "0.0.2")
	uninstallerMock := installerSuccessMock("SsmTest", "0.0.1")
	repoMock := &repository_mock.MockedRepository{}
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.2", localpackages.Updating).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.2", localpackages.RollbackUninstall).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.RollbackInstall).Return(nil)
	repoMock.On("RemovePackage", mock.Anything, "SsmTest", mock.Anything).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Installed).Return(nil)
	tracer := trace.NewTracer(log.NewMockLog())
	tracer.BeginSection("test segment root")
	output := &trace.PluginOutputTrace{Tracer: tracer}

	executeConfigurePackage(tracer, contextMock, repoMock, installerMock, uninstallerMock, true, localpackages.Updating, output)

	uninstallerMock.AssertExpectations(t)
	installerMock.AssertExpectations(t)
	repoMock.AssertExpectations(t)
}

func TestUpdate_UpdateFailed_RollbackInstallFailed_DirectlyReinstallFailed(t *testing.T) {
	installerMock := trueUpdateInstallerMockRollbackUninstallFails("SsmTest", "0.0.2")
	uninstallerMock := installerFailedMock("SsmTest", "0.0.1")
	repoMock := &repository_mock.MockedRepository{}
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.2", localpackages.Updating).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.2", localpackages.RollbackUninstall).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.RollbackInstall).Return(nil)
	repoMock.On("SetInstallState", mock.Anything, "SsmTest", "0.0.1", localpackages.Failed).Return(nil)
	tracer := trace.NewTracer(log.NewMockLog())
	tracer.BeginSection("test segment root")
	output := &trace.PluginOutputTrace{Tracer: tracer}

	executeConfigurePackage(tracer, contextMock, repoMock, installerMock, uninstallerMock, true, localpackages.Updating, output)

	uninstallerMock.AssertExpectations(t)
	installerMock.AssertExpectations(t)
	repoMock.AssertExpectations(t)
}
