// Copyright 2015 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 engine

import (
	"bytes"
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"testing"

	"github.com/aws/amazon-ecs-init/ecs-init/cache"
	"github.com/aws/amazon-ecs-init/ecs-init/gpu"
	"github.com/golang/mock/gomock"
)

// getDockerClientMock backs up getDockerClient package-level function and replaces it with the mock passed as
// parameter. The backup can be restored by executing the returned function in a deferred manner.
// (e.g. defer getDockerClientMock(mock)() )
func getDockerClientMock(mockDocker dockerClient) func() {
	getDockerClientBkp := getDockerClient
	getDockerClient = func() (dockerClient, error) {
		return mockDocker, nil
	}
	return func() {
		getDockerClient = getDockerClientBkp
	}
}

func TestPreStartImageAlreadyCachedAndLoaded(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	mockDocker := NewMockdockerClient(mockCtrl)
	defer getDockerClientMock(mockDocker)()
	mockDownloader := NewMockdownloader(mockCtrl)

	mockDocker.EXPECT().LoadEnvVars().Return(nil)
	// Docker reports image is loaded.
	mockDocker.EXPECT().IsAgentImageLoaded().Return(true, nil)
	// Agent tarball and state is present
	mockDownloader.EXPECT().AgentCacheStatus().Return(cache.StatusCached)

	mockLoopbackRouting := NewMockloopbackRouting(mockCtrl)
	mockLoopbackRouting.EXPECT().Enable().Return(nil)
	mockIpv6RouterAdvertisements := NewMockipv6RouterAdvertisements(mockCtrl)
	mockIpv6RouterAdvertisements.EXPECT().Disable().Return(nil)
	mockRoute := NewMockcredentialsProxyRoute(mockCtrl)
	mockRoute.EXPECT().Create().Return(nil)

	engine := &Engine{
		downloader:               mockDownloader,
		loopbackRouting:          mockLoopbackRouting,
		ipv6RouterAdvertisements: mockIpv6RouterAdvertisements,
		credentialsProxyRoute:    mockRoute,
	}
	err := engine.PreStart()
	if err != nil {
		t.Errorf("engine pre-start error: %v", err)
	}
}

func TestPreStartReloadNeeded(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	cachedAgentBuffer := ioutil.NopCloser(&bytes.Buffer{})

	mockDocker := NewMockdockerClient(mockCtrl)
	defer getDockerClientMock(mockDocker)()
	mockDownloader := NewMockdownloader(mockCtrl)

	mockDocker.EXPECT().LoadEnvVars().Return(nil)
	// Docker reports image is loaded.
	mockDocker.EXPECT().IsAgentImageLoaded().Return(true, nil)
	// Agent tarball and state is present, but requires a reload off of disk
	mockDownloader.EXPECT().AgentCacheStatus().Return(cache.StatusReloadNeeded)
	mockDownloader.EXPECT().LoadCachedAgent().Return(cachedAgentBuffer, nil)
	mockDocker.EXPECT().LoadImage(cachedAgentBuffer)
	mockDownloader.EXPECT().RecordCachedAgent()

	mockLoopbackRouting := NewMockloopbackRouting(mockCtrl)
	mockLoopbackRouting.EXPECT().Enable().Return(nil)
	mockIpv6RouterAdvertisements := NewMockipv6RouterAdvertisements(mockCtrl)
	mockIpv6RouterAdvertisements.EXPECT().Disable().Return(nil)
	mockRoute := NewMockcredentialsProxyRoute(mockCtrl)
	mockRoute.EXPECT().Create().Return(nil)

	engine := &Engine{
		downloader:               mockDownloader,
		loopbackRouting:          mockLoopbackRouting,
		ipv6RouterAdvertisements: mockIpv6RouterAdvertisements,
		credentialsProxyRoute:    mockRoute,
	}
	err := engine.PreStart()
	if err != nil {
		t.Errorf("engine pre-start error: %v", err)
	}
}

