package common

import (
	"context"
	"errors"
	"fmt"
	"testing"
	"time"

	"github.com/go-logr/logr/testr"
	"github.com/google/go-cmp/cmp"
	. "github.com/onsi/gomega"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	"github.com/tidbcloud/eks-provider/api/v1alpha1"
	"github.com/tidbcloud/eks-provider/controllers/network/task"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/account"
	"github.com/tidbcloud/infra-provider/pkg/account/schedule/plugin"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
)

func TestCreate(t *testing.T) {
	cases := map[string]struct {
		accountCount int
		input        func(*fakeScheduler, *cloudv1.Network, []*cloudv1.Account)
		expect       func(*WithT, *cloudv1.Network, deploy.Result)
	}{
		"skip schedule if accountRef is not empty": {
			accountCount: 3,
			input: func(s *fakeScheduler, network *cloudv1.Network, accts []*cloudv1.Account) {
				network.Spec.AccountRef = &cloudv1.AccountReference{
					Name: "account-1",
				}
				s.setScheduleResult("", errors.New("should not be called"))
			},
			expect: func(g *WithT, network *cloudv1.Network, result deploy.Result) {
				g.Expect(result).Should(Equal(deploy.Done()))
				g.Expect(network.Spec.AccountRef.Name).Should(Equal("account-1"))
			},
		},
		"panic if unsupported provider": {
			accountCount: 3,
			input: func(s *fakeScheduler, network *cloudv1.Network, accts []*cloudv1.Account) {
				network.Spec.Provider = "unsupported"
				network.Spec.AccountRef = &cloudv1.AccountReference{
					Name: "",
				}
				s.setScheduleResult("", errors.New("should not be called"))
			},
			expect: func(g *WithT, network *cloudv1.Network, result deploy.Result) {
				g.Expect(result).Should(Equal(deploy.Panic(fmt.Errorf("not support provider %s", network.Spec.Provider))))
				g.Expect(network.Spec.AccountRef.Name).Should(Equal(""))
			},
		},
		"filter accounts that are other provider": {
			accountCount: 3,
			input: func(s *fakeScheduler, network *cloudv1.Network, accts []*cloudv1.Account) {
				network.Spec.Provider = consts.ProviderTypeGKE
				network.Spec.AccountRef = &cloudv1.AccountReference{
					Name: "",
				}
				accts[0].Spec.Provider = cloudv1.CloudProviderAWS
				accts[1].Spec.Provider = cloudv1.CloudProviderGCP // only account-1 support
				accts[2].Spec.Provider = cloudv1.CloudProviderAWS
				s.schedFunc = func(sctx *plugin.SchedContext, n *cloudv1.Network, a []*cloudv1.Account) (string, error) {
					if len(a) != 1 {
						return "", fmt.Errorf("should only have 1 account")
					}
					if a[0].Spec.Provider != "gcp" {
						return "", fmt.Errorf("should only have gcp account")
					}
					return a[0].Name, nil
				}
			},
			expect: func(g *WithT, network *cloudv1.Network, result deploy.Result) {
				g.Expect(result).Should(Equal(deploy.Panic(fmt.Errorf("wait for the network to be updated in Create")).WithRequeuAfter(time.Millisecond)))
				g.Expect(network.Spec.AccountRef.Name).Should(Equal("account-1"))
			},
		},
		"panic if schedule failed": {
			accountCount: 3,
			input: func(s *fakeScheduler, network *cloudv1.Network, accts []*cloudv1.Account) {
				network.Spec.Provider = consts.ProviderTypeEKS
				network.Spec.AccountRef = &cloudv1.AccountReference{
					Name: "",
				}
				s.setScheduleResult("", errors.New("failed by fake scheduler"))
			},
			expect: func(g *WithT, network *cloudv1.Network, result deploy.Result) {
				g.Expect(result).Should(Equal(deploy.Panic(fmt.Errorf("schedule account failed: failed by fake scheduler"))))
				g.Expect(network.Spec.AccountRef.Name).Should(Equal(""))
			},
		},
		"panic if unschedulable": {
			accountCount: 3,
			input: func(s *fakeScheduler, network *cloudv1.Network, accts []*cloudv1.Account) {
				network.Spec.Provider = consts.ProviderTypeEKS
				network.Spec.AccountRef = &cloudv1.AccountReference{
					Name: "",
				}
				s.setScheduleResult("", nil)
			},
			expect: func(g *WithT, network *cloudv1.Network, result deploy.Result) {
				g.Expect(result).Should(Equal(deploy.Panic(fmt.Errorf("selected account is empty"))))
				g.Expect(network.Spec.AccountRef.Name).Should(Equal(""))
			},
		},
		"schedule account successfully": {
			accountCount: 3,
			input: func(s *fakeScheduler, network *cloudv1.Network, accts []*cloudv1.Account) {
				network.Spec.Provider = consts.ProviderTypeEKS
				network.Spec.AccountRef = &cloudv1.AccountReference{
					Name: "",
				}
				s.setScheduleResult("account-1", nil)
			},
			expect: func(g *WithT, network *cloudv1.Network, result deploy.Result) {
				g.Expect(result).Should(Equal(deploy.Panic(fmt.Errorf("wait for the network to be updated in Create")).WithRequeuAfter(time.Millisecond)))
				g.Expect(network.Spec.AccountRef.Name).Should(Equal("account-1"))
			},
		},
	}

	for name, c := range cases {
		t.Run(name, func(t *testing.T) {
			g := NewWithT(t)

			network := &cloudv1.Network{}
			network.Name = "test-network"
			network.Namespace = "test-ns"
			accounts := accounts(c.accountCount)
			sched := &fakeScheduler{}

			c.input(sched, network, accounts)

			s := runtime.NewScheme()
			g.Expect(scheme.AddToScheme(s)).Should(Succeed())
			g.Expect(cloudv1.SchemeBuilder.AddToScheme(s)).Should(Succeed())
			cli := fake.NewClientBuilder().WithScheme(s).Build()
			task := &Account{
				BaseTask: &task.BaseTask{
					Context: &task.Context{
						Network: network,
						Local:   cli,
						Logger:  testr.New(t),
					},
				},
				sched: sched,
			}

			if len(accounts) != 0 {
				for _, acct := range accounts {
					g.Expect(cli.Create(context.TODO(), acct.DeepCopy())).Should(Succeed())
				}
			}

			result := task.Create(context.Background())
			c.expect(g, network, result)
		})
	}
}

