//go:build unit || all
// +build unit all

package eks

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

	"github.com/crossplane-contrib/provider-aws/apis/iam/v1beta1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/onsi/gomega"
	v1 "k8s.io/api/core/v1"
	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/eks-provider/controllers/imports/task"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/config"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/scheme"
)

func TestImportNodeRolePostCreate_IAMReadinessAndCooldown(t *testing.T) {
	g := gomega.NewWithT(t)

	roleName := "import-us-123"

	fixedNow := time.Now()

	makeRole := func(arn string, readyTime time.Time) *v1beta1.Role {
		r := &v1beta1.Role{
			ObjectMeta: metav1.ObjectMeta{Name: roleName},
		}
		r.Status.AtProvider.ARN = arn
		r.Status.Conditions = []xpv1.Condition{{
			Type:               xpv1.TypeReady,
			Status:             v1.ConditionTrue,
			LastTransitionTime: metav1.NewTime(readyTime),
		}}
		return r
	}

	makeAttachment := func(name string, readyTime time.Time) *v1beta1.RolePolicyAttachment {
		r := &v1beta1.RolePolicyAttachment{
			ObjectMeta: metav1.ObjectMeta{Name: name, Labels: map[string]string{RoleRefLabelKey: roleName}},
		}
		r.Status.AtProvider.AttachedPolicyARN = "arn:policy"
		r.Status.Conditions = []xpv1.Condition{{
			Type:               xpv1.TypeReady,
			Status:             v1.ConditionTrue,
			LastTransitionTime: metav1.NewTime(readyTime),
		}}
		return r
	}
	makeAttachmentWithoutTransition := func(name string) *v1beta1.RolePolicyAttachment {
		r := &v1beta1.RolePolicyAttachment{
			ObjectMeta: metav1.ObjectMeta{Name: name, Labels: map[string]string{RoleRefLabelKey: roleName}},
		}
		r.Status.AtProvider.AttachedPolicyARN = "arn:policy"
		r.Status.Conditions = []xpv1.Condition{{
			Type:   xpv1.TypeReady,
			Status: v1.ConditionTrue,
		}}
		return r
	}

	// case 1: one attachment not ready -> retry
	{
		role := makeRole("arn:role", fixedNow.Add(-2*time.Minute))
		// attachments: 1 not ready
		attachments := []client.Object{}
		for i := 0; i < expectedRoleAttachmentCount; i++ {
			name := policyAttachmentName(roleName, "arn:policy") + "-" + strconv.Itoa(i)
			if i == 0 {
				// create an unready attachment (no Ready condition)
				a := &v1beta1.RolePolicyAttachment{
					ObjectMeta: metav1.ObjectMeta{Name: name, Labels: map[string]string{RoleRefLabelKey: roleName}},
					Status:     v1beta1.RolePolicyAttachmentStatus{},
				}
				attachments = append(attachments, a)
			} else {
				a := makeAttachment(name, fixedNow.Add(-2*time.Minute))
				attachments = append(attachments, a)
			}
		}

		objs := []client.Object{role}
		for _, o := range attachments {
			objs = append(objs, o)
		}

		cl := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(objs...).Build()

		ctx := &task.Context{Local: cl, ProjectID: "123", Config: config.RootConfig{ControllerConfig: config.ControllerConfig{Network: config.NetworkConfig{Region: "us"}}}, Import: &cloudv1.Import{}}
		nodeRole := NewNodeRole(ctx).(*NodeRole)
		nodeRole.Now = func() time.Time { return fixedNow }
		res := nodeRole.PostCreate(context.Background())
		g.Expect(res.RawError()).NotTo(gomega.BeNil())
		g.Expect(k8s.ShouldRequeue(res.RawError()).RequeueAfter).To(gomega.Equal(iamReadyRequeueInterval))
	}

	// case 2: all ready but within 1 minute -> retry
	{
		role := makeRole("arn:role", fixedNow.Add(-30*time.Second))
		attachments := make([]client.Object, 0, expectedRoleAttachmentCount)
		for i := 0; i < expectedRoleAttachmentCount; i++ {
			name := policyAttachmentName(roleName, "arn:policy") + "-" + strconv.Itoa(i)
			a := makeAttachment(name, fixedNow.Add(-30*time.Second))
			attachments = append(attachments, a)
		}
		objs := []client.Object{role}
		for _, o := range attachments {
			objs = append(objs, o)
		}
		cl := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(objs...).Build()
		ctx := &task.Context{Local: cl, ProjectID: "123", Config: config.RootConfig{ControllerConfig: config.ControllerConfig{Network: config.NetworkConfig{Region: "us"}}}, Import: &cloudv1.Import{}}
		nodeRole := NewNodeRole(ctx).(*NodeRole)
		nodeRole.Now = func() time.Time { return fixedNow }
		res := nodeRole.PostCreate(context.Background())
		g.Expect(res.RawError()).NotTo(gomega.BeNil())
		g.Expect(k8s.ShouldRequeue(res.RawError()).RequeueAfter).To(gomega.Equal(iamReadyRequeueInterval))
	}

	// case 3: role ARN not populated yet -> retry
	{
		role := makeRole("", fixedNow.Add(-2*time.Minute))
		attachments := make([]client.Object, 0, expectedRoleAttachmentCount)
		for i := 0; i < expectedRoleAttachmentCount; i++ {
			name := policyAttachmentName(roleName, "arn:policy") + "-" + strconv.Itoa(i)
			a := makeAttachment(name, fixedNow.Add(-2*time.Minute))
			attachments = append(attachments, a)
		}
		objs := []client.Object{role}
		for _, o := range attachments {
			objs = append(objs, o)
		}
		cl := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(objs...).Build()
		ctx := &task.Context{Local: cl, ProjectID: "123", Config: config.RootConfig{ControllerConfig: config.ControllerConfig{Network: config.NetworkConfig{Region: "us"}}}, Import: &cloudv1.Import{}}
		nodeRole := NewNodeRole(ctx).(*NodeRole)
		nodeRole.Now = func() time.Time { return fixedNow }
		res := nodeRole.PostCreate(context.Background())
		g.Expect(res.RawError()).NotTo(gomega.BeNil())
		g.Expect(k8s.ShouldRequeue(res.RawError()).RequeueAfter).To(gomega.Equal(iamReadyRequeueInterval))
	}

	// case 4: transition time not populated yet -> retry
	{
		role := makeRole("arn:role", fixedNow.Add(-2*time.Minute))
		attachments := make([]client.Object, 0, expectedRoleAttachmentCount)
		for i := 0; i < expectedRoleAttachmentCount; i++ {
			name := policyAttachmentName(roleName, "arn:policy") + "-" + strconv.Itoa(i)
			a := makeAttachmentWithoutTransition(name)
			attachments = append(attachments, a)
		}
		objs := []client.Object{role}
		for _, o := range attachments {
			objs = append(objs, o)
		}
		cl := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(objs...).Build()
		ctx := &task.Context{Local: cl, ProjectID: "123", Config: config.RootConfig{ControllerConfig: config.ControllerConfig{Network: config.NetworkConfig{Region: "us"}}}, Import: &cloudv1.Import{}}
		nodeRole := NewNodeRole(ctx).(*NodeRole)
		nodeRole.Now = func() time.Time { return fixedNow }
		res := nodeRole.PostCreate(context.Background())
		g.Expect(res.RawError()).NotTo(gomega.BeNil())
		g.Expect(k8s.ShouldRequeue(res.RawError()).RequeueAfter).To(gomega.Equal(iamReadyRequeueInterval))
	}

	// case 5: all ready and beyond 1 minute -> done + ARN set
	{
		role := makeRole("arn:role", fixedNow.Add(-2*time.Minute))
		attachments := make([]client.Object, 0, expectedRoleAttachmentCount)
		for i := 0; i < expectedRoleAttachmentCount; i++ {
			name := policyAttachmentName(roleName, "arn:policy") + "-" + strconv.Itoa(i)
			a := makeAttachment(name, fixedNow.Add(-2*time.Minute))
			attachments = append(attachments, a)
		}
		objs := []client.Object{role}
		for _, o := range attachments {
			objs = append(objs, o)
		}
		cl := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(objs...).Build()
		imp := &cloudv1.Import{}
		ctx := &task.Context{Local: cl, ProjectID: "123", Config: config.RootConfig{ControllerConfig: config.ControllerConfig{Network: config.NetworkConfig{Region: "us"}}}, Import: imp}
		nodeRole := NewNodeRole(ctx).(*NodeRole)
		nodeRole.Now = func() time.Time { return fixedNow }
		res := nodeRole.PostCreate(context.Background())
		g.Expect(res.RawError()).To(gomega.BeNil())
		g.Expect(imp.Status.AWSNodeRoleARN).To(gomega.Equal("arn:role"))
	}
}