func TestPreStartImageNotLoadedCached(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	cachedAgentBuffer := ioutil.NopCloser(&bytes.Buffer{})

	mockDocker := NewMockdockerClient(mockCtrl)
	mockDownloader := NewMockdownloader(mockCtrl)
	defer getDockerClientMock(mockDocker)()
	mockLoopbackRouting := NewMockloopbackRouting(mockCtrl)
	mockRoute := NewMockcredentialsProxyRoute(mockCtrl)

	mockDocker.EXPECT().LoadEnvVars().Return(nil)
	mockRoute.EXPECT().Create().Return(nil)
	mockLoopbackRouting.EXPECT().Enable().Return(nil)
	mockIpv6RouterAdvertisements := NewMockipv6RouterAdvertisements(mockCtrl)
	mockIpv6RouterAdvertisements.EXPECT().Disable().Return(nil)
	mockDocker.EXPECT().IsAgentImageLoaded().Return(false, nil)
	mockDownloader.EXPECT().AgentCacheStatus().Return(cache.StatusCached)
	mockDownloader.EXPECT().LoadCachedAgent().Return(cachedAgentBuffer, nil)
	mockDocker.EXPECT().LoadImage(cachedAgentBuffer)
	mockDownloader.EXPECT().RecordCachedAgent()

	engine := &Engine{
		downloader:               mockDownloader,
		loopbackRouting:          mockLoopbackRouting,
		ipv6RouterAdvertisements: mockIpv6RouterAdvertisements,
		credentialsProxyRoute:    mockRoute,
	}
	err := engine.PreStart()
	if err != nil {
		t.Errorf("engine pre-start error: %v", err)
	}
}

func TestPreStartImageNotCached(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	cachedAgentBuffer := ioutil.NopCloser(&bytes.Buffer{})

	mockDocker := NewMockdockerClient(mockCtrl)
	defer getDockerClientMock(mockDocker)()
	mockDownloader := NewMockdownloader(mockCtrl)

	mockDocker.EXPECT().LoadEnvVars().Return(nil)
	mockDocker.EXPECT().IsAgentImageLoaded().Return(false, nil)
	mockDownloader.EXPECT().AgentCacheStatus().Return(cache.StatusUncached)
	mockDownloader.EXPECT().DownloadAgent()
	mockDownloader.EXPECT().LoadCachedAgent().Return(cachedAgentBuffer, nil)
	mockDocker.EXPECT().LoadImage(cachedAgentBuffer)
	mockDownloader.EXPECT().RecordCachedAgent()

	mockLoopbackRouting := NewMockloopbackRouting(mockCtrl)
	mockLoopbackRouting.EXPECT().Enable().Return(nil)
	mockIpv6RouterAdvertisements := NewMockipv6RouterAdvertisements(mockCtrl)
	mockIpv6RouterAdvertisements.EXPECT().Disable().Return(nil)
	mockRoute := NewMockcredentialsProxyRoute(mockCtrl)
	mockRoute.EXPECT().Create().Return(nil)

	engine := &Engine{
		downloader:               mockDownloader,
		loopbackRouting:          mockLoopbackRouting,
		ipv6RouterAdvertisements: mockIpv6RouterAdvertisements,
		credentialsProxyRoute:    mockRoute,
	}
	err := engine.PreStart()
	if err != nil {
		t.Errorf("engine pre-start error: %v", err)
	}
}

func TestPreStartGPUSetupSuccessful(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	mockDocker := NewMockdockerClient(mockCtrl)
	defer getDockerClientMock(mockDocker)()
	mockDownloader := NewMockdownloader(mockCtrl)
	mockGPUManager := gpu.NewMockGPUManager(mockCtrl)

	mockDocker.EXPECT().LoadEnvVars().Return(map[string]string{
		"ECS_ENABLE_GPU_SUPPORT": "true",
	})
	mockGPUManager.EXPECT().Setup().Return(nil)
	// Docker reports image is loaded.
	mockDocker.EXPECT().IsAgentImageLoaded().Return(true, nil)
	// Agent tarball and state is present
	mockDownloader.EXPECT().AgentCacheStatus().Return(cache.StatusCached)

	mockLoopbackRouting := NewMockloopbackRouting(mockCtrl)
	mockLoopbackRouting.EXPECT().Enable().Return(nil)
	mockIpv6RouterAdvertisements := NewMockipv6RouterAdvertisements(mockCtrl)
	mockIpv6RouterAdvertisements.EXPECT().Disable().Return(nil)
	mockRoute := NewMockcredentialsProxyRoute(mockCtrl)
	mockRoute.EXPECT().Create().Return(nil)

	engine := &Engine{
		downloader:               mockDownloader,
		loopbackRouting:          mockLoopbackRouting,
		ipv6RouterAdvertisements: mockIpv6RouterAdvertisements,
		credentialsProxyRoute:    mockRoute,
		nvidiaGPUManager:         mockGPUManager,
	}
	err := engine.PreStart()
	if err != nil {
		t.Errorf("engine pre-start error: %v", err)
	}
}

