//go:build unit

// Copyright 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 dependencygraph

import (
	"fmt"
	"testing"
	"time"

	apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container"
	apicontainerstatus "github.com/aws/amazon-ecs-agent/agent/api/container/status"
	apitask "github.com/aws/amazon-ecs-agent/agent/api/task"
	"github.com/aws/amazon-ecs-agent/agent/config"
	"github.com/aws/amazon-ecs-agent/agent/taskresource"
	mock_taskresource "github.com/aws/amazon-ecs-agent/agent/taskresource/mocks"
	resourcestatus "github.com/aws/amazon-ecs-agent/agent/taskresource/status"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/assert"
)

func volumeStrToVol(vols []string) []apicontainer.VolumeFrom {
	ret := make([]apicontainer.VolumeFrom, len(vols))
	for i, v := range vols {
		ret[i] = apicontainer.VolumeFrom{SourceContainer: v, ReadOnly: false}
	}
	return ret
}

func steadyStateContainer(name string, dependsOn []apicontainer.DependsOn, desiredState apicontainerstatus.ContainerStatus, steadyState apicontainerstatus.ContainerStatus) *apicontainer.Container {
	container := apicontainer.NewContainerWithSteadyState(steadyState)
	container.Name = name
	container.DependsOnUnsafe = dependsOn
	container.DesiredStatusUnsafe = desiredState
	return container
}

func createdContainer(name string, dependsOn []apicontainer.DependsOn, steadyState apicontainerstatus.ContainerStatus) *apicontainer.Container {
	container := apicontainer.NewContainerWithSteadyState(steadyState)
	container.Name = name
	container.DependsOnUnsafe = dependsOn
	container.DesiredStatusUnsafe = apicontainerstatus.ContainerCreated
	return container
}

func TestValidDependencies(t *testing.T) {
	// Empty task
	task := &apitask.Task{}
	cfg := config.Config{}
	resolveable := ValidDependencies(task, &cfg)
	assert.True(t, resolveable, "The zero dependency graph should resolve")

	task = &apitask.Task{
		Containers: []*apicontainer.Container{
			{
				Name:                "redis",
				DesiredStatusUnsafe: apicontainerstatus.ContainerRunning,
			},
		},
	}
	resolveable = ValidDependencies(task, &cfg)
	assert.True(t, resolveable, "One container should resolve trivially")

	// Webserver stack
	php := steadyStateContainer("php", []apicontainer.DependsOn{{ContainerName: "db", Condition: startCondition}}, apicontainerstatus.ContainerRunning, apicontainerstatus.ContainerRunning)
	db := steadyStateContainer("db", []apicontainer.DependsOn{{ContainerName: "dbdatavolume", Condition: createCondition}}, apicontainerstatus.ContainerRunning, apicontainerstatus.ContainerRunning)
	dbdata := createdContainer("dbdatavolume", []apicontainer.DependsOn{}, apicontainerstatus.ContainerRunning)
	webserver := steadyStateContainer("webserver", []apicontainer.DependsOn{{ContainerName: "php", Condition: startCondition}, {ContainerName: "htmldata", Condition: createCondition}}, apicontainerstatus.ContainerRunning, apicontainerstatus.ContainerRunning)
	htmldata := steadyStateContainer("htmldata", []apicontainer.DependsOn{{ContainerName: "sharedcssfiles", Condition: createCondition}}, apicontainerstatus.ContainerRunning, apicontainerstatus.ContainerRunning)
	sharedcssfiles := createdContainer("sharedcssfiles", []apicontainer.DependsOn{}, apicontainerstatus.ContainerRunning)

	task = &apitask.Task{
		Containers: []*apicontainer.Container{
			php, db, dbdata, webserver, htmldata, sharedcssfiles,
		},
	}

	resolveable = ValidDependencies(task, &cfg)
	assert.True(t, resolveable, "The webserver group should resolve just fine")
}

func TestValidDependenciesWithCycles(t *testing.T) {
	// Unresolveable: cycle
	task := &apitask.Task{
		Containers: []*apicontainer.Container{
			steadyStateContainer("a", []apicontainer.DependsOn{{ContainerName: "b", Condition: createCondition}}, apicontainerstatus.ContainerRunning, apicontainerstatus.ContainerRunning),
			steadyStateContainer("b", []apicontainer.DependsOn{{ContainerName: "a", Condition: createCondition}}, apicontainerstatus.ContainerRunning, apicontainerstatus.ContainerRunning),
		},
	}
	cfg := config.Config{}
	resolveable := ValidDependencies(task, &cfg)
	assert.False(t, resolveable, "Cycle should not be resolveable")
}

func TestValidDependenciesWithUnresolvedReference(t *testing.T) {
	// Unresolveable, reference doesn't exist
	task := &apitask.Task{
		Containers: []*apicontainer.Container{
			steadyStateContainer("php", []apicontainer.DependsOn{{ContainerName: "db", Condition: createCondition}}, apicontainerstatus.ContainerRunning, apicontainerstatus.ContainerRunning),
		},
	}
	cfg := config.Config{}
	resolveable := ValidDependencies(task, &cfg)
	assert.False(t, resolveable, "Nonexistent reference shouldn't resolve")
}

func TestDependenciesAreResolvedWhenSteadyStateIsRunning(t *testing.T) {
	task := &apitask.Task{
		Containers: []*apicontainer.Container{
			{
				Name:                "redis",
				DesiredStatusUnsafe: apicontainerstatus.ContainerRunning,
			},
		},
	}
	cfg := config.Config{}
	_, err := DependenciesAreResolved(task.Containers[0], task.Containers, "", nil, nil, &cfg)
	assert.NoError(t, err, "One container should resolve trivially")

	// Webserver stack
	php := steadyStateContainer("php", []apicontainer.DependsOn{{ContainerName: "db", Condition: startCondition}}, apicontainerstatus.ContainerRunning, apicontainerstatus.ContainerRunning)
	db := steadyStateContainer("db", []apicontainer.DependsOn{{ContainerName: "dbdatavolume", Condition: createCondition}}, apicontainerstatus.ContainerRunning, apicontainerstatus.ContainerRunning)
	dbdata := createdContainer("dbdatavolume", []apicontainer.DependsOn{}, apicontainerstatus.ContainerRunning)
	webserver := steadyStateContainer("webserver", []apicontainer.DependsOn{{ContainerName: "php", Condition: startCondition}, {ContainerName: "htmldata", Condition: createCondition}}, apicontainerstatus.ContainerRunning, apicontainerstatus.ContainerRunning)
	htmldata := steadyStateContainer("htmldata", []apicontainer.DependsOn{{ContainerName: "sharedcssfiles", Condition: createCondition}}, apicontainerstatus.ContainerRunning, apicontainerstatus.ContainerRunning)
	sharedcssfiles := createdContainer("sharedcssfiles", []apicontainer.DependsOn{}, apicontainerstatus.ContainerRunning)

	task = &apitask.Task{
		Containers: []*apicontainer.Container{
			php, db, dbdata, webserver, htmldata, sharedcssfiles,
		},
	}

	_, err = DependenciesAreResolved(php, task.Containers, "", nil, nil, &cfg)
	assert.Error(t, err, "Shouldn't be resolved; db isn't running")

	_, err = DependenciesAreResolved(db, task.Containers, "", nil, nil, &cfg)
	assert.Error(t, err, "Shouldn't be resolved; dbdatavolume isn't created")

	_, err = DependenciesAreResolved(dbdata, task.Containers, "", nil, nil, &cfg)
	assert.NoError(t, err, "data volume with no deps should resolve")

	dbdata.SetKnownStatus(apicontainerstatus.ContainerCreated)
	_, err = DependenciesAreResolved(php, task.Containers, "", nil, nil, &cfg)
	assert.Error(t, err, "Php shouldn't run, db is not created")

	db.SetKnownStatus(apicontainerstatus.ContainerCreated)
	_, err = DependenciesAreResolved(php, task.Containers, "", nil, nil, &cfg)
	assert.Error(t, err, "Php shouldn't run, db is not running")

	_, err = DependenciesAreResolved(db, task.Containers, "", nil, nil, &cfg)
	assert.NoError(t, err, "db should be resolved, dbdata volume is Created")
	db.SetKnownStatus(apicontainerstatus.ContainerRunning)

	_, err = DependenciesAreResolved(php, task.Containers, "", nil, nil, &cfg)
	assert.NoError(t, err, "Php should resolve")
}

