package progress

import (
	"fmt"
	"strconv"
	"testing"

	"github.com/docker/docker/api/types/swarm"
	"github.com/docker/docker/pkg/progress"
	"gotest.tools/v3/assert"
	is "gotest.tools/v3/assert/cmp"
)

type mockProgress struct {
	p []progress.Progress
}

func (mp *mockProgress) WriteProgress(p progress.Progress) error {
	mp.p = append(mp.p, p)
	return nil
}

func (mp *mockProgress) clear() {
	mp.p = nil
}

type updaterTester struct {
	t           *testing.T
	updater     progressUpdater
	p           *mockProgress
	service     swarm.Service
	activeNodes map[string]struct{}
	rollback    bool
}

func (u updaterTester) testUpdater(tasks []swarm.Task, expectedConvergence bool, expectedProgress []progress.Progress) {
	u.p.clear()

	converged, err := u.updater.update(u.service, tasks, u.activeNodes, u.rollback)
	assert.Check(u.t, err)
	assert.Check(u.t, is.Equal(expectedConvergence, converged))
	assert.Check(u.t, is.DeepEqual(expectedProgress, u.p.p))
}

func TestReplicatedProgressUpdaterOneReplica(t *testing.T) {
	replicas := uint64(1)

	service := swarm.Service{
		Spec: swarm.ServiceSpec{
			Mode: swarm.ServiceMode{
				Replicated: &swarm.ReplicatedService{
					Replicas: &replicas,
				},
			},
		},
	}

	p := &mockProgress{}
	updaterTester := updaterTester{
		t: t,
		updater: &replicatedProgressUpdater{
			progressOut: p,
		},
		p:           p,
		activeNodes: map[string]struct{}{"a": {}, "b": {}},
		service:     service,
	}

	tasks := []swarm.Task{}

	updaterTester.testUpdater(tasks, false,
		[]progress.Progress{
			{ID: "overall progress", Action: "0 out of 1 tasks"},
			{ID: "1/1", Action: " "},
			{ID: "overall progress", Action: "0 out of 1 tasks"},
		})

	// Task with DesiredState beyond Running is ignored
	tasks = append(tasks,
		swarm.Task{ID: "1",
			NodeID:       "a",
			DesiredState: swarm.TaskStateShutdown,
			Status:       swarm.TaskStatus{State: swarm.TaskStateNew},
		})
	updaterTester.testUpdater(tasks, false,
		[]progress.Progress{
			{ID: "overall progress", Action: "0 out of 1 tasks"},
		})

	// Task with valid DesiredState and State updates progress bar
	tasks[0].DesiredState = swarm.TaskStateRunning
	updaterTester.testUpdater(tasks, false,
		[]progress.Progress{
			{ID: "1/1", Action: "new      ", Current: 1, Total: 9, HideCounts: true},
			{ID: "overall progress", Action: "0 out of 1 tasks"},
		})

	// If the task exposes an error, we should show that instead of the
	// progress bar.
	tasks[0].Status.Err = "something is wrong"
	updaterTester.testUpdater(tasks, false,
		[]progress.Progress{
			{ID: "1/1", Action: "something is wrong"},
			{ID: "overall progress", Action: "0 out of 1 tasks"},
		})

	// When the task reaches running, update should return true
	tasks[0].Status.Err = ""
	tasks[0].Status.State = swarm.TaskStateRunning
	updaterTester.testUpdater(tasks, true,
		[]progress.Progress{
			{ID: "1/1", Action: "running  ", Current: 9, Total: 9, HideCounts: true},
			{ID: "overall progress", Action: "1 out of 1 tasks"},
		})

	// If the task fails, update should return false again
	tasks[0].Status.Err = "task failed"
	tasks[0].Status.State = swarm.TaskStateFailed
	updaterTester.testUpdater(tasks, false,
		[]progress.Progress{
			{ID: "1/1", Action: "task failed"},
			{ID: "overall progress", Action: "0 out of 1 tasks"},
		})

	// If the task is restarted, progress output should be shown for the
	// replacement task, not the old task.
	tasks[0].DesiredState = swarm.TaskStateShutdown
	tasks = append(tasks,
		swarm.Task{ID: "2",
			NodeID:       "b",
			DesiredState: swarm.TaskStateRunning,
			Status:       swarm.TaskStatus{State: swarm.TaskStateRunning},
		})
	updaterTester.testUpdater(tasks, true,
		[]progress.Progress{
			{ID: "1/1", Action: "running  ", Current: 9, Total: 9, HideCounts: true},
			{ID: "overall progress", Action: "1 out of 1 tasks"},
		})

	// Add a new task while the current one is still running, to simulate
	// "start-then-stop" updates.
	tasks = append(tasks,
		swarm.Task{ID: "3",
			NodeID:       "b",
			DesiredState: swarm.TaskStateRunning,
			Status:       swarm.TaskStatus{State: swarm.TaskStatePreparing},
		})
	updaterTester.testUpdater(tasks, false,
		[]progress.Progress{
			{ID: "1/1", Action: "preparing", Current: 6, Total: 9, HideCounts: true},
			{ID: "overall progress", Action: "0 out of 1 tasks"},
		})
}

