package ack

import (
	"context"
	"strconv"
	"testing"
	"time"

	ackv1alpha1 "github.com/crossplane-contrib/provider-upjet-alibabacloud/apis/ack/v1alpha1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/go-logr/logr"
	. "github.com/onsi/gomega"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/consts"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/appbox/task"
)

func TestIsRecoverableSyncError(t *testing.T) {
	syncedReconcileError := func(message string) xpv1.Condition {
		return xpv1.Condition{
			Type:    xpv1.TypeSynced,
			Status:  corev1.ConditionFalse,
			Reason:  xpv1.ReasonReconcileError,
			Message: message,
		}
	}

	syncedOtherFailure := func(message string) xpv1.Condition {
		return xpv1.Condition{
			Type:    xpv1.TypeSynced,
			Status:  corev1.ConditionFalse,
			Reason:  "SomeOtherReason",
			Message: message,
		}
	}

	lastAsyncApplyFailure := func(message string) xpv1.Condition {
		return xpv1.Condition{
			Type:    "LastAsyncOperation",
			Status:  corev1.ConditionFalse,
			Reason:  "ApplyFailure",
			Message: message,
		}
	}

	lastAsyncOtherFailure := func(message string) xpv1.Condition {
		return xpv1.Condition{
			Type:    "LastAsyncOperation",
			Status:  corev1.ConditionFalse,
			Reason:  "SomeOtherReason",
			Message: message,
		}
	}

	tests := []struct {
		name       string
		conditions []xpv1.Condition
		want       bool
	}{
		{
			name:       "nil conditions",
			conditions: nil,
			want:       false,
		},
		{
			name:       "empty conditions",
			conditions: []xpv1.Condition{},
			want:       false,
		},
		{
			name:       "last async op failed but synced not failed",
			conditions: []xpv1.Condition{lastAsyncApplyFailure("state is updating")},
			want:       false,
		},
		{
			name:       "synced failed but last async op not failed",
			conditions: []xpv1.Condition{syncedReconcileError("boom")},
			want:       false,
		},
		{
			name:       "synced failed but last async reason not apply failure",
			conditions: []xpv1.Condition{syncedReconcileError("boom"), lastAsyncOtherFailure("state is updating")},
			want:       false,
		},
		{
			name:       "synced reason not reconcile error",
			conditions: []xpv1.Condition{syncedOtherFailure("boom"), lastAsyncApplyFailure("state is updating")},
			want:       false,
		},
		{
			name:       "synced and last async failed but message not recoverable",
			conditions: []xpv1.Condition{syncedReconcileError("boom"), lastAsyncApplyFailure("some other error")},
			want:       false,
		},
		{
			name:       "recoverable pattern: state is updating",
			conditions: []xpv1.Condition{syncedReconcileError("boom"), lastAsyncApplyFailure("cluster state is updating, try later")},
			want:       true,
		},
		{
			name:       "recoverable pattern: InvalidClusterStatus.Forbidden",
			conditions: []xpv1.Condition{syncedReconcileError("boom"), lastAsyncApplyFailure("InvalidClusterStatus.Forbidden: cluster status is updating")},
			want:       true,
		},
		{
			name:       "recoverable pattern is case-insensitive",
			conditions: []xpv1.Condition{syncedReconcileError("boom"), lastAsyncApplyFailure("STATE IS UPDATING")},
			want:       true,
		},
		{
			name:       "synced message matches pattern but last async message does not",
			conditions: []xpv1.Condition{syncedReconcileError("state is updating"), lastAsyncApplyFailure("some other error")},
			want:       false,
		},
		{
			name:       "empty last async message is not recoverable",
			conditions: []xpv1.Condition{syncedReconcileError("boom"), lastAsyncApplyFailure("")},
			want:       false,
		},
		{
			name: "multiple last async conditions uses first match only",
			conditions: []xpv1.Condition{
				syncedReconcileError("boom"),
				lastAsyncApplyFailure("some other error"),
				lastAsyncApplyFailure("state is updating"),
			},
			want: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			g := NewWithT(t)

			got := isRecoverableSyncError(tt.conditions)
			g.Expect(got).To(Equal(tt.want))
		})
	}
}