func TestPreStartGPUSetupError(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	mockDocker := NewMockdockerClient(mockCtrl)
	defer getDockerClientMock(mockDocker)()
	mockGPUManager := gpu.NewMockGPUManager(mockCtrl)

	mockDocker.EXPECT().LoadEnvVars().Return(map[string]string{
		"ECS_ENABLE_GPU_SUPPORT": "true",
	})
	mockGPUManager.EXPECT().Setup().Return(errors.New("gpu setup failed"))
	engine := &Engine{
		nvidiaGPUManager: mockGPUManager,
	}
	err := engine.PreStart()
	if err == nil {
		t.Error("Expected error to be returned but was nil")
	}
}

func TestStartSupervisedCannotStart(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	mockDocker := NewMockdockerClient(mockCtrl)
	defer getDockerClientMock(mockDocker)()
	mockDocker.EXPECT().RemoveExistingAgentContainer()
	mockDocker.EXPECT().StartAgent().Return(0, errors.New("test error"))

	engine := &Engine{}
	err := engine.StartSupervised()
	if err == nil {
		t.Error("Expected error to be returned but was nil")
	}
}

func TestStartSupervisedExitsWhenTerminalFailure(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	mockDocker := NewMockdockerClient(mockCtrl)
	defer getDockerClientMock(mockDocker)()

	gomock.InOrder(
		mockDocker.EXPECT().RemoveExistingAgentContainer(),
		mockDocker.EXPECT().StartAgent().Return(1, nil),
		mockDocker.EXPECT().RemoveExistingAgentContainer(),
		mockDocker.EXPECT().StartAgent().Return(1, nil),
		mockDocker.EXPECT().RemoveExistingAgentContainer(),
		mockDocker.EXPECT().StartAgent().Return(1, nil),
		mockDocker.EXPECT().RemoveExistingAgentContainer(),
		mockDocker.EXPECT().StartAgent().Return(TerminalFailureAgentExitCode, nil),
	)

	engine := &Engine{}
	err := engine.StartSupervised()

	if err == nil {
		t.Error("Expected error to be returned but was nil")
	}

	_, ok := err.(*TerminalError)
	if !ok {
		t.Error("Expected error to be of type TerminalError")
	}
}

func TestLogContainerFailureAgentExitCodeFailure(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	mockDocker := NewMockdockerClient(mockCtrl)
	defer getDockerClientMock(mockDocker)()
	mockDocker.EXPECT().RemoveExistingAgentContainer()
	mockDocker.EXPECT().StartAgent().Return(2, nil)
	mockDocker.EXPECT().GetContainerLogTail(gomock.Any())
	mockDocker.EXPECT().RemoveExistingAgentContainer()
	mockDocker.EXPECT().StartAgent().Return(0, errors.New("test error"))

	engine := &Engine{}
	err := engine.StartSupervised()
	if err == nil {
		t.Error("Expected error to be returned but was nil")
	}
}

func TestStartSupervisedExitsWhenTerminalSuccess(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	mockDocker := NewMockdockerClient(mockCtrl)
	defer getDockerClientMock(mockDocker)()
	gomock.InOrder(
		mockDocker.EXPECT().RemoveExistingAgentContainer(),
		mockDocker.EXPECT().StartAgent().Return(1, nil),
		mockDocker.EXPECT().RemoveExistingAgentContainer(),
		mockDocker.EXPECT().StartAgent().Return(1, nil),
		mockDocker.EXPECT().RemoveExistingAgentContainer(),
		mockDocker.EXPECT().StartAgent().Return(1, nil),
		mockDocker.EXPECT().RemoveExistingAgentContainer(),
		mockDocker.EXPECT().StartAgent().Return(terminalSuccessAgentExitCode, nil),
	)

	engine := &Engine{}
	err := engine.StartSupervised()
	if err != nil {
		t.Error("Expected error to be nil but was returned")
	}
}

