package upgradevalidations_test

import (
	"context"
	"errors"
	"testing"

	"github.com/golang/mock/gomock"
	. "github.com/onsi/gomega"

	"github.com/aws/eks-anywhere/internal/test"
	"github.com/aws/eks-anywhere/pkg/api/v1alpha1"
	"github.com/aws/eks-anywhere/pkg/cluster"
	pmock "github.com/aws/eks-anywhere/pkg/providers/mocks"
	"github.com/aws/eks-anywhere/pkg/types"
	"github.com/aws/eks-anywhere/pkg/utils/ptr"
	"github.com/aws/eks-anywhere/pkg/validations/mocks"
	"github.com/aws/eks-anywhere/pkg/validations/upgradevalidations"
	releasev1alpha1 "github.com/aws/eks-anywhere/release/api/v1alpha1"
)

func TestValidateGitOpsImmutableFieldsRef(t *testing.T) {
	tests := []struct {
		name           string
		oldRef, newRef *v1alpha1.Ref
		wantErr        string
	}{
		{
			name:   "old gitRef nil, new gitRef not nil",
			oldRef: nil,
			newRef: &v1alpha1.Ref{
				Kind: "GitOpsConfig",
				Name: "gitops-new",
			},
			wantErr: "",
		},
		{
			name: "old gitRef not nil, new gitRef nil",
			oldRef: &v1alpha1.Ref{
				Kind: "GitOpsConfig",
				Name: "gitops-old",
			},
			newRef:  nil,
			wantErr: "once cluster.spec.gitOpsRef is set, it is immutable",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(tt *testing.T) {
			g := NewWithT(t)
			ctx := context.Background()
			clusterSpec := test.NewClusterSpec(func(s *cluster.Spec) {
				s.Cluster.Name = testclustername
				s.Cluster.Spec.GitOpsRef = tc.newRef
			})
			cluster := &types.Cluster{
				KubeconfigFile: "kubeconfig",
			}

			oldCluster := &v1alpha1.Cluster{
				Spec: v1alpha1.ClusterSpec{
					GitOpsRef: tc.oldRef,
				},
			}

			err := upgradevalidations.ValidateGitOpsImmutableFields(ctx, nil, cluster, clusterSpec, oldCluster)
			if tc.wantErr == "" {
				g.Expect(err).To(Succeed())
			} else {
				g.Expect(err).To(MatchError(ContainSubstring(tc.wantErr)))
			}
		})
	}
}

type gitOpsTest struct {
	*WithT
	ctx context.Context
	k   *mocks.MockKubectlClient
	c   *types.Cluster
	o   *v1alpha1.Cluster
	s   *cluster.Spec
}

func newGitClientTest(t *testing.T) *gitOpsTest {
	ctrl := gomock.NewController(t)
	return &gitOpsTest{
		WithT: NewWithT(t),
		ctx:   context.Background(),
		k:     mocks.NewMockKubectlClient(ctrl),
		c: &types.Cluster{
			KubeconfigFile: "kubeconfig",
		},
		o: &v1alpha1.Cluster{
			Spec: v1alpha1.ClusterSpec{
				GitOpsRef: &v1alpha1.Ref{
					Name: "test",
					Kind: "GitOpsConfig",
				},
			},
		},
		s: test.NewClusterSpec(func(s *cluster.Spec) {
			s.Cluster.Name = testclustername
			s.Cluster.Spec.GitOpsRef = &v1alpha1.Ref{
				Name: "test",
				Kind: "GitOpsConfig",
			}
		}),
	}
}

func TestValidateGitOpsImmutableFieldsGetEksaGitOpsConfigError(t *testing.T) {
	g := newGitClientTest(t)
	g.k.EXPECT().GetEksaGitOpsConfig(g.ctx, g.s.Cluster.Spec.GitOpsRef.Name, "kubeconfig", "").Return(nil, errors.New("error in get gitops config"))
	g.Expect(upgradevalidations.ValidateGitOpsImmutableFields(g.ctx, g.k, g.c, g.s, g.o)).To(MatchError(ContainSubstring("error in get gitops config")))
}

func TestValidateGitOpsImmutableFieldsGetEksaFluxConfigError(t *testing.T) {
	g := newGitClientTest(t)
	g.o.Spec.GitOpsRef.Kind = "FluxConfig"
	g.s.Cluster.Spec.GitOpsRef.Kind = "FluxConfig"
	g.k.EXPECT().GetEksaFluxConfig(g.ctx, g.s.Cluster.Spec.GitOpsRef.Name, "kubeconfig", "").Return(nil, errors.New("error in get flux config"))
	g.Expect(upgradevalidations.ValidateGitOpsImmutableFields(g.ctx, g.k, g.c, g.s, g.o)).To(MatchError(ContainSubstring("error in get flux config")))
}