func TestReplicatedProgressUpdaterManyReplicas(t *testing.T) {
	replicas := uint64(50)

	service := swarm.Service{
		Spec: swarm.ServiceSpec{
			Mode: swarm.ServiceMode{
				Replicated: &swarm.ReplicatedService{
					Replicas: &replicas,
				},
			},
		},
	}

	p := &mockProgress{}
	updaterTester := updaterTester{
		t: t,
		updater: &replicatedProgressUpdater{
			progressOut: p,
		},
		p:           p,
		activeNodes: map[string]struct{}{"a": {}, "b": {}},
		service:     service,
	}

	tasks := []swarm.Task{}

	// No per-task progress bars because there are too many replicas
	updaterTester.testUpdater(tasks, false,
		[]progress.Progress{
			{ID: "overall progress", Action: fmt.Sprintf("0 out of %d tasks", replicas)},
			{ID: "overall progress", Action: fmt.Sprintf("0 out of %d tasks", replicas)},
		})

	for i := 0; i != int(replicas); i++ {
		tasks = append(tasks,
			swarm.Task{
				ID:           strconv.Itoa(i),
				Slot:         i + 1,
				NodeID:       "a",
				DesiredState: swarm.TaskStateRunning,
				Status:       swarm.TaskStatus{State: swarm.TaskStateNew},
			})

		if i%2 == 1 {
			tasks[i].NodeID = "b"
		}
		updaterTester.testUpdater(tasks, false,
			[]progress.Progress{
				{ID: "overall progress", Action: fmt.Sprintf("%d out of %d tasks", i, replicas)},
			})

		tasks[i].Status.State = swarm.TaskStateRunning
		updaterTester.testUpdater(tasks, uint64(i) == replicas-1,
			[]progress.Progress{
				{ID: "overall progress", Action: fmt.Sprintf("%d out of %d tasks", i+1, replicas)},
			})
	}
}