func TestStartSupervisedUpgradeOpenFailure(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	mockDocker := NewMockdockerClient(mockCtrl)
	defer getDockerClientMock(mockDocker)()
	mockDownloader := NewMockdownloader(mockCtrl)

	gomock.InOrder(
		mockDocker.EXPECT().RemoveExistingAgentContainer(),
		mockDocker.EXPECT().StartAgent().Return(upgradeAgentExitCode, nil),
		mockDownloader.EXPECT().LoadDesiredAgent().Return(nil, errors.New("test error")),
		mockDocker.EXPECT().RemoveExistingAgentContainer(),
		mockDocker.EXPECT().StartAgent().Return(terminalSuccessAgentExitCode, nil),
	)

	engine := &Engine{
		downloader: mockDownloader,
	}
	err := engine.StartSupervised()
	if err != nil {
		t.Error("Expected error to be nil but was returned")
	}
}

func TestStartSupervisedUpgradeLoadFailure(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	mockDocker := NewMockdockerClient(mockCtrl)
	defer getDockerClientMock(mockDocker)()
	mockDownloader := NewMockdownloader(mockCtrl)

	gomock.InOrder(
		mockDocker.EXPECT().RemoveExistingAgentContainer(),
		mockDocker.EXPECT().StartAgent().Return(upgradeAgentExitCode, nil),
		mockDownloader.EXPECT().LoadDesiredAgent().Return(&os.File{}, nil),
		mockDocker.EXPECT().LoadImage(gomock.Any()).Return(errors.New("test error")),
		mockDocker.EXPECT().RemoveExistingAgentContainer(),
		mockDocker.EXPECT().StartAgent().Return(terminalSuccessAgentExitCode, nil),
	)

	engine := &Engine{
		downloader: mockDownloader,
	}
	err := engine.StartSupervised()
	if err != nil {
		t.Error("Expected error to be nil but was returned")
	}
}

func TestStartSupervisedUpgrade(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	mockDocker := NewMockdockerClient(mockCtrl)
	defer getDockerClientMock(mockDocker)()
	mockDownloader := NewMockdownloader(mockCtrl)

	gomock.InOrder(
		mockDocker.EXPECT().RemoveExistingAgentContainer(),
		mockDocker.EXPECT().StartAgent().Return(upgradeAgentExitCode, nil),
		mockDownloader.EXPECT().LoadDesiredAgent().Return(&os.File{}, nil),
		mockDocker.EXPECT().LoadImage(gomock.Any()),
		mockDownloader.EXPECT().RecordCachedAgent(),
		mockDocker.EXPECT().RemoveExistingAgentContainer(),
		mockDocker.EXPECT().StartAgent().Return(terminalSuccessAgentExitCode, nil),
	)

	engine := &Engine{
		downloader: mockDownloader,
	}
	err := engine.StartSupervised()
	if err != nil {
		t.Error("Expected error to be nil but was returned")
	}
}

func TestPreStop(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	mockDocker := NewMockdockerClient(mockCtrl)
	defer getDockerClientMock(mockDocker)()
	mockDocker.EXPECT().StopAgent()

	engine := &Engine{}
	err := engine.PreStop()
	if err != nil {
		t.Errorf("engine pre-stop error: %v", err)
	}
}

func TestReloadCacheNotCached(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	cachedAgentBuffer := ioutil.NopCloser(&bytes.Buffer{})

	mockDocker := NewMockdockerClient(mockCtrl)
	defer getDockerClientMock(mockDocker)()
	mockDownloader := NewMockdownloader(mockCtrl)

	mockDownloader.EXPECT().IsAgentCached().Return(false)
	mockDownloader.EXPECT().DownloadAgent()
	mockDownloader.EXPECT().LoadCachedAgent().Return(cachedAgentBuffer, nil)
	mockDocker.EXPECT().LoadImage(cachedAgentBuffer)
	mockDownloader.EXPECT().RecordCachedAgent()

	engine := &Engine{
		downloader: mockDownloader,
	}
	err := engine.ReloadCache()
	if err != nil {
		t.Errorf("engine reload-cache error: %v", err)
	}
}

func TestReloadCacheCached(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	cachedAgentBuffer := ioutil.NopCloser(&bytes.Buffer{})

	mockDocker := NewMockdockerClient(mockCtrl)
	defer getDockerClientMock(mockDocker)()
	mockDownloader := NewMockdownloader(mockCtrl)

	mockDownloader.EXPECT().IsAgentCached().Return(true)
	mockDownloader.EXPECT().LoadCachedAgent().Return(cachedAgentBuffer, nil)
	mockDocker.EXPECT().LoadImage(cachedAgentBuffer)
	mockDownloader.EXPECT().RecordCachedAgent()

	engine := &Engine{
		downloader: mockDownloader,
	}
	err := engine.ReloadCache()
	if err != nil {
		t.Errorf("engine reload-cache error: %v", err)
	}
}