func TestRunDependencies(t *testing.T) {
	c1 := &apicontainer.Container{
		Name:              "a",
		KnownStatusUnsafe: apicontainerstatus.ContainerStatusNone,
	}
	c2 := &apicontainer.Container{
		Name:                    "b",
		KnownStatusUnsafe:       apicontainerstatus.ContainerStatusNone,
		DesiredStatusUnsafe:     apicontainerstatus.ContainerCreated,
		SteadyStateDependencies: []string{"a"},
	}
	task := &apitask.Task{Containers: []*apicontainer.Container{c1, c2}}
	cfg := config.Config{}
	_, err := DependenciesAreResolved(c2, task.Containers, "", nil, nil, &cfg)
	assert.Error(t, err, "Dependencies should not be resolved")

	task.Containers[1].SetDesiredStatus(apicontainerstatus.ContainerRunning)
	_, err = DependenciesAreResolved(c2, task.Containers, "", nil, nil, &cfg)
	assert.Error(t, err, "Dependencies should not be resolved")

	task.Containers[0].SetKnownStatus(apicontainerstatus.ContainerRunning)
	_, err = DependenciesAreResolved(c2, task.Containers, "", nil, nil, &cfg)
	assert.NoError(t, err, "Dependencies should be resolved")

	task.Containers[1].SetDesiredStatus(apicontainerstatus.ContainerCreated)
	_, err = DependenciesAreResolved(c1, task.Containers, "", nil, nil, &cfg)
	assert.NoError(t, err, "Dependencies should be resolved")
}

func TestRunDependenciesWhenSteadyStateIsResourcesProvisionedForOneContainer(t *testing.T) {
	// Webserver stack
	php := steadyStateContainer("php", []apicontainer.DependsOn{{ContainerName: "db", Condition: createCondition}}, apicontainerstatus.ContainerRunning, apicontainerstatus.ContainerRunning)
	db := steadyStateContainer("db", []apicontainer.DependsOn{{ContainerName: "dbdatavolume", Condition: createCondition}}, apicontainerstatus.ContainerRunning, apicontainerstatus.ContainerRunning)
	dbdata := createdContainer("dbdatavolume", []apicontainer.DependsOn{}, apicontainerstatus.ContainerRunning)
	webserver := steadyStateContainer("webserver", []apicontainer.DependsOn{{ContainerName: "php", Condition: createCondition}, {ContainerName: "htmldata", Condition: createCondition}}, apicontainerstatus.ContainerRunning, apicontainerstatus.ContainerRunning)
	htmldata := steadyStateContainer("htmldata", []apicontainer.DependsOn{{ContainerName: "sharedcssfiles", Condition: createCondition}}, apicontainerstatus.ContainerRunning, apicontainerstatus.ContainerRunning)
	sharedcssfiles := createdContainer("sharedcssfiles", []apicontainer.DependsOn{}, apicontainerstatus.ContainerRunning)
	// The Pause container, being added to the webserver stack
	pause := steadyStateContainer("pause", []apicontainer.DependsOn{}, apicontainerstatus.ContainerResourcesProvisioned, apicontainerstatus.ContainerResourcesProvisioned)

	task := &apitask.Task{
		Containers: []*apicontainer.Container{
			php, db, dbdata, webserver, htmldata, sharedcssfiles, pause,
		},
	}

	cfg := config.Config{}

	// Add a dependency on the pause container for all containers in the webserver stack
	for _, container := range task.Containers {
		if container.Name == "pause" {
			continue
		}
		container.SteadyStateDependencies = []string{"pause"}
		_, err := DependenciesAreResolved(container, task.Containers, "", nil, nil, &cfg)
		assert.Error(t, err, "Shouldn't be resolved; pause isn't running")
	}

	_, err := DependenciesAreResolved(pause, task.Containers, "", nil, nil, &cfg)
	assert.NoError(t, err, "Pause container's dependencies should be resolved")

	// Transition pause container to RUNNING
	pause.KnownStatusUnsafe = apicontainerstatus.ContainerRunning
	// Transition dependencies in webserver stack to CREATED/RUNNING state
	dbdata.KnownStatusUnsafe = apicontainerstatus.ContainerCreated
	db.KnownStatusUnsafe = apicontainerstatus.ContainerRunning
	for _, container := range task.Containers {
		if container.Name == "pause" {
			continue
		}
		// Assert that dependencies remain unresolved until the pause container reaches
		// RESOURCES_PROVISIONED
		_, err = DependenciesAreResolved(container, task.Containers, "", nil, nil, &cfg)
		assert.Error(t, err, "Shouldn't be resolved; pause isn't running")
	}
	pause.KnownStatusUnsafe = apicontainerstatus.ContainerResourcesProvisioned
	// Dependecies should be resolved now that the 'pause' container has
	// transitioned into RESOURCES_PROVISIONED
	_, err = DependenciesAreResolved(php, task.Containers, "", nil, nil, &cfg)
	assert.NoError(t, err, "Php should resolve")
}

func TestOnSteadyStateIsResolved(t *testing.T) {
	testcases := []struct {
		TargetDesired apicontainerstatus.ContainerStatus
		RunKnown      apicontainerstatus.ContainerStatus
		Resolved      bool
	}{
		{
			TargetDesired: apicontainerstatus.ContainerStatusNone,
			Resolved:      false,
		},
		{
			TargetDesired: apicontainerstatus.ContainerPulled,
			Resolved:      false,
		},
		{
			TargetDesired: apicontainerstatus.ContainerCreated,
			RunKnown:      apicontainerstatus.ContainerCreated,
			Resolved:      false,
		},
		{
			TargetDesired: apicontainerstatus.ContainerCreated,
			RunKnown:      apicontainerstatus.ContainerRunning,
			Resolved:      true,
		},
		{
			TargetDesired: apicontainerstatus.ContainerCreated,
			RunKnown:      apicontainerstatus.ContainerStopped,
			Resolved:      true,
		},
	}
	for _, tc := range testcases {
		t.Run(fmt.Sprintf("T:%s+R:%s", tc.TargetDesired.String(), tc.RunKnown.String()),
			assertResolved(onSteadyStateIsResolved, tc.TargetDesired, tc.RunKnown, tc.Resolved))
	}
}