func TestValidateGitOpsImmutableFieldsFluxConfig(t *testing.T) {
	tests := []struct {
		name     string
		new, old *v1alpha1.FluxConfig
		wantErr  string
	}{
		{
			name: "github repo diff",
			new: &v1alpha1.FluxConfig{
				Spec: v1alpha1.FluxConfigSpec{
					Github: &v1alpha1.GithubProviderConfig{
						Repository: "a",
					},
				},
			},
			old: &v1alpha1.FluxConfig{
				Spec: v1alpha1.FluxConfigSpec{
					Github: &v1alpha1.GithubProviderConfig{
						Repository: "b",
					},
				},
			},
			wantErr: "fluxConfig spec.github.repository is immutable",
		},
		{
			name: "github owner diff",
			new: &v1alpha1.FluxConfig{
				Spec: v1alpha1.FluxConfigSpec{
					Github: &v1alpha1.GithubProviderConfig{
						Owner: "a",
					},
				},
			},
			old: &v1alpha1.FluxConfig{
				Spec: v1alpha1.FluxConfigSpec{
					Github: &v1alpha1.GithubProviderConfig{
						Owner: "b",
					},
				},
			},
			wantErr: "fluxConfig spec.github.owner is immutable",
		},
		{
			name: "github personal diff",
			new: &v1alpha1.FluxConfig{
				Spec: v1alpha1.FluxConfigSpec{
					Github: &v1alpha1.GithubProviderConfig{
						Personal: true,
					},
				},
			},
			old: &v1alpha1.FluxConfig{
				Spec: v1alpha1.FluxConfigSpec{
					Github: &v1alpha1.GithubProviderConfig{
						Personal: false,
					},
				},
			},
			wantErr: "fluxConfig spec.github.personal is immutable",
		},
		{
			name: "branch diff",
			new: &v1alpha1.FluxConfig{
				Spec: v1alpha1.FluxConfigSpec{
					Branch: "a",
				},
			},
			old: &v1alpha1.FluxConfig{
				Spec: v1alpha1.FluxConfigSpec{
					Branch: "b",
				},
			},
			wantErr: "fluxConfig spec.branch is immutable",
		},
		{
			name: "clusterConfigPath diff",
			new: &v1alpha1.FluxConfig{
				Spec: v1alpha1.FluxConfigSpec{
					ClusterConfigPath: "a",
				},
			},
			old: &v1alpha1.FluxConfig{
				Spec: v1alpha1.FluxConfigSpec{
					ClusterConfigPath: "b",
				},
			},
			wantErr: "fluxConfig spec.clusterConfigPath is immutable",
		},
		{
			name: "systemNamespace diff",
			new: &v1alpha1.FluxConfig{
				Spec: v1alpha1.FluxConfigSpec{
					SystemNamespace: "a",
				},
			},
			old: &v1alpha1.FluxConfig{
				Spec: v1alpha1.FluxConfigSpec{
					SystemNamespace: "b",
				},
			},
			wantErr: "fluxConfig spec.systemNamespace is immutable",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(tt *testing.T) {
			g := newGitClientTest(t)
			g.o.Spec.GitOpsRef.Kind = "FluxConfig"
			g.s.Cluster.Spec.GitOpsRef.Kind = "FluxConfig"
			g.s.FluxConfig = tc.new

			g.k.EXPECT().GetEksaFluxConfig(g.ctx, g.s.Cluster.Spec.GitOpsRef.Name, "kubeconfig", "").Return(tc.old, nil)

			err := upgradevalidations.ValidateGitOpsImmutableFields(g.ctx, g.k, g.c, g.s, g.o)
			if tc.wantErr == "" {
				g.Expect(err).To(Succeed())
			} else {
				g.Expect(err).To(MatchError(ContainSubstring(tc.wantErr)))
			}
		})
	}
}