func TestHandleAddonSyncFailure(t *testing.T) {
	addonName := "test-addon"
	attemptKey := consts.AnnoKeyACKAddonRecoveryAttempt(addonName)
	timestampKey := consts.AnnoKeyACKAddonRecoveryTimestamp(addonName)

	newAddon := func(name string) *ackv1alpha1.KubernetesAddon {
		return &ackv1alpha1.KubernetesAddon{
			ObjectMeta: metav1.ObjectMeta{
				Name: name,
			},
		}
	}

	tests := []struct {
		name        string
		annotations map[string]string
		wantErr     bool
		wantDeleted bool
		validate    func(*WithT, map[string]string)
	}{
		{
			name: "max attempts reached",
			annotations: map[string]string{
				attemptKey: strconv.Itoa(maxRecoveryAttempts),
			},
			wantErr:     true,
			wantDeleted: false,
		},
		{
			name: "backoff not elapsed",
			annotations: map[string]string{
				attemptKey:   "1",
				timestampKey: strconv.FormatInt(time.Now().Unix(), 10),
			},
			wantErr:     true,
			wantDeleted: false,
		},
		{
			name:        "delete addon and update annotations",
			annotations: nil,
			wantErr:     false,
			wantDeleted: true,
			validate: func(g *WithT, anns map[string]string) {
				g.Expect(anns).NotTo(BeNil())
				g.Expect(anns[attemptKey]).To(Equal("1"))
				ts, err := strconv.ParseInt(anns[timestampKey], 10, 64)
				g.Expect(err).To(BeNil())
				g.Expect(ts).To(BeNumerically(">", int64(0)))
			},
		},
		{
			name: "invalid attempts disables recovery",
			annotations: map[string]string{
				attemptKey: "abc",
			},
			wantErr:     true,
			wantDeleted: false,
		},
		{
			name: "invalid timestamp enforces backoff wait",
			annotations: map[string]string{
				attemptKey:   "0",
				timestampKey: "abc",
			},
			wantErr:     true,
			wantDeleted: false,
			validate: func(g *WithT, anns map[string]string) {
				g.Expect(anns[attemptKey]).To(Equal("0"))
				g.Expect(anns[timestampKey]).To(Equal("abc"))
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			g := NewWithT(t)
			ctx := context.Background()

			addon := newAddon("test-addon-cr")
			cli := fake.NewClientBuilder().WithScheme(buildTestScheme()).WithObjects(addon).Build()

			annotations := make(map[string]string)
			for k, v := range tt.annotations {
				annotations[k] = v
			}
			if tt.annotations == nil {
				annotations = nil
			}

			taskCtx := &task.Context{
				Local:  cli,
				Logger: logr.Discard(),
				APPBox: &v1alpha1.APPBox{
					ObjectMeta: metav1.ObjectMeta{
						Annotations: annotations,
					},
				},
			}
			addonsTask := &addons{BaseTask: &task.BaseTask{Context: taskCtx}}

			err := addonsTask.handleAddonSyncFailure(ctx, addon, addonName)
			g.Expect(err != nil).To(Equal(tt.wantErr))

			gotAddon := &ackv1alpha1.KubernetesAddon{ObjectMeta: metav1.ObjectMeta{Name: addon.GetName()}}
			getErr := cli.Get(ctx, client.ObjectKeyFromObject(gotAddon), gotAddon)
			if tt.wantDeleted {
				g.Expect(apierrors.IsNotFound(getErr)).To(BeTrue())
			} else {
				g.Expect(getErr).To(BeNil())
			}

			if tt.validate != nil {
				tt.validate(g, taskCtx.APPBox.Annotations)
			}
		})
	}
}