func assertResolved(f func(target *apicontainer.Container, dep *apicontainer.Container) bool, targetDesired, depKnown apicontainerstatus.ContainerStatus, expectedResolved bool) func(t *testing.T) {
	return func(t *testing.T) {
		target := &apicontainer.Container{
			DesiredStatusUnsafe: targetDesired,
		}
		dep := &apicontainer.Container{
			KnownStatusUnsafe: depKnown,
		}
		resolved := f(target, dep)
		assert.Equal(t, expectedResolved, resolved)
	}
}

func TestVerifyTransitionDependenciesResolved(t *testing.T) {
	testcases := []struct {
		Name            string
		TargetKnown     apicontainerstatus.ContainerStatus
		TargetDesired   apicontainerstatus.ContainerStatus
		TargetNext      apicontainerstatus.ContainerStatus
		DependencyName  string
		DependencyKnown apicontainerstatus.ContainerStatus
		SatisfiedStatus apicontainerstatus.ContainerStatus
		ResolvedErr     error
	}{
		{
			Name:            "Nothing running, pull depends on running",
			TargetKnown:     apicontainerstatus.ContainerStatusNone,
			TargetDesired:   apicontainerstatus.ContainerRunning,
			TargetNext:      apicontainerstatus.ContainerPulled,
			DependencyName:  "container",
			DependencyKnown: apicontainerstatus.ContainerStatusNone,
			SatisfiedStatus: apicontainerstatus.ContainerRunning,
			ResolvedErr:     ErrContainerDependencyNotResolved,
		},

		{
			Name:            "Nothing running, pull depends on resources provisioned",
			TargetKnown:     apicontainerstatus.ContainerStatusNone,
			TargetDesired:   apicontainerstatus.ContainerRunning,
			TargetNext:      apicontainerstatus.ContainerPulled,
			DependencyName:  "container",
			DependencyKnown: apicontainerstatus.ContainerStatusNone,
			SatisfiedStatus: apicontainerstatus.ContainerResourcesProvisioned,
			ResolvedErr:     ErrContainerDependencyNotResolved,
		},
		{
			Name:            "Nothing running, create depends on running",
			TargetKnown:     apicontainerstatus.ContainerStatusNone,
			TargetDesired:   apicontainerstatus.ContainerRunning,
			TargetNext:      apicontainerstatus.ContainerCreated,
			DependencyName:  "container",
			DependencyKnown: apicontainerstatus.ContainerStatusNone,
			SatisfiedStatus: apicontainerstatus.ContainerRunning,
		},
		{
			Name:            "Dependency created, pull depends on running",
			TargetKnown:     apicontainerstatus.ContainerStatusNone,
			TargetDesired:   apicontainerstatus.ContainerRunning,
			TargetNext:      apicontainerstatus.ContainerPulled,
			DependencyName:  "container",
			DependencyKnown: apicontainerstatus.ContainerCreated,
			SatisfiedStatus: apicontainerstatus.ContainerRunning,
			ResolvedErr:     ErrContainerDependencyNotResolved,
		},
		{
			Name:            "Dependency created, pull depends on resources provisioned",
			TargetKnown:     apicontainerstatus.ContainerStatusNone,
			TargetDesired:   apicontainerstatus.ContainerRunning,
			TargetNext:      apicontainerstatus.ContainerPulled,
			DependencyName:  "container",
			DependencyKnown: apicontainerstatus.ContainerCreated,
			SatisfiedStatus: apicontainerstatus.ContainerResourcesProvisioned,
			ResolvedErr:     ErrContainerDependencyNotResolved,
		},
		{
			Name:            "Dependency running, pull depends on running",
			TargetKnown:     apicontainerstatus.ContainerStatusNone,
			TargetDesired:   apicontainerstatus.ContainerRunning,
			TargetNext:      apicontainerstatus.ContainerPulled,
			DependencyName:  "container",
			DependencyKnown: apicontainerstatus.ContainerRunning,
			SatisfiedStatus: apicontainerstatus.ContainerRunning,
		},
		{
			Name:            "Dependency running, pull depends on resources provisioned",
			TargetKnown:     apicontainerstatus.ContainerStatusNone,
			TargetDesired:   apicontainerstatus.ContainerRunning,
			TargetNext:      apicontainerstatus.ContainerPulled,
			DependencyName:  "container",
			DependencyKnown: apicontainerstatus.ContainerRunning,
			SatisfiedStatus: apicontainerstatus.ContainerResourcesProvisioned,
			ResolvedErr:     ErrContainerDependencyNotResolved,
		},
		{
			Name:            "Dependency resources provisioned, pull depends on resources provisioned",
			TargetKnown:     apicontainerstatus.ContainerStatusNone,
			TargetDesired:   apicontainerstatus.ContainerRunning,
			TargetNext:      apicontainerstatus.ContainerPulled,
			DependencyName:  "container",
			DependencyKnown: apicontainerstatus.ContainerResourcesProvisioned,
			SatisfiedStatus: apicontainerstatus.ContainerResourcesProvisioned,
		},
		{
			Name:            "Dependency running, create depends on created",
			TargetKnown:     apicontainerstatus.ContainerPulled,
			TargetDesired:   apicontainerstatus.ContainerRunning,
			TargetNext:      apicontainerstatus.ContainerCreated,
			DependencyName:  "container",
			DependencyKnown: apicontainerstatus.ContainerRunning,
			SatisfiedStatus: apicontainerstatus.ContainerCreated,
		},
		{
			Name:            "Target running, create depends on running",
			TargetKnown:     apicontainerstatus.ContainerRunning,
			TargetDesired:   apicontainerstatus.ContainerRunning,
			TargetNext:      apicontainerstatus.ContainerRunning,
			DependencyName:  "container",
			DependencyKnown: apicontainerstatus.ContainerRunning,
			SatisfiedStatus: apicontainerstatus.ContainerCreated,
		},
		{
			Name:            "Target pulled, desired stopped",
			TargetKnown:     apicontainerstatus.ContainerPulled,
			TargetDesired:   apicontainerstatus.ContainerStopped,
			TargetNext:      apicontainerstatus.ContainerRunning,
			DependencyName:  "container",
			DependencyKnown: apicontainerstatus.ContainerStatusNone,
			SatisfiedStatus: apicontainerstatus.ContainerCreated,
		},
		// Note: Not all possible situations are tested here.  The only situations tested here are ones that are
		// expected to reasonably happen at the time this code was written.  Other behavior is not expected to occur,
		// so it is not tested.
	}
	for _, tc := range testcases {
		t.Run(tc.Name, func(t *testing.T) {
			target := &apicontainer.Container{
				KnownStatusUnsafe:         tc.TargetKnown,
				DesiredStatusUnsafe:       tc.TargetDesired,
				TransitionDependenciesMap: make(map[apicontainerstatus.ContainerStatus]apicontainer.TransitionDependencySet),
			}
			target.BuildContainerDependency(tc.DependencyName, tc.SatisfiedStatus, tc.TargetNext)
			dep := &apicontainer.Container{
				Name:              tc.DependencyName,
				KnownStatusUnsafe: tc.DependencyKnown,
			}
			containers := make(map[string]*apicontainer.Container)
			containers[dep.Name] = dep
			resolved := verifyTransitionDependenciesResolved(target, containers, nil)
			assert.Equal(t, tc.ResolvedErr, resolved)
		})
	}
}