func TestValidateImmutableFields(t *testing.T) {
	tests := []struct {
		Name             string
		ConfigureCurrent func(current *v1alpha1.Cluster)
		ConfigureDesired func(desired *v1alpha1.Cluster)
		ExpectedError    string
	}{
		{
			Name: "Toggle Spec.ClusterNetwork.CNIConfig.Cilium.SkipUpgrade on",
			ConfigureCurrent: func(current *v1alpha1.Cluster) {
				current.Spec.ClusterNetwork.CNIConfig = &v1alpha1.CNIConfig{
					Cilium: &v1alpha1.CiliumConfig{
						SkipUpgrade: ptr.Bool(false),
					},
				}
			},
			ConfigureDesired: func(desired *v1alpha1.Cluster) {
				desired.Spec.ClusterNetwork.CNIConfig = &v1alpha1.CNIConfig{
					Cilium: &v1alpha1.CiliumConfig{
						SkipUpgrade: ptr.Bool(true),
					},
				}
			},
		},
		{
			Name: "Spec.ClusterNetwork.CNIConfig.Cilium.SkipUpgrade unset",
			ConfigureCurrent: func(current *v1alpha1.Cluster) {
				current.Spec.ClusterNetwork.CNIConfig = &v1alpha1.CNIConfig{
					Cilium: &v1alpha1.CiliumConfig{},
				}
			},
			ConfigureDesired: func(desired *v1alpha1.Cluster) {
				desired.Spec.ClusterNetwork.CNIConfig = &v1alpha1.CNIConfig{
					Cilium: &v1alpha1.CiliumConfig{},
				}
			},
		},
		{
			Name: "Toggle Spec.ClusterNetwork.CNIConfig.Cilium.SkipUpgrade off",
			ConfigureCurrent: func(current *v1alpha1.Cluster) {
				current.Spec.ClusterNetwork.CNIConfig = &v1alpha1.CNIConfig{
					Cilium: &v1alpha1.CiliumConfig{
						SkipUpgrade: ptr.Bool(true),
					},
				}
			},
			ConfigureDesired: func(desired *v1alpha1.Cluster) {
				desired.Spec.ClusterNetwork.CNIConfig = &v1alpha1.CNIConfig{
					Cilium: &v1alpha1.CiliumConfig{
						SkipUpgrade: ptr.Bool(false),
					},
				}
			},
			ExpectedError: "spec.clusterNetwork.cniConfig.cilium.skipUpgrade cannot be toggled off",
		},
		{
			Name: "Add AWS IAM identity provider when none existed",
			ConfigureCurrent: func(current *v1alpha1.Cluster) {
				current.Spec.IdentityProviderRefs = []v1alpha1.Ref{}
			},
			ConfigureDesired: func(desired *v1alpha1.Cluster) {
				desired.Spec.IdentityProviderRefs = []v1alpha1.Ref{
					{
						Kind: v1alpha1.AWSIamConfigKind,
						Name: "aws-iam",
					},
				}
			},
		},
		{
			Name: "Remove AWS IAM identity provider when one existed",
			ConfigureCurrent: func(current *v1alpha1.Cluster) {
				current.Spec.IdentityProviderRefs = []v1alpha1.Ref{
					{
						Kind: v1alpha1.AWSIamConfigKind,
						Name: "aws-iam",
					},
				}
			},
			ConfigureDesired: func(desired *v1alpha1.Cluster) {
				desired.Spec.IdentityProviderRefs = []v1alpha1.Ref{}
			},
		},
		{
			Name: "Both old and new clusters have no AWS IAM config",
			ConfigureCurrent: func(current *v1alpha1.Cluster) {
				current.Spec.IdentityProviderRefs = []v1alpha1.Ref{}
			},
			ConfigureDesired: func(desired *v1alpha1.Cluster) {
				desired.Spec.IdentityProviderRefs = []v1alpha1.Ref{}
			},
		},
	}

	clstr := &types.Cluster{}

	for _, tc := range tests {
		t.Run(tc.Name, func(t *testing.T) {
			g := NewWithT(t)
			ctrl := gomock.NewController(t)

			current := &cluster.Spec{
				Config: &cluster.Config{
					Cluster: &v1alpha1.Cluster{
						Spec: v1alpha1.ClusterSpec{
							WorkerNodeGroupConfigurations: []v1alpha1.WorkerNodeGroupConfiguration{{}},
						},
					},
				},
				Bundles: &releasev1alpha1.Bundles{},
			}
			desired := current.DeepCopy()

			tc.ConfigureCurrent(current.Config.Cluster)
			tc.ConfigureDesired(desired.Config.Cluster)

			client := mocks.NewMockKubectlClient(ctrl)
			client.EXPECT().
				GetEksaCluster(gomock.Any(), clstr, current.Cluster.Name).
				Return(current.Cluster, nil)

			provider := pmock.NewMockProvider(ctrl)

			// The algorithm calls out to the provider to validate the new spec only if it finds
			// no errors in the generic validation first.
			if tc.ExpectedError == "" {
				provider.EXPECT().
					ValidateNewSpec(gomock.Any(), gomock.Any(), gomock.Any()).
					Return(nil)
			}

			err := upgradevalidations.ValidateImmutableFields(
				context.Background(),
				client,
				clstr,
				desired,
				provider,
			)

			if tc.ExpectedError == "" {
				g.Expect(err).To(Succeed())
			} else {
				g.Expect(err).To(MatchError(ContainSubstring(tc.ExpectedError)))
			}
		})
	}
}