func TestGlobalProgressUpdaterOneNode(t *testing.T) {
	service := swarm.Service{
		Spec: swarm.ServiceSpec{
			Mode: swarm.ServiceMode{
				Global: &swarm.GlobalService{},
			},
		},
	}

	p := &mockProgress{}
	updaterTester := updaterTester{
		t: t,
		updater: &globalProgressUpdater{
			progressOut: p,
		},
		p:           p,
		activeNodes: map[string]struct{}{"a": {}, "b": {}},
		service:     service,
	}

	tasks := []swarm.Task{}

	updaterTester.testUpdater(tasks, false,
		[]progress.Progress{
			{ID: "overall progress", Action: "waiting for new tasks"},
		})

	// Task with DesiredState beyond Running is ignored
	tasks = append(tasks,
		swarm.Task{ID: "1",
			NodeID:       "a",
			DesiredState: swarm.TaskStateShutdown,
			Status:       swarm.TaskStatus{State: swarm.TaskStateNew},
		})
	updaterTester.testUpdater(tasks, false,
		[]progress.Progress{
			{ID: "overall progress", Action: "0 out of 1 tasks"},
			{ID: "overall progress", Action: "0 out of 1 tasks"},
		})

	// Task with valid DesiredState and State updates progress bar
	tasks[0].DesiredState = swarm.TaskStateRunning
	updaterTester.testUpdater(tasks, false,
		[]progress.Progress{
			{ID: "a", Action: "new      ", Current: 1, Total: 9, HideCounts: true},
			{ID: "overall progress", Action: "0 out of 1 tasks"},
		})

	// If the task exposes an error, we should show that instead of the
	// progress bar.
	tasks[0].Status.Err = "something is wrong"
	updaterTester.testUpdater(tasks, false,
		[]progress.Progress{
			{ID: "a", Action: "something is wrong"},
			{ID: "overall progress", Action: "0 out of 1 tasks"},
		})

	// When the task reaches running, update should return true
	tasks[0].Status.Err = ""
	tasks[0].Status.State = swarm.TaskStateRunning
	updaterTester.testUpdater(tasks, true,
		[]progress.Progress{
			{ID: "a", Action: "running  ", Current: 9, Total: 9, HideCounts: true},
			{ID: "overall progress", Action: "1 out of 1 tasks"},
		})

	// If the task fails, update should return false again
	tasks[0].Status.Err = "task failed"
	tasks[0].Status.State = swarm.TaskStateFailed
	updaterTester.testUpdater(tasks, false,
		[]progress.Progress{
			{ID: "a", Action: "task failed"},
			{ID: "overall progress", Action: "0 out of 1 tasks"},
		})

	// If the task is restarted, progress output should be shown for the
	// replacement task, not the old task.
	tasks[0].DesiredState = swarm.TaskStateShutdown
	tasks = append(tasks,
		swarm.Task{ID: "2",
			NodeID:       "a",
			DesiredState: swarm.TaskStateRunning,
			Status:       swarm.TaskStatus{State: swarm.TaskStateRunning},
		})
	updaterTester.testUpdater(tasks, true,
		[]progress.Progress{
			{ID: "a", Action: "running  ", Current: 9, Total: 9, HideCounts: true},
			{ID: "overall progress", Action: "1 out of 1 tasks"},
		})

	// Add a new task while the current one is still running, to simulate
	// "start-then-stop" updates.
	tasks = append(tasks,
		swarm.Task{ID: "3",
			NodeID:       "a",
			DesiredState: swarm.TaskStateRunning,
			Status:       swarm.TaskStatus{State: swarm.TaskStatePreparing},
		})
	updaterTester.testUpdater(tasks, false,
		[]progress.Progress{
			{ID: "a", Action: "preparing", Current: 6, Total: 9, HideCounts: true},
			{ID: "overall progress", Action: "0 out of 1 tasks"},
		})
}

func TestGlobalProgressUpdaterManyNodes(t *testing.T) {
	nodes := 50

	service := swarm.Service{
		Spec: swarm.ServiceSpec{
			Mode: swarm.ServiceMode{
				Global: &swarm.GlobalService{},
			},
		},
	}

	p := &mockProgress{}
	updaterTester := updaterTester{
		t: t,
		updater: &globalProgressUpdater{
			progressOut: p,
		},
		p:           p,
		activeNodes: map[string]struct{}{},
		service:     service,
	}

	for i := 0; i != nodes; i++ {
		updaterTester.activeNodes[strconv.Itoa(i)] = struct{}{}
	}

	tasks := []swarm.Task{}

	updaterTester.testUpdater(tasks, false,
		[]progress.Progress{
			{ID: "overall progress", Action: "waiting for new tasks"},
		})

	for i := 0; i != nodes; i++ {
		tasks = append(tasks,
			swarm.Task{
				ID:           "task" + strconv.Itoa(i),
				NodeID:       strconv.Itoa(i),
				DesiredState: swarm.TaskStateRunning,
				Status:       swarm.TaskStatus{State: swarm.TaskStateNew},
			})
	}

	updaterTester.testUpdater(tasks, false,
		[]progress.Progress{
			{ID: "overall progress", Action: fmt.Sprintf("0 out of %d tasks", nodes)},
			{ID: "overall progress", Action: fmt.Sprintf("0 out of %d tasks", nodes)},
		})

	for i := 0; i != nodes; i++ {
		tasks[i].Status.State = swarm.TaskStateRunning
		updaterTester.testUpdater(tasks, i == nodes-1,
			[]progress.Progress{
				{ID: "overall progress", Action: fmt.Sprintf("%d out of %d tasks", i+1, nodes)},
			})
	}
}