func TestVerifyResourceDependenciesResolved(t *testing.T) {
	testcases := []struct {
		Name            string
		TargetKnown     apicontainerstatus.ContainerStatus
		TargetDep       apicontainerstatus.ContainerStatus
		DependencyKnown resourcestatus.ResourceStatus
		RequiredStatus  resourcestatus.ResourceStatus

		ExpectedResolved bool
	}{
		{
			Name:             "resource none,container pull depends on resource created",
			TargetKnown:      apicontainerstatus.ContainerStatusNone,
			TargetDep:        apicontainerstatus.ContainerPulled,
			DependencyKnown:  resourcestatus.ResourceStatus(0),
			RequiredStatus:   resourcestatus.ResourceStatus(1),
			ExpectedResolved: false,
		},
		{
			Name:             "resource created,container pull depends on resource created",
			TargetKnown:      apicontainerstatus.ContainerStatusNone,
			TargetDep:        apicontainerstatus.ContainerPulled,
			DependencyKnown:  resourcestatus.ResourceStatus(1),
			RequiredStatus:   resourcestatus.ResourceStatus(1),
			ExpectedResolved: true,
		},
		{
			Name:             "resource none,container create depends on resource created",
			TargetKnown:      apicontainerstatus.ContainerStatusNone,
			TargetDep:        apicontainerstatus.ContainerCreated,
			DependencyKnown:  resourcestatus.ResourceStatus(0),
			RequiredStatus:   resourcestatus.ResourceStatus(1),
			ExpectedResolved: true,
		},
	}
	for _, tc := range testcases {
		t.Run(tc.Name, func(t *testing.T) {
			resourceName := "resource"
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()
			mockResource := mock_taskresource.NewMockTaskResource(ctrl)
			gomock.InOrder(
				mockResource.EXPECT().SetKnownStatus(tc.DependencyKnown),
				mockResource.EXPECT().GetKnownStatus().Return(tc.DependencyKnown).AnyTimes(),
			)
			mockResource.SetKnownStatus(tc.DependencyKnown)
			target := &apicontainer.Container{
				KnownStatusUnsafe:         tc.TargetKnown,
				TransitionDependenciesMap: make(map[apicontainerstatus.ContainerStatus]apicontainer.TransitionDependencySet),
			}
			target.BuildResourceDependency(resourceName, tc.RequiredStatus, tc.TargetDep)
			resources := make(map[string]taskresource.TaskResource)
			resources[resourceName] = mockResource
			resolved := verifyResourceDependenciesResolved(target, resources)
			assert.Equal(t, tc.ExpectedResolved, resolved)
		})
	}
}

func TestVerifyTransitionResourceDependenciesResolved(t *testing.T) {
	testcases := []struct {
		Name            string
		TargetKnown     apicontainerstatus.ContainerStatus
		TargetDep       apicontainerstatus.ContainerStatus
		DependencyKnown resourcestatus.ResourceStatus
		RequiredStatus  resourcestatus.ResourceStatus
		ResolvedErr     error
	}{
		{
			Name:            "resource none,container pull depends on resource created",
			TargetKnown:     apicontainerstatus.ContainerStatusNone,
			TargetDep:       apicontainerstatus.ContainerPulled,
			DependencyKnown: resourcestatus.ResourceStatus(0),
			RequiredStatus:  resourcestatus.ResourceStatus(1),
			ResolvedErr:     ErrResourceDependencyNotResolved,
		},
		{
			Name:            "resource created,container pull depends on resource created",
			TargetKnown:     apicontainerstatus.ContainerStatusNone,
			TargetDep:       apicontainerstatus.ContainerPulled,
			DependencyKnown: resourcestatus.ResourceStatus(1),
			RequiredStatus:  resourcestatus.ResourceStatus(1),
		},
		{
			Name:            "resource none,container create depends on resource created",
			TargetKnown:     apicontainerstatus.ContainerStatusNone,
			TargetDep:       apicontainerstatus.ContainerCreated,
			DependencyKnown: resourcestatus.ResourceStatus(0),
			RequiredStatus:  resourcestatus.ResourceStatus(1),
		},
	}
	for _, tc := range testcases {
		t.Run(tc.Name, func(t *testing.T) {
			resourceName := "resource"
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()
			mockResource := mock_taskresource.NewMockTaskResource(ctrl)
			gomock.InOrder(
				mockResource.EXPECT().SetKnownStatus(tc.DependencyKnown),
				mockResource.EXPECT().GetKnownStatus().Return(tc.DependencyKnown).AnyTimes(),
			)
			mockResource.SetKnownStatus(tc.DependencyKnown)
			target := &apicontainer.Container{
				KnownStatusUnsafe:         tc.TargetKnown,
				TransitionDependenciesMap: make(map[apicontainerstatus.ContainerStatus]apicontainer.TransitionDependencySet),
			}
			target.BuildResourceDependency(resourceName, tc.RequiredStatus, tc.TargetDep)
			resources := make(map[string]taskresource.TaskResource)
			resources[resourceName] = mockResource
			resolved := verifyTransitionDependenciesResolved(target, nil, resources)
			assert.Equal(t, tc.ResolvedErr, resolved)
		})
	}
}

func TestTransitionDependencyResourceNotFound(t *testing.T) {
	// this test verifies if error is thrown when the resource dependency is not
	// found in the list of task resources
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()
	mockResource := mock_taskresource.NewMockTaskResource(ctrl)
	gomock.InOrder(
		mockResource.EXPECT().SetKnownStatus(resourcestatus.ResourceStatus(1)),
	)
	mockResource.SetKnownStatus(resourcestatus.ResourceStatus(1))
	target := &apicontainer.Container{
		KnownStatusUnsafe:         apicontainerstatus.ContainerStatusNone,
		TransitionDependenciesMap: make(map[apicontainerstatus.ContainerStatus]apicontainer.TransitionDependencySet),
	}
	target.BuildResourceDependency("resource", resourcestatus.ResourceStatus(1), apicontainerstatus.ContainerPulled)

	resources := make(map[string]taskresource.TaskResource)
	resources["resource1"] = mockResource // different resource name
	resolved := verifyTransitionDependenciesResolved(target, nil, resources)
	assert.Equal(t, ErrResourceDependencyNotResolved, resolved)
}

