package deploy

import (
	"context"
	"testing"

	"github.com/go-logr/logr"
	"github.com/stretchr/testify/assert"
)

type mockTask struct {
	name       string
	meta       ClusterMeta
	calls      *[]string
	create     Result
	postCreate Result
	delete     Result
	postDelete Result
}

func (t *mockTask) Name() string {
	return t.name
}

func (t *mockTask) ClusterMeta() ClusterMeta {
	return t.meta
}

func (t *mockTask) Create(context.Context) Result {
	*t.calls = append(*t.calls, t.name+":create")
	return t.create
}

func (t *mockTask) PostCreate(context.Context) Result {
	*t.calls = append(*t.calls, t.name+":postCreate-create")
	return t.postCreate
}

func (t *mockTask) Delete(context.Context) Result {
	*t.calls = append(*t.calls, t.name+":delete")
	return t.delete
}

func (t *mockTask) PostDelete(context.Context) Result {
	*t.calls = append(*t.calls, t.name+":postCreate-delete")
	return t.postDelete
}

func TestDeployerDeployFlow(t *testing.T) {
	tests := []struct {
		name              string
		buildTasks        func(*[]string) []Task
		expectCalls       []string
		expectResultCount int
		expectStepTypes   []stepType
	}{
		{
			name: "success - all tasks complete",
			buildTasks: func(calls *[]string) []Task {
				t1 := &mockTask{
					name:       "t1",
					calls:      calls,
					meta:       ClusterMeta{Agent: "a"},
					create:     Done(),
					postCreate: Done(),
				}
				t2 := &mockTask{
					name:       "t2",
					calls:      calls,
					meta:       ClusterMeta{Agent: "a"},
					create:     Done(),
					postCreate: Done(),
				}
				t3 := &mockTask{
					name:       "t3",
					calls:      calls,
					meta:       ClusterMeta{Agent: "a"},
					create:     Done(),
					postCreate: Done(),
				}
				return []Task{t1, t2, t3}
			},
			expectCalls: []string{
				"t1:create",
				"t1:postCreate-create",
				"t2:create",
				"t2:postCreate-create",
				"t3:create",
				"t3:postCreate-create",
			},
			expectResultCount: 6,
			expectStepTypes:   []stepType{stepCreate, stepPostCreate, stepCreate, stepPostCreate, stepCreate, stepPostCreate},
		},
		{
			name: "error state triggers post-create and continues",
			buildTasks: func(calls *[]string) []Task {
				t1 := &mockTask{
					name:       "t1",
					calls:      calls,
					meta:       ClusterMeta{Agent: "a"},
					create:     Error(assert.AnError),
					postCreate: Done(),
				}
				t2 := &mockTask{
					name:       "t2",
					calls:      calls,
					meta:       ClusterMeta{Agent: "a"},
					create:     Done(),
					postCreate: Done(),
				}
				return []Task{t1, t2}
			},
			expectCalls: []string{
				"t1:create",
				"t1:postCreate-create",
				"t2:create",
				"t2:postCreate-create",
			},
			expectResultCount: 4,
			expectStepTypes:   []stepType{stepCreate, stepPostCreate, stepCreate, stepPostCreate},
		},
		{
			name: "stop deploy on panic",
			buildTasks: func(calls *[]string) []Task {
				t1 := &mockTask{
					name:       "t1",
					calls:      calls,
					meta:       ClusterMeta{Agent: "a"},
					create:     Done(),
					postCreate: Done(),
				}
				t2 := &mockTask{
					name:       "t2",
					calls:      calls,
					meta:       ClusterMeta{Agent: "a"},
					create:     Panic(assert.AnError),
					postCreate: Done(),
				}
				t3 := &mockTask{
					name:       "t3",
					calls:      calls,
					meta:       ClusterMeta{Agent: "a"},
					create:     Done(),
					postCreate: Done(),
				}
				return []Task{t1, t2, t3}
			},
			expectCalls: []string{
				"t1:create",
				"t1:postCreate-create",
				"t2:create",
			},
			expectResultCount: 3,
			expectStepTypes:   []stepType{stepCreate, stepPostCreate, stepCreate},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var calls []string
			d := NewDeployer(tt.buildTasks(&calls), nil, logr.Discard())
			results := d.Deploy(context.Background())

			assert.Equal(t, tt.expectCalls, calls)
			assert.Len(t, results, tt.expectResultCount)
			for idx, step := range tt.expectStepTypes {
				assert.Equal(t, step, results[idx].stepType)
			}
		})
	}
}

