package deploy

import (
	"errors"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	ctrl "sigs.k8s.io/controller-runtime"
)

func TestResult(t *testing.T) {
	err := errors.New("test error")

	t.Run("error method", func(t *testing.T) {
		assert.Nil(t, Done().Error())
		for _, r := range []Result{Error(err), Panic(err)} {
			assert.Error(t, r.Error())
			assert.Contains(t, r.Error().Error(), "test error")
		}
	})

	t.Run("is done state", func(t *testing.T) {
		assert.True(t, Done().IsDoneState())
		for _, r := range []Result{Error(err), Panic(err)} {
			assert.False(t, r.IsDoneState())
		}
	})

	t.Run("with requeue after", func(t *testing.T) {
		d := 5 * time.Minute
		r := Error(errors.New("retry-me")).WithRequeueAfter(d)
		assert.NotNil(t, r.err)
		assert.Equal(t, d, ShouldRequeue(r.err).RequeueAfter)
	})

	t.Run("with name", func(t *testing.T) {
		assert.Equal(t, "my-task", Done().WithName("my-task").task)
	})

	t.Run("panic or done", func(t *testing.T) {
		assert.Equal(t, stateDone, PanicOrDone(nil).state)
		r := PanicOrDone(errors.New("boom"))
		assert.Equal(t, statePanic, r.state)
		assert.Error(t, r.err)
	})

	t.Run("error or done", func(t *testing.T) {
		assert.Equal(t, stateDone, ErrorOrDone(nil).state)
		r := ErrorOrDone(errors.New("transient"))
		assert.Equal(t, stateError, r.state)
		assert.Error(t, r.err)
	})
}

func TestMinResult(t *testing.T) {
	tests := []struct {
		name    string
		lhs     ctrl.Result
		rhs     ctrl.Result
		wantRe  bool
		wantDur time.Duration
	}{
		{
			name:   "both zero returns zero",
			lhs:    ctrl.Result{},
			rhs:    ctrl.Result{},
			wantRe: false,
		},
		{
			name:   "lhs has requeue rhs zero returns lhs",
			lhs:    ctrl.Result{Requeue: true},
			rhs:    ctrl.Result{},
			wantRe: true,
		},
		{
			name:    "rhs has requeue after lhs zero returns rhs",
			lhs:     ctrl.Result{},
			rhs:     ctrl.Result{RequeueAfter: 3 * time.Minute},
			wantRe:  true,
			wantDur: 3 * time.Minute,
		},
		{
			name:    "both have requeue after picks smaller",
			lhs:     ctrl.Result{RequeueAfter: 5 * time.Minute},
			rhs:     ctrl.Result{RequeueAfter: 2 * time.Minute},
			wantRe:  true,
			wantDur: 2 * time.Minute,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := minResult(tt.lhs, tt.rhs)
			assert.Equal(t, tt.wantRe, got.Requeue)
			if tt.wantDur > 0 {
				assert.Equal(t, tt.wantDur, got.RequeueAfter)
			}
		})
	}
}

func TestHasPanic(t *testing.T) {
	errBoom := errors.New("boom")
	tests := []struct {
		name    string
		results []Result
		want    bool
	}{
		{
			name:    "no panic returns false",
			results: []Result{Done(), Error(errBoom).WithName("t1")},
			want:    false,
		},
		{
			name:    "panic at index 0 returns true",
			results: []Result{Panic(errBoom).WithName("first")},
			want:    true,
		},
		{
			name: "panic after non-panic results returns true",
			results: []Result{
				Done().WithName("t0"),
				Error(errBoom).WithName("t1"),
				Panic(errBoom).WithName("t2"),
			},
			want: true,
		},
		{
			name:    "empty results returns false",
			results: nil,
			want:    false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.want, HasPanic(tt.results))
		})
	}
}

func TestMinRequeue(t *testing.T) {
	tests := []struct {
		name              string
		results           []Result
		wantErrs          bool
		wantRetriableErrs bool
		wantRequeue       bool
	}{
		{
			name:     "all done returns zero result and no errors",
			results:  []Result{Done(), Done(), Done()},
			wantErrs: false,
		},
		{
			name: "mix of done and error",
			results: []Result{
				Done(),
				Error(errors.New("fail")).WithName("t1"),
				Done(),
			},
			wantErrs: true,
		},
		{
			name: "requeue-only errors excluded from error list",
			results: []Result{
				{state: stateError, err: NewRetriableError(ErrorRequeueOnly, ctrl.Result{Requeue: true}), task: "t1"},
			},
			wantErrs:          false,
			wantRetriableErrs: false,
			wantRequeue:       true,
		},
		{
			name: "retriable errors separated from regular errors",
			results: []Result{
				Error(errors.New("regular")).WithName("t1"),
				{state: stateError, err: NewRetriableError(errors.New("transient"), ctrl.Result{RequeueAfter: 3 * time.Minute}), task: "t2"},
			},
			wantErrs:          true,
			wantRetriableErrs: true,
			wantRequeue:       true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			res, errs, retriableErrs := MinRequeue(tt.results)
			if !tt.wantErrs {
				assert.NoError(t, errs)
			} else {
				assert.Error(t, errs)
			}
			if !tt.wantRetriableErrs {
				assert.NoError(t, retriableErrs)
			} else {
				assert.Error(t, retriableErrs)
			}
			assert.Equal(t, tt.wantRequeue, res.Requeue)
		})
	}
}