// TestVerifyTransitionDependenciesResolvedForResource verifies the logic of function TaskResourceDependenciesAreResolved
func TestVerifyTransitionDependenciesResolvedForResource(t *testing.T) {
	testcases := []struct {
		Name            string
		TargetKnown     resourcestatus.ResourceStatus
		TargetDesired   resourcestatus.ResourceStatus
		TargetNext      resourcestatus.ResourceStatus
		DependencyName  string
		DependencyKnown apicontainerstatus.ContainerStatus
		SatisfiedStatus apicontainerstatus.ContainerStatus
		ResolvedErr     error
	}{
		{
			Name:            "desired created, resource created depends on container resources provisioned",
			TargetKnown:     resourcestatus.ResourceStatus(0),
			TargetDesired:   resourcestatus.ResourceStatus(1),
			TargetNext:      resourcestatus.ResourceStatus(1),
			DependencyName:  "container",
			DependencyKnown: apicontainerstatus.ContainerStatusNone,
			SatisfiedStatus: apicontainerstatus.ContainerResourcesProvisioned,
			ResolvedErr:     ErrContainerDependencyNotResolvedForResource,
		},
		{
			Name:            "desired removed, resource created depends on container resources provisioned",
			TargetKnown:     resourcestatus.ResourceStatus(0),
			TargetDesired:   resourcestatus.ResourceStatus(2),
			TargetNext:      resourcestatus.ResourceStatus(1),
			DependencyName:  "container",
			DependencyKnown: apicontainerstatus.ContainerStatusNone,
			SatisfiedStatus: apicontainerstatus.ContainerResourcesProvisioned,
			ResolvedErr:     ErrContainerDependencyNotResolvedForResource,
		},
		{
			Name:            "desired created, resource created depends on resourcesProvisioned, dependency resourcesProvisioned",
			TargetKnown:     resourcestatus.ResourceStatus(0),
			TargetDesired:   resourcestatus.ResourceStatus(1),
			TargetNext:      resourcestatus.ResourceStatus(1),
			DependencyName:  "container",
			DependencyKnown: apicontainerstatus.ContainerResourcesProvisioned,
			SatisfiedStatus: apicontainerstatus.ContainerResourcesProvisioned,
			ResolvedErr:     nil,
		},
		{
			Name:            "desired created, resource created depends on created, dependency passed created",
			TargetKnown:     resourcestatus.ResourceStatus(0),
			TargetDesired:   resourcestatus.ResourceStatus(1),
			TargetNext:      resourcestatus.ResourceStatus(1),
			DependencyName:  "container",
			DependencyKnown: apicontainerstatus.ContainerRunning,
			SatisfiedStatus: apicontainerstatus.ContainerCreated,
			ResolvedErr:     nil,
		},
		{
			Name:            "desired removed, resource removed depends on stopped, dependency resourceProvisioned",
			TargetKnown:     resourcestatus.ResourceStatus(1),
			TargetDesired:   resourcestatus.ResourceStatus(2),
			TargetNext:      resourcestatus.ResourceStatus(2),
			DependencyName:  "container",
			DependencyKnown: apicontainerstatus.ContainerResourcesProvisioned,
			SatisfiedStatus: apicontainerstatus.ContainerStopped,
			ResolvedErr:     ErrContainerDependencyNotResolvedForResource,
		},
		{
			Name:            "desired removed, resource removed depends on stopped, dependency stopped",
			TargetKnown:     resourcestatus.ResourceStatus(1),
			TargetDesired:   resourcestatus.ResourceStatus(2),
			TargetNext:      resourcestatus.ResourceStatus(2),
			DependencyName:  "container",
			DependencyKnown: apicontainerstatus.ContainerStopped,
			SatisfiedStatus: apicontainerstatus.ContainerStopped,
			ResolvedErr:     nil,
		},
	}
	for _, tc := range testcases {
		t.Run(tc.Name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			mockResource := mock_taskresource.NewMockTaskResource(ctrl)
			con := &apicontainer.Container{
				Name:              tc.DependencyName,
				KnownStatusUnsafe: tc.DependencyKnown,
			}
			containers := []*apicontainer.Container{
				con,
			}
			containerDep := []apicontainer.ContainerDependency{
				{
					ContainerName:   tc.DependencyName,
					SatisfiedStatus: tc.SatisfiedStatus,
				},
			}
			gomock.InOrder(
				mockResource.EXPECT().NextKnownState().Return(tc.TargetNext),
				mockResource.EXPECT().GetContainerDependencies(tc.TargetNext).Return(containerDep),
			)
			resolved := TaskResourceDependenciesAreResolved(mockResource, containers)
			assert.Equal(t, tc.ResolvedErr, resolved)
		})
	}

}

// TestResourceTransitionDependencyNotFound verifies if error is thrown when the container dependency for resource
// is not found in the list of task containers
func TestResourceTransitionDependencyNotFound(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()
	mockResource := mock_taskresource.NewMockTaskResource(ctrl)
	containerDep := []apicontainer.ContainerDependency{
		{
			ContainerName:   "container2",
			SatisfiedStatus: apicontainerstatus.ContainerStopped,
		},
	}
	con := &apicontainer.Container{
		Name:                      "container1",
		KnownStatusUnsafe:         apicontainerstatus.ContainerStopped,
		TransitionDependenciesMap: make(map[apicontainerstatus.ContainerStatus]apicontainer.TransitionDependencySet),
	}
	existingContainers := []*apicontainer.Container{
		con,
	}
	gomock.InOrder(
		mockResource.EXPECT().NextKnownState().Return(resourcestatus.ResourceStatus(2)),
		mockResource.EXPECT().GetContainerDependencies(resourcestatus.ResourceStatus(2)).Return(containerDep),
	)
	resolved := TaskResourceDependenciesAreResolved(mockResource, existingContainers)
	assert.Equal(t, ErrContainerDependencyNotResolvedForResource, resolved)
}

func TestResourceTransitionDependencyNoDependency(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()
	mockResource := mock_taskresource.NewMockTaskResource(ctrl)
	con := &apicontainer.Container{
		Name:                      "container1",
		KnownStatusUnsafe:         apicontainerstatus.ContainerStopped,
		TransitionDependenciesMap: make(map[apicontainerstatus.ContainerStatus]apicontainer.TransitionDependencySet),
	}
	existingContainers := []*apicontainer.Container{
		con,
	}
	gomock.InOrder(
		mockResource.EXPECT().NextKnownState().Return(resourcestatus.ResourceStatus(2)),
		mockResource.EXPECT().GetContainerDependencies(resourcestatus.ResourceStatus(2)).Return(nil),
	)
	assert.Nil(t, TaskResourceDependenciesAreResolved(mockResource, existingContainers))
}

func TestContainerOrderingCanResolve(t *testing.T) {
	testcases := []struct {
		TargetDesired       apicontainerstatus.ContainerStatus
		DependencyDesired   apicontainerstatus.ContainerStatus
		DependencyKnown     apicontainerstatus.ContainerStatus
		DependencyCondition string
		ExitCode            int
		Resolvable          bool
	}{
		{
			TargetDesired:       apicontainerstatus.ContainerCreated,
			DependencyDesired:   apicontainerstatus.ContainerStatusNone,
			DependencyCondition: createCondition,
			Resolvable:          false,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerCreated,
			DependencyDesired:   apicontainerstatus.ContainerStopped,
			DependencyCondition: createCondition,
			Resolvable:          true,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerCreated,
			DependencyDesired:   apicontainerstatus.ContainerZombie,
			DependencyCondition: createCondition,
			Resolvable:          false,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerRunning,
			DependencyDesired:   apicontainerstatus.ContainerStatusNone,
			DependencyCondition: createCondition,
			Resolvable:          false,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerRunning,
			DependencyDesired:   apicontainerstatus.ContainerCreated,
			DependencyCondition: createCondition,
			Resolvable:          true,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerRunning,
			DependencyDesired:   apicontainerstatus.ContainerRunning,
			DependencyCondition: createCondition,
			Resolvable:          true,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerRunning,
			DependencyDesired:   apicontainerstatus.ContainerStopped,
			DependencyCondition: createCondition,
			Resolvable:          true,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerCreated,
			DependencyDesired:   apicontainerstatus.ContainerCreated,
			DependencyCondition: startCondition,
			Resolvable:          true,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerRunning,
			DependencyDesired:   apicontainerstatus.ContainerRunning,
			DependencyCondition: startCondition,
			Resolvable:          true,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerCreated,
			DependencyDesired:   apicontainerstatus.ContainerRunning,
			DependencyCondition: startCondition,
			Resolvable:          true,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerRunning,
			DependencyDesired:   apicontainerstatus.ContainerZombie,
			DependencyCondition: startCondition,
			Resolvable:          false,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerRunning,
			DependencyDesired:   apicontainerstatus.ContainerStopped,
			DependencyCondition: successCondition,
			Resolvable:          true,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerRunning,
			DependencyDesired:   apicontainerstatus.ContainerRunning,
			DependencyCondition: successCondition,
			Resolvable:          true,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerRunning,
			DependencyKnown:     apicontainerstatus.ContainerRunning,
			DependencyDesired:   apicontainerstatus.ContainerStopped,
			ExitCode:            0,
			DependencyCondition: successCondition,
			Resolvable:          true,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerRunning,
			DependencyKnown:     apicontainerstatus.ContainerStopped,
			ExitCode:            0,
			DependencyCondition: successCondition,
			Resolvable:          true,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerRunning,
			DependencyKnown:     apicontainerstatus.ContainerStopped,
			ExitCode:            1,
			DependencyCondition: successCondition,
			Resolvable:          false,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerRunning,
			DependencyDesired:   apicontainerstatus.ContainerStopped,
			DependencyCondition: completeCondition,
			Resolvable:          true,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerRunning,
			DependencyDesired:   apicontainerstatus.ContainerRunning,
			DependencyCondition: completeCondition,
			Resolvable:          true,
		},
	}
	cfg := config.Config{}
	for _, tc := range testcases {
		t.Run(fmt.Sprintf("T:%s+V:%s", tc.TargetDesired.String(), tc.DependencyDesired.String()),
			assertContainerOrderingCanResolve(containerOrderingDependenciesCanResolve, tc.TargetDesired, tc.DependencyDesired, tc.DependencyKnown, tc.DependencyCondition, tc.ExitCode, tc.Resolvable, &cfg))
	}
}