func TestPostCreate(t *testing.T) {
	cases := map[string]struct {
		input  func(acct *cloudv1.Account, net *cloudv1.Network, info *account.Info)
		expect func(g *WithT, result deploy.Result, ctx *task.BaseTask)
	}{
		"update eks network": {
			input: func(acct *cloudv1.Account, net *cloudv1.Network, info *account.Info) {
				net.Spec.Provider = consts.ProviderTypeEKS
				acct.Spec.AWS = &cloudv1.AWSConfig{
					AccountID:     "1234567890",
					AssumeRoleARN: "arn:aws:iam::1234567890:role/test-role-arn",
					ExternalID:    pointer.String("123"),
				}
				info.NetworkInfo = account.NetworkInfo{
					ServiceRoleName: "svc-role",
				}
			},
			expect: func(g *WithT, result deploy.Result, ctx *task.BaseTask) {
				expect := getNetwork()
				expect.Spec.Provider = consts.ProviderTypeEKS
				expect.ResourceVersion = ctx.Network.ResourceVersion // ignore resource version
				expect.Status.IAM.AccountID = ""
				expect.Status.IAM.ExternalId = "588fc1129521d091782397226b0fc580"
				expectEKSNetwork, err := v1alpha1.EKSNetworkSpecForNetwork(expect)
				g.Expect(err).Should(Succeed())
				expectEKSNetwork.ServiceRoleArn = "arn:aws:iam::1234567890:role/svc-role"
				expectEKSNetwork.AssumeRoleArn = "arn:aws:iam::1234567890:role/test-role-arn"
				expectEKSNetwork.AssumeExternalId = "123"
				_ = v1alpha1.SetEKSNetworkSpec(expect, expectEKSNetwork)

				g.Expect(cmp.Diff(ctx.Network, expect)).Should(BeEmpty(), "+got, -want")
				saved := &cloudv1.Network{}
				err = ctx.Local.Get(context.Background(), client.ObjectKeyFromObject(expect), saved)
				saved.TypeMeta = expect.TypeMeta // ignore type meta
				g.Expect(err).Should(Succeed())
			},
		},
		"panic if aws config of account is nil": {
			input: func(acct *cloudv1.Account, net *cloudv1.Network, info *account.Info) {
				net.Spec.Provider = consts.ProviderTypeEKS
				acct.Spec.AWS = nil
			},
			expect: func(g *WithT, result deploy.Result, ctx *task.BaseTask) {
				g.Expect(result).Should(Equal(deploy.Panic(fmt.Errorf("account %s has no aws account info", accountName(0)))))

				// should not update network
				expect := getNetwork()
				expect.Spec.Provider = consts.ProviderTypeEKS
				expect.ResourceVersion = ctx.Network.ResourceVersion // ignore resource version
				g.Expect(cmp.Diff(ctx.Network, expect)).Should(BeEmpty(), "+got, -want")
			},
		},
		"panic if aws role of account is empty": {
			input: func(acct *cloudv1.Account, net *cloudv1.Network, info *account.Info) {
				net.Spec.Provider = consts.ProviderTypeEKS
				acct.Spec.AWS = &cloudv1.AWSConfig{}
			},
			expect: func(g *WithT, result deploy.Result, ctx *task.BaseTask) {
				g.Expect(result).Should(Equal(deploy.Panic(fmt.Errorf("account %s has no aws account info", accountName(0)))))

				// should not update network
				expect := getNetwork()
				expect.Spec.Provider = consts.ProviderTypeEKS
				expect.ResourceVersion = ctx.Network.ResourceVersion // ignore resource version
				g.Expect(cmp.Diff(ctx.Network, expect)).Should(BeEmpty(), "+got, -want")
			},
		},
		"panic if gcp config of account is nil": {
			input: func(acct *cloudv1.Account, net *cloudv1.Network, info *account.Info) {
				net.Spec.Provider = consts.ProviderTypeGKE
				acct.Spec.GCP = nil
			},
			expect: func(g *WithT, result deploy.Result, ctx *task.BaseTask) {
				g.Expect(result).Should(Equal(deploy.Panic(fmt.Errorf("account %s has no gcp account info", accountName(0)))))

				// should not update network
				expect := getNetwork()
				expect.Spec.Provider = consts.ProviderTypeGKE
				expect.ResourceVersion = ctx.Network.ResourceVersion // ignore resource version
				g.Expect(cmp.Diff(ctx.Network, expect)).Should(BeEmpty(), "+got, -want")
			},
		},
		"panic if gcp project of account is empty": {
			input: func(acct *cloudv1.Account, net *cloudv1.Network, info *account.Info) {
				net.Spec.Provider = consts.ProviderTypeGKE
				acct.Spec.GCP = &cloudv1.GCPConfig{}
			},
			expect: func(g *WithT, result deploy.Result, ctx *task.BaseTask) {
				g.Expect(result).Should(Equal(deploy.Panic(fmt.Errorf("account %s has no gcp account info", accountName(0)))))

				// should not update network
				expect := getNetwork()
				expect.Spec.Provider = consts.ProviderTypeGKE
				expect.ResourceVersion = ctx.Network.ResourceVersion // ignore resource version
				g.Expect(cmp.Diff(ctx.Network, expect)).Should(BeEmpty(), "+got, -want")
			},
		},
		"update gke network spec": {
			input: func(acct *cloudv1.Account, net *cloudv1.Network, info *account.Info) {
				net.Spec.Provider = consts.ProviderTypeGKE
				acct.Spec.GCP = &cloudv1.GCPConfig{
					ProjectID: "test-project",
				}
			},
			expect: func(g *WithT, result deploy.Result, ctx *task.BaseTask) {
				g.Expect(result).Should(Equal(deploy.Panic(nil).WithRequeuAfter(0)))

				// check network spec
				expect := &v1alpha1.GKENetworkSpec{}
				expect.TypeMeta = ctx.GKENetworkSpec.TypeMeta // ignore type meta
				expect.ProjectID = "test-project"
				g.Expect(cmp.Diff(ctx.GKENetworkSpec, expect)).Should(BeEmpty(), "+got, -want")
			},
		},
		"skip if project of gke network is set": {
			input: func(acct *cloudv1.Account, net *cloudv1.Network, info *account.Info) {
				net.Spec.Provider = consts.ProviderTypeGKE
				_ = v1alpha1.SetGKENetworkSpec(net, &v1alpha1.GKENetworkSpec{
					ProjectID: "test-project-2",
				})
				acct.Spec.GCP = &cloudv1.GCPConfig{
					ProjectID: "test-project",
				}
			},
			expect: func(g *WithT, result deploy.Result, ctx *task.BaseTask) {
				g.Expect(result).Should(Equal(deploy.Done()))

				// check network spec
				expect := &v1alpha1.GKENetworkSpec{}
				expect.TypeMeta = ctx.GKENetworkSpec.TypeMeta // ignore type meta
				expect.ProjectID = "test-project-2"
				g.Expect(cmp.Diff(ctx.GKENetworkSpec, expect)).Should(BeEmpty(), "+got, -want")
			},
		},
	}

	for name, c := range cases {
		t.Run(name, func(t *testing.T) {
			g := NewWithT(t)

			acct := accounts(1)[0]
			network := getNetwork()
			info := &account.Info{}

			c.input(acct, network, info)

			s := runtime.NewScheme()
			g.Expect(scheme.AddToScheme(s)).Should(Succeed())
			g.Expect(cloudv1.SchemeBuilder.AddToScheme(s)).Should(Succeed())
			cli := fake.NewClientBuilder().WithScheme(s).Build()
			g.Expect(cli.Create(context.TODO(), acct.DeepCopy())).Should(Succeed())
			g.Expect(cli.Create(context.TODO(), network)).Should(Succeed())

			task := &Account{
				BaseTask: &task.BaseTask{
					Context: &task.Context{
						Network:        network,
						Account:        acct,
						AcctInfoGetter: info,
						Local:          cli,
						Logger:         testr.New(t),
					},
				},
			}
			if task.Network.Spec.Provider == consts.ProviderTypeGKE {
				task.GKENetworkSpec, _ = v1alpha1.GKENetworkSpecForNetwork(task.Network)
			}
			if task.Network.Spec.Provider == consts.ProviderTypeEKS {
				task.EKSNetworkSpec, _ = v1alpha1.EKSNetworkSpecForNetwork(task.Network)
			}

			result := task.PostCreate(context.Background())
			c.expect(g, result, task.BaseTask)
		})
	}
}

type fakeScheduler struct {
	schedFunc func(*plugin.SchedContext, *cloudv1.Network, []*cloudv1.Account) (string, error)
}

func (s *fakeScheduler) Schedule(sctx *plugin.SchedContext, network *cloudv1.Network, accts []*cloudv1.Account) (string, error) {
	if s.schedFunc != nil {
		return s.schedFunc(sctx, network, accts)
	}
	return "", errors.New("failed by default")
}

func (s *fakeScheduler) setScheduleResult(name string, err error) {
	s.schedFunc = func(sctx *plugin.SchedContext, n *cloudv1.Network, a []*cloudv1.Account) (string, error) {
		return name, err
	}
}

func accounts(count int) []*cloudv1.Account {
	accts := make([]*cloudv1.Account, count)
	for i := 0; i < count; i++ {
		accts[i] = &cloudv1.Account{
			ObjectMeta: metav1.ObjectMeta{
				Name: accountName(i),
			},
		}
	}
	return accts
}

func getNetwork() *cloudv1.Network {
	return &cloudv1.Network{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test-network",
			Namespace: "test-ns",
		},
	}
}

func accountName(index int) string {
	return fmt.Sprintf("account-%d", index)
}