func TestDeployerDestroyFlow(t *testing.T) {
	tests := []struct {
		name              string
		buildTasks        func(*[]string) []Task
		ignoreDelete      []string
		expectCalls       []string
		expectResultCount int
		expectStepTypes   []stepType
	}{
		{
			name: "success - all tasks destroyed",
			buildTasks: func(calls *[]string) []Task {
				guardian := &mockTask{
					name:       "guardian",
					calls:      calls,
					meta:       ClusterMeta{Agent: "a"},
					delete:     Done(),
					postDelete: Done(),
				}
				t1 := &mockTask{
					name:       "t1",
					calls:      calls,
					meta:       ClusterMeta{Agent: "a"},
					delete:     Done(),
					postDelete: Done(),
				}
				t2 := &mockTask{
					name:       "t2",
					calls:      calls,
					meta:       ClusterMeta{Agent: "a"},
					delete:     Done(),
					postDelete: Done(),
				}
				return []Task{guardian, t1, t2}
			},
			ignoreDelete: nil,
			expectCalls: []string{
				"guardian:delete",
				"guardian:postCreate-delete",
				"t2:delete",
				"t2:postCreate-delete",
				"t1:delete",
				"t1:postCreate-delete",
			},
			expectResultCount: 6,
			expectStepTypes:   []stepType{stepDelete, stepPostDelete, stepDelete, stepPostDelete, stepDelete, stepPostDelete},
		},
		{
			name: "destroy respects guardian order and ignore list",
			buildTasks: func(calls *[]string) []Task {
				guardian := &mockTask{
					name:       "guardian",
					calls:      calls,
					meta:       ClusterMeta{Agent: "a"},
					delete:     Done(),
					postDelete: Done(),
				}
				t1 := &mockTask{
					name:       "t1",
					calls:      calls,
					meta:       ClusterMeta{Agent: "a"},
					delete:     Done(),
					postDelete: Done(),
				}
				t2 := &mockTask{
					name:       "t2",
					calls:      calls,
					meta:       ClusterMeta{Agent: "a"},
					delete:     Error(assert.AnError),
					postDelete: Done(),
				}
				return []Task{guardian, t1, t2}
			},
			ignoreDelete: []string{"t1"},
			expectCalls: []string{
				"guardian:delete",
				"guardian:postCreate-delete",
				"t2:delete",
				"t2:postCreate-delete",
			},
			expectResultCount: 4,
			expectStepTypes:   []stepType{stepDelete, stepPostDelete},
		},
		{
			name: "panic during delete stops destroy",
			buildTasks: func(calls *[]string) []Task {
				guardian := &mockTask{
					name:   "guardian",
					calls:  calls,
					meta:   ClusterMeta{Agent: "a"},
					delete: Panic(assert.AnError),
				}
				t1 := &mockTask{
					name:       "t1",
					calls:      calls,
					meta:       ClusterMeta{Agent: "a"},
					delete:     Done(),
					postDelete: Done(),
				}
				t2 := &mockTask{
					name:       "t2",
					calls:      calls,
					meta:       ClusterMeta{Agent: "a"},
					delete:     Done(),
					postDelete: Done(),
				}
				return []Task{guardian, t1, t2}
			},
			expectCalls: []string{
				"guardian:delete",
			},
			expectResultCount: 1,
			expectStepTypes:   []stepType{stepDelete},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var calls []string
			d := NewDeployer(tt.buildTasks(&calls), tt.ignoreDelete, logr.Discard())
			results := d.Destroy(context.Background())

			assert.Equal(t, tt.expectCalls, calls)
			assert.Len(t, results, tt.expectResultCount)
			for idx, step := range tt.expectStepTypes {
				assert.Equal(t, step, results[idx].stepType)
			}
		})
	}
}