func TestContainerOrderingIsResolved(t *testing.T) {
	testcases := []struct {
		TargetDesired       apicontainerstatus.ContainerStatus
		DependencyKnown     apicontainerstatus.ContainerStatus
		DependencyCondition string
		Resolved            bool
		ExitCode            int
	}{
		{
			TargetDesired:       apicontainerstatus.ContainerCreated,
			DependencyKnown:     apicontainerstatus.ContainerStatusNone,
			DependencyCondition: createCondition,
			Resolved:            false,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerCreated,
			DependencyKnown:     apicontainerstatus.ContainerCreated,
			DependencyCondition: createCondition,
			Resolved:            true,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerRunning,
			DependencyKnown:     apicontainerstatus.ContainerStopped,
			DependencyCondition: createCondition,
			Resolved:            true,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerCreated,
			DependencyKnown:     apicontainerstatus.ContainerStopped,
			DependencyCondition: createCondition,
			Resolved:            true,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerRunning,
			DependencyKnown:     apicontainerstatus.ContainerStatusNone,
			DependencyCondition: createCondition,
			Resolved:            false,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerRunning,
			DependencyKnown:     apicontainerstatus.ContainerCreated,
			DependencyCondition: createCondition,
			Resolved:            true,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerCreated,
			DependencyKnown:     apicontainerstatus.ContainerCreated,
			DependencyCondition: startCondition,
			Resolved:            true,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerCreated,
			DependencyKnown:     apicontainerstatus.ContainerRunning,
			DependencyCondition: startCondition,
			Resolved:            true,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerCreated,
			DependencyKnown:     apicontainerstatus.ContainerZombie,
			DependencyCondition: startCondition,
			Resolved:            false,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerRunning,
			DependencyKnown:     apicontainerstatus.ContainerRunning,
			DependencyCondition: startCondition,
			Resolved:            true,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerRunning,
			DependencyKnown:     apicontainerstatus.ContainerZombie,
			DependencyCondition: startCondition,
			Resolved:            false,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerRunning,
			DependencyKnown:     apicontainerstatus.ContainerStopped,
			DependencyCondition: successCondition,
			ExitCode:            0,
			Resolved:            true,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerRunning,
			DependencyKnown:     apicontainerstatus.ContainerStopped,
			DependencyCondition: successCondition,
			ExitCode:            1,
			Resolved:            false,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerRunning,
			DependencyKnown:     apicontainerstatus.ContainerStopped,
			DependencyCondition: completeCondition,
			ExitCode:            0,
			Resolved:            true,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerRunning,
			DependencyKnown:     apicontainerstatus.ContainerStopped,
			DependencyCondition: completeCondition,
			ExitCode:            1,
			Resolved:            true,
		},
		{
			TargetDesired:       apicontainerstatus.ContainerRunning,
			DependencyKnown:     apicontainerstatus.ContainerRunning,
			DependencyCondition: completeCondition,
			Resolved:            false,
		},
	}
	cfg := config.Config{}
	for _, tc := range testcases {
		t.Run(fmt.Sprintf("T:%s+V:%s", tc.TargetDesired.String(), tc.DependencyKnown.String()),
			assertContainerOrderingResolved(containerOrderingDependenciesIsResolved, tc.TargetDesired, tc.DependencyKnown, tc.DependencyCondition, tc.ExitCode, tc.Resolved, &cfg))
	}
}