func TestPrestartLoopbackRoutingNotEnabled(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	mockDocker := NewMockdockerClient(mockCtrl)
	defer getDockerClientMock(mockDocker)()
	mockDownloader := NewMockdownloader(mockCtrl)
	mockLoopbackRouting := NewMockloopbackRouting(mockCtrl)

	mockDocker.EXPECT().LoadEnvVars().Return(nil)
	mockLoopbackRouting.EXPECT().Enable().Return(fmt.Errorf("sysctl not found"))
	mockRoute := NewMockcredentialsProxyRoute(mockCtrl)

	engine := &Engine{
		downloader:            mockDownloader,
		loopbackRouting:       mockLoopbackRouting,
		credentialsProxyRoute: mockRoute,
	}
	err := engine.PreStart()
	if err == nil {
		t.Error("Expected pre-start error when loopback routing not enabled")
	}
}

func TestPrestartCredentialsProxyRouteNotCreated(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	mockDocker := NewMockdockerClient(mockCtrl)
	defer getDockerClientMock(mockDocker)()
	mockDownloader := NewMockdownloader(mockCtrl)
	mockLoopbackRouting := NewMockloopbackRouting(mockCtrl)
	mockIpv6RouterAdvertisements := NewMockipv6RouterAdvertisements(mockCtrl)
	mockIpv6RouterAdvertisements.EXPECT().Disable().Return(nil)

	mockDocker.EXPECT().LoadEnvVars().Return(nil)
	mockLoopbackRouting.EXPECT().Enable().Return(nil)
	mockRoute := NewMockcredentialsProxyRoute(mockCtrl)
	mockRoute.EXPECT().Create().Return(fmt.Errorf("iptables not found"))

	engine := &Engine{
		downloader:               mockDownloader,
		loopbackRouting:          mockLoopbackRouting,
		ipv6RouterAdvertisements: mockIpv6RouterAdvertisements,
		credentialsProxyRoute:    mockRoute,
	}
	err := engine.PreStart()
	if err == nil {
		t.Error("Expected pre-start error when the credentials proxy route cannot be created")
	}
}

func TestPostStop(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	mockLoopbackRouting := NewMockloopbackRouting(mockCtrl)
	mockLoopbackRouting.EXPECT().RestoreDefault().Return(nil)
	mockRoute := NewMockcredentialsProxyRoute(mockCtrl)
	mockRoute.EXPECT().Remove().Return(nil)

	engine := &Engine{
		loopbackRouting:       mockLoopbackRouting,
		credentialsProxyRoute: mockRoute,
	}
	err := engine.PostStop()
	if err != nil {
		t.Errorf("engine post-stop error: %v", err)
	}
}

func TestPostStopLoopbackRoutingError(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	mockLoopbackRouting := NewMockloopbackRouting(mockCtrl)
	mockLoopbackRouting.EXPECT().RestoreDefault().Return(fmt.Errorf("cannot restore"))
	mockRoute := NewMockcredentialsProxyRoute(mockCtrl)
	mockRoute.EXPECT().Remove().Return(nil)

	engine := &Engine{
		loopbackRouting:       mockLoopbackRouting,
		credentialsProxyRoute: mockRoute,
	}
	err := engine.PostStop()
	if err == nil {
		t.Error("Expected error during engine post-stop")
	}
}

func TestPostStopCredentialsProxyRouteRemoveError(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	mockLoopbackRouting := NewMockloopbackRouting(mockCtrl)
	mockLoopbackRouting.EXPECT().RestoreDefault().Return(nil)
	mockRoute := NewMockcredentialsProxyRoute(mockCtrl)
	mockRoute.EXPECT().Remove().Return(fmt.Errorf("cannot remove"))

	engine := &Engine{
		loopbackRouting:       mockLoopbackRouting,
		credentialsProxyRoute: mockRoute,
	}
	err := engine.PostStop()
	if err != nil {
		t.Errorf("engine post-stop error: %v", err)
	}
}