func TestContainerOrderingIsResolvedWithDependentContainersPullUpfront(t *testing.T) {
	testcases := []struct {
		TargetKnown         apicontainerstatus.ContainerStatus
		DependencyKnown     apicontainerstatus.ContainerStatus
		DependencyCondition string
		Resolved            bool
		ExitCode            int
	}{
		{
			TargetKnown:         apicontainerstatus.ContainerStatusNone,
			DependencyKnown:     apicontainerstatus.ContainerStatusNone,
			DependencyCondition: startCondition,
			Resolved:            true,
		},
		{
			TargetKnown:         apicontainerstatus.ContainerStatusNone,
			DependencyKnown:     apicontainerstatus.ContainerPulled,
			DependencyCondition: startCondition,
			Resolved:            true,
		},
		{
			TargetKnown:         apicontainerstatus.ContainerStatusNone,
			DependencyKnown:     apicontainerstatus.ContainerStopped,
			DependencyCondition: startCondition,
			Resolved:            true,
		},
		{
			TargetKnown:         apicontainerstatus.ContainerCreated,
			DependencyKnown:     apicontainerstatus.ContainerStatusNone,
			DependencyCondition: startCondition,
			Resolved:            false,
		},
		{
			TargetKnown:         apicontainerstatus.ContainerStatusNone,
			DependencyKnown:     apicontainerstatus.ContainerStatusNone,
			DependencyCondition: successCondition,
			Resolved:            true,
		},
		{
			TargetKnown:         apicontainerstatus.ContainerStatusNone,
			DependencyKnown:     apicontainerstatus.ContainerPulled,
			DependencyCondition: successCondition,
			Resolved:            true,
		},
		{
			TargetKnown:         apicontainerstatus.ContainerStatusNone,
			DependencyKnown:     apicontainerstatus.ContainerCreated,
			DependencyCondition: successCondition,
			Resolved:            true,
		},
		{
			TargetKnown:         apicontainerstatus.ContainerStatusNone,
			DependencyKnown:     apicontainerstatus.ContainerRunning,
			DependencyCondition: successCondition,
			Resolved:            true,
		},
		{
			TargetKnown:         apicontainerstatus.ContainerStatusNone,
			DependencyKnown:     apicontainerstatus.ContainerStopped,
			ExitCode:            1,
			DependencyCondition: successCondition,
			Resolved:            true,
		},
		{
			TargetKnown:         apicontainerstatus.ContainerPulled,
			DependencyKnown:     apicontainerstatus.ContainerStatusNone,
			DependencyCondition: successCondition,
			Resolved:            false,
		},
		{
			TargetKnown:         apicontainerstatus.ContainerPulled,
			DependencyKnown:     apicontainerstatus.ContainerStopped,
			ExitCode:            1,
			DependencyCondition: successCondition,
			Resolved:            false,
		},
		{
			TargetKnown:         apicontainerstatus.ContainerStatusNone,
			DependencyKnown:     apicontainerstatus.ContainerStatusNone,
			DependencyCondition: completeCondition,
			Resolved:            true,
		},
		{
			TargetKnown:         apicontainerstatus.ContainerStatusNone,
			DependencyKnown:     apicontainerstatus.ContainerPulled,
			DependencyCondition: completeCondition,
			Resolved:            true,
		},
		{
			TargetKnown:         apicontainerstatus.ContainerStatusNone,
			DependencyKnown:     apicontainerstatus.ContainerCreated,
			DependencyCondition: completeCondition,
			Resolved:            true,
		},
		{
			TargetKnown:         apicontainerstatus.ContainerStatusNone,
			DependencyKnown:     apicontainerstatus.ContainerRunning,
			DependencyCondition: completeCondition,
			Resolved:            true,
		},
		{
			TargetKnown:         apicontainerstatus.ContainerStatusNone,
			DependencyKnown:     apicontainerstatus.ContainerStopped,
			DependencyCondition: completeCondition,
			Resolved:            true,
		},
		{
			TargetKnown:         apicontainerstatus.ContainerPulled,
			DependencyKnown:     apicontainerstatus.ContainerStatusNone,
			DependencyCondition: completeCondition,
			Resolved:            false,
		},
	}

	cfg := config.Config{
		DependentContainersPullUpfront: config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled},
	}
	for _, tc := range testcases {
		t.Run(fmt.Sprintf("DependencyCondition:%s+T:%s+V:%s", tc.DependencyCondition, tc.TargetKnown.String(), tc.DependencyKnown.String()),
			assertContainerTargetOrderingResolved(containerOrderingDependenciesIsResolved, tc.TargetKnown, tc.DependencyKnown, tc.DependencyCondition, tc.ExitCode, tc.Resolved, &cfg))
	}
}

func assertContainerTargetOrderingResolved(f func(target *apicontainer.Container, dep *apicontainer.Container, depCond string, cfg *config.Config) bool, targetKnown, depKnown apicontainerstatus.ContainerStatus, depCond string, exitcode int, expectedResolvable bool, cfg *config.Config) func(t *testing.T) {
	return func(t *testing.T) {
		target := &apicontainer.Container{
			KnownStatusUnsafe: targetKnown,
		}
		dep := &apicontainer.Container{
			KnownStatusUnsafe:   depKnown,
			KnownExitCodeUnsafe: aws.Int(exitcode),
		}

		resolvable := f(target, dep, depCond, cfg)
		assert.Equal(t, expectedResolvable, resolvable)
	}
}

func assertContainerOrderingCanResolve(f func(target *apicontainer.Container, dep *apicontainer.Container, depCond string, cfg *config.Config) bool, targetDesired, depDesired, depKnown apicontainerstatus.ContainerStatus, depCond string, exitcode int, expectedResolvable bool, cfg *config.Config) func(t *testing.T) {
	return func(t *testing.T) {
		target := &apicontainer.Container{
			DesiredStatusUnsafe: targetDesired,
		}
		dep := &apicontainer.Container{
			DesiredStatusUnsafe: depDesired,
			KnownStatusUnsafe:   depKnown,
			KnownExitCodeUnsafe: aws.Int(exitcode),
		}
		resolvable := f(target, dep, depCond, cfg)
		assert.Equal(t, expectedResolvable, resolvable)
	}
}

func assertContainerOrderingResolved(f func(target *apicontainer.Container, dep *apicontainer.Container, depCond string, cfg *config.Config) bool, targetDesired, depKnown apicontainerstatus.ContainerStatus, depCond string, exitcode int, expectedResolved bool, cfg *config.Config) func(t *testing.T) {
	return func(t *testing.T) {
		target := &apicontainer.Container{
			DesiredStatusUnsafe: targetDesired,
		}
		dep := &apicontainer.Container{
			KnownStatusUnsafe:   depKnown,
			KnownExitCodeUnsafe: aws.Int(exitcode),
		}
		resolved := f(target, dep, depCond, cfg)
		assert.Equal(t, expectedResolved, resolved)
	}
}

func TestContainerOrderingHealthyConditionIsResolved(t *testing.T) {
	testcases := []struct {
		TargetDesired                 apicontainerstatus.ContainerStatus
		TargetKnown                   apicontainerstatus.ContainerStatus
		DependencyKnownHealthStatus   apicontainerstatus.ContainerHealthStatus
		HealthCheckType               string
		DependencyKnownHealthExitCode int
		DependencyCondition           string
		Resolved                      bool
	}{
		{
			TargetDesired:               apicontainerstatus.ContainerCreated,
			DependencyKnownHealthStatus: apicontainerstatus.ContainerHealthy,
			HealthCheckType:             "docker",
			DependencyCondition:         healthyCondition,
			Resolved:                    true,
		},
		{
			TargetDesired:                 apicontainerstatus.ContainerCreated,
			DependencyKnownHealthStatus:   apicontainerstatus.ContainerUnhealthy,
			HealthCheckType:               "docker",
			DependencyKnownHealthExitCode: 1,
			DependencyCondition:           healthyCondition,
			Resolved:                      false,
		},
		{
			TargetDesired: apicontainerstatus.ContainerCreated,
			Resolved:      false,
		},
	}
	cfg := config.Config{}
	for _, tc := range testcases {
		t.Run(fmt.Sprintf("DependencyKnownHealthStatus:%s+T:%s+V:%s", tc.DependencyKnownHealthStatus, tc.TargetDesired.String(), tc.DependencyKnownHealthStatus.String()),
			assertContainerOrderingHealthyConditionResolved(containerOrderingDependenciesIsResolved, tc.TargetDesired, tc.TargetKnown, tc.DependencyKnownHealthStatus, tc.HealthCheckType, tc.DependencyKnownHealthExitCode, tc.DependencyCondition, tc.Resolved, &cfg))
	}
}

func TestContainerOrderingHealthyConditionIsResolvedWithDependentContainersPullUpfront(t *testing.T) {
	testcases := []struct {
		TargetDesired                 apicontainerstatus.ContainerStatus
		TargetKnown                   apicontainerstatus.ContainerStatus
		DependencyKnownHealthStatus   apicontainerstatus.ContainerHealthStatus
		HealthCheckType               string
		DependencyKnownHealthExitCode int
		DependencyCondition           string
		Resolved                      bool
	}{
		{
			TargetKnown:                 apicontainerstatus.ContainerStatusNone,
			DependencyKnownHealthStatus: apicontainerstatus.ContainerHealthy,
			HealthCheckType:             "docker",
			DependencyCondition:         healthyCondition,
			Resolved:                    true,
		},
		{
			TargetKnown:                   apicontainerstatus.ContainerStatusNone,
			DependencyKnownHealthStatus:   apicontainerstatus.ContainerUnhealthy,
			HealthCheckType:               "docker",
			DependencyKnownHealthExitCode: 1,
			DependencyCondition:           healthyCondition,
			Resolved:                      true,
		},
		{
			TargetKnown:                   apicontainerstatus.ContainerPulled,
			DependencyKnownHealthStatus:   apicontainerstatus.ContainerUnhealthy,
			HealthCheckType:               "docker",
			DependencyKnownHealthExitCode: 1,
			DependencyCondition:           healthyCondition,
			Resolved:                      false,
		},
	}
	cfg := config.Config{
		DependentContainersPullUpfront: config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled},
	}
	for _, tc := range testcases {
		t.Run(fmt.Sprintf("DependencyKnownHealthStatus:%s+T:%s+V:%s", tc.DependencyKnownHealthStatus, tc.TargetKnown.String(), tc.DependencyKnownHealthStatus.String()),
			assertContainerOrderingHealthyConditionResolved(containerOrderingDependenciesIsResolved, tc.TargetDesired, tc.TargetKnown, tc.DependencyKnownHealthStatus, tc.HealthCheckType, tc.DependencyKnownHealthExitCode, tc.DependencyCondition, tc.Resolved, &cfg))
	}
}

func assertContainerOrderingHealthyConditionResolved(f func(target *apicontainer.Container, dep *apicontainer.Container, depCond string, cfg *config.Config) bool, targetDesired apicontainerstatus.ContainerStatus, targetKnown apicontainerstatus.ContainerStatus, depHealthKnown apicontainerstatus.ContainerHealthStatus, healthCheckEnabled string, depHealthKnownExitCode int, depCond string, expectedResolved bool, cfg *config.Config) func(t *testing.T) {
	return func(t *testing.T) {
		target := &apicontainer.Container{
			DesiredStatusUnsafe: targetDesired,
			KnownStatusUnsafe:   targetKnown,
		}
		dep := &apicontainer.Container{
			Health: apicontainer.HealthStatus{
				Status:   depHealthKnown,
				ExitCode: depHealthKnownExitCode,
			},
			HealthCheckType: healthCheckEnabled,
		}
		resolved := f(target, dep, depCond, cfg)
		assert.Equal(t, expectedResolved, resolved)
	}
}

func dependsOn(vals ...string) []apicontainer.DependsOn {
	d := make([]apicontainer.DependsOn, len(vals))
	for i, val := range vals {
		d[i] = apicontainer.DependsOn{ContainerName: val}
	}
	return d
}

// TestVerifyShutdownOrder validates that the shutdown graph works in the inverse order of the startup graph
// This test always uses a running target
func TestVerifyShutdownOrder(t *testing.T) {

	// dependencies aren't transitive, so we can check multiple levels within here
	others := map[string]*apicontainer.Container{
		"A": {
			Name:              "A",
			DependsOnUnsafe:   dependsOn("B"),
			KnownStatusUnsafe: apicontainerstatus.ContainerStopped,
		},
		"B": {
			Name:              "B",
			DependsOnUnsafe:   dependsOn("C", "D"),
			KnownStatusUnsafe: apicontainerstatus.ContainerRunning,
		},
		"C": {
			Name:              "C",
			DependsOnUnsafe:   dependsOn("E", "F"),
			KnownStatusUnsafe: apicontainerstatus.ContainerStopped,
		},
		"D": {
			Name:              "D",
			DependsOnUnsafe:   dependsOn("E"),
			KnownStatusUnsafe: apicontainerstatus.ContainerRunning,
		},
	}

	testCases := []struct {
		TestName      string
		TargetName    string
		ShouldResolve bool
	}{
		{
			TestName:      "Dependency is already stopped",
			TargetName:    "F",
			ShouldResolve: true,
		},
		{
			TestName:      "Running with a running dependency",
			TargetName:    "D",
			ShouldResolve: false,
		},
		{
			TestName:      "One dependency running, one stopped",
			TargetName:    "E",
			ShouldResolve: false,
		},
		{
			TestName:      "No dependencies declared",
			TargetName:    "Z",
			ShouldResolve: true,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.TestName, func(t *testing.T) {
			// create a target container
			target := &apicontainer.Container{
				Name:                tc.TargetName,
				KnownStatusUnsafe:   apicontainerstatus.ContainerRunning,
				DesiredStatusUnsafe: apicontainerstatus.ContainerStopped,
			}

			// Validation
			if tc.ShouldResolve {
				assert.NoError(t, verifyShutdownOrder(target, others))
			} else {
				assert.Error(t, verifyShutdownOrder(target, others))
			}
		})
	}
}

func TestStartTimeoutForContainerOrdering(t *testing.T) {
	testcases := []struct {
		DependencyStartedAt    time.Time
		DependencyStartTimeout uint
		DependencyCondition    string
		ExpectedTimedOut       bool
	}{
		{
			DependencyStartedAt:    time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
			DependencyStartTimeout: 10,
			DependencyCondition:    healthyCondition,
			ExpectedTimedOut:       true,
		},
		{
			DependencyStartedAt:    time.Date(4000, 1, 1, 0, 0, 0, 0, time.UTC),
			DependencyStartTimeout: 10,
			DependencyCondition:    successCondition,
			ExpectedTimedOut:       false,
		},
		{
			DependencyStartedAt:    time.Time{},
			DependencyStartTimeout: 10,
			DependencyCondition:    successCondition,
			ExpectedTimedOut:       false,
		},
	}

	for _, tc := range testcases {
		t.Run(fmt.Sprintf("T:dependency time out: %v", tc.ExpectedTimedOut),
			assertContainerOrderingNotTimedOut(hasDependencyTimedOut, tc.DependencyStartedAt, tc.DependencyStartTimeout, tc.DependencyCondition, tc.ExpectedTimedOut))
	}
}

func assertContainerOrderingNotTimedOut(f func(dep *apicontainer.Container, depCond string) bool, startedAt time.Time, timeout uint, depCond string, expectedTimedOut bool) func(t *testing.T) {
	return func(t *testing.T) {
		dep := &apicontainer.Container{
			Name:         "dep",
			StartTimeout: timeout,
		}

		dep.SetStartedAt(startedAt)

		timedOut := f(dep, depCond)
		assert.Equal(t, expectedTimedOut, timedOut)
	}
}

func TestVerifyContainerOrderingStatusResolvableFailOnDependencyWontStart(t *testing.T) {
	targetName := "target"
	dependencyName := "dependency"
	target := &apicontainer.Container{
		Name:                targetName,
		KnownStatusUnsafe:   apicontainerstatus.ContainerPulled,
		DesiredStatusUnsafe: apicontainerstatus.ContainerCreated,
		DependsOnUnsafe: []apicontainer.DependsOn{
			{
				ContainerName: dependencyName,
			},
		},
	}
	dep := &apicontainer.Container{
		Name:                dependencyName,
		KnownStatusUnsafe:   apicontainerstatus.ContainerPulled,
		DesiredStatusUnsafe: apicontainerstatus.ContainerStopped,
		AppliedStatus:       apicontainerstatus.ContainerStatusNone,
	}
	contMap := map[string]*apicontainer.Container{
		targetName:     target,
		dependencyName: dep,
	}
	dummyResolves := func(*apicontainer.Container, *apicontainer.Container, string, *config.Config) bool {
		return true
	}
	_, err := verifyContainerOrderingStatusResolvable(target, contMap, &config.Config{}, dummyResolves)
	assert.Error(t, err)
}
