package cmdutils

import (
	"path/filepath"

	"github.com/aws/aws-sdk-go-v2/aws"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/spf13/cobra"
	"github.com/spf13/pflag"

	clusterutils "github.com/weaveworks/eksctl/integration/utilities/cluster"
	api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5"
	"github.com/weaveworks/eksctl/pkg/ctl/cmdutils/filter"
)

var _ = Describe("cmdutils configfile", func() {

	newCmd := func() *cobra.Command {
		return &cobra.Command{
			Use: "test",
			Run: func(_ *cobra.Command, _ []string) {},
		}
	}

	const examplesDir = "../../../examples/"

	Context("load configfiles", func() {

		It("should handle name argument", func() {
			cfg := api.NewClusterConfig()

			{
				cmd := &Cmd{
					ClusterConfig: cfg,
					NameArg:       "foo-1",
					CobraCommand:  newCmd(),
				}

				err := NewMetadataLoader(cmd).Load()
				Expect(err).NotTo(HaveOccurred())
				Expect(cfg.Metadata.Name).To(Equal("foo-1"))
			}

			{
				cmd := &Cmd{
					ClusterConfig: cfg,
					NameArg:       "foo-2",
					CobraCommand:  newCmd(),
				}

				err := NewMetadataLoader(cmd).Load()
				Expect(err).To(HaveOccurred())
				Expect(err.Error()).To(Equal("--cluster=foo-1 and argument foo-2 cannot be used at the same time"))
			}

			{
				cmd := &Cmd{
					ClusterConfig:     cfg,
					NameArg:           "foo-3",
					CobraCommand:      newCmd(),
					ClusterConfigFile: examplesDir + "01-simple-cluster.yaml",
				}

				err := NewMetadataLoader(cmd).Load()
				Expect(err).To(HaveOccurred())
				Expect(err.Error()).To(Equal(ErrCannotUseWithConfigFile(`name argument`).Error()))

				fs := cmd.CobraCommand.Flags()

				fs.StringVar(&cfg.Metadata.Name, "cluster", "", "")
				cmd.CobraCommand.Flag("cluster").Changed = true

				Expect(cmd.CobraCommand.Flag("cluster").Changed).To(BeTrue())

				err = NewMetadataLoader(cmd).Load()

				Expect(err).To(HaveOccurred())
				Expect(err.Error()).To(Equal(ErrCannotUseWithConfigFile("--cluster").Error()))
			}
		})

		Describe("name argument", func() {
			When("given as --name switch", func() {
				It("succeeds", func() {
					cfg := api.NewClusterConfig()
					cobraCmd := newCmd()
					name := "foo-2"
					cobraCmd.SetArgs([]string{"--name", name})

					cmd := &Cmd{
						ClusterConfig:     cfg,
						NameArg:           name,
						CobraCommand:      cobraCmd,
						ClusterConfigFile: examplesDir + "01-simple-cluster.yaml",
						ProviderConfig:    api.ProviderConfig{},
					}
					l := newCommonClusterConfigLoader(cmd)
					l.flagsIncompatibleWithConfigFile.Delete("name")

					err := l.Load()
					Expect(err).NotTo(HaveOccurred())
				})
			})
			When("given as positional argument", func() {
				It("succeeds", func() {
					cfg := api.NewClusterConfig()
					cmd := &Cmd{
						ClusterConfig:     cfg,
						NameArg:           "foo-2",
						CobraCommand:      newCmd(),
						ClusterConfigFile: examplesDir + "01-simple-cluster.yaml",
						ProviderConfig:    api.ProviderConfig{},
					}
					l := newCommonClusterConfigLoader(cmd)
					l.flagsIncompatibleWithConfigFile.Delete("name")

					err := l.Load()
					Expect(err).NotTo(HaveOccurred())
				})
			})
		})

		Describe("managed nodegroup with container runtime", func() {
			When("container runtime is set for a managed nodegroup", func() {
				It("fails validation", func() {
					cfg := api.NewClusterConfig()
					cobraCmd := newCmd()
					name := "foo-2"
					cobraCmd.SetArgs([]string{"--name", name})

					cmd := &Cmd{
						ClusterConfig:     cfg,
						NameArg:           name,
						CobraCommand:      cobraCmd,
						ClusterConfigFile: "test_data/managed-nodegroup-with-container-runtime.yaml",
						ProviderConfig:    api.ProviderConfig{},
					}
					l := newCommonClusterConfigLoader(cmd)
					l.flagsIncompatibleWithConfigFile.Delete("name")

					err := l.Load()
					Expect(err).To(MatchError(ContainSubstring("error unmarshaling JSON: while decoding JSON: json: unknown field \"containerRuntime\"")))
				})
			})
		})

		It("load all of example file", func() {
			examples, err := filepath.Glob(examplesDir + "*.yaml")
			Expect(err).NotTo(HaveOccurred())

			Expect(examples).NotTo(BeEmpty())
			for _, example := range examples {
				cmd := &Cmd{
					CobraCommand:      newCmd(),
					ClusterConfigFile: example,
					ClusterConfig:     api.NewClusterConfig(),
					ProviderConfig:    api.ProviderConfig{},
				}

				err := NewMetadataLoader(cmd).Load()
				Expect(err).NotTo(HaveOccurred())

				cfg := cmd.ClusterConfig
				Expect(err).NotTo(HaveOccurred())
				Expect(cfg.Metadata.Name).NotTo(BeEmpty())
				Expect(cfg.Metadata.Region).NotTo(BeEmpty())
				Expect(cfg.Metadata.Region).To(Equal(cmd.ProviderConfig.Region))
			}
		})
	})

	Describe("CreateClusterLoader", func() {
		It("should set VPC.NAT.Gateway with the correct value", func() {
			natTests := []struct {
				configFile      string
				expectedGateway string
			}{
				// No VPC set
				{"01-simple-cluster.yaml", api.ClusterSingleNAT},
				// VPC set but not NAT
				{"02-custom-vpc-cidr-no-nodes.yaml", api.ClusterSingleNAT},
				// VPC and subnets set but not NAT
				{"04-existing-vpc.yaml", api.ClusterSingleNAT},
				// NAT set
				{"09-nat-gateways.yaml", api.ClusterHighlyAvailableNAT},
			}

			for _, natTest := range natTests {
				cmd := &Cmd{
					CobraCommand:      newCmd(),
					ClusterConfigFile: filepath.Join(examplesDir, natTest.configFile),
					ClusterConfig:     api.NewClusterConfig(),
					ProviderConfig:    api.ProviderConfig{},
				}

				params := &CreateClusterCmdParams{WithoutNodeGroup: true, CreateManagedNGOptions: CreateManagedNGOptions{
					Managed: false,
				}}
				Expect(NewCreateClusterLoader(cmd, filter.NewNodeGroupFilter(), nil, params).Load()).To(Succeed())
				cfg := cmd.ClusterConfig
				Expect(cfg.VPC.NAT.Gateway).To(Not(BeNil()))
				Expect(*cfg.VPC.NAT.Gateway).To(Equal(natTest.expectedGateway))
			}
		})

		It("should return an error for unset nodegroups", func() {
			cmd := &Cmd{
				CobraCommand:      newCmd(),
				ClusterConfigFile: filepath.Join("test_data", "unset-nodegroups.yaml"),
				ClusterConfig:     api.NewClusterConfig(),
				ProviderConfig:    api.ProviderConfig{},
			}
			params := &CreateClusterCmdParams{}
			Expect(NewCreateClusterLoader(cmd, filter.NewNodeGroupFilter(), nil, params).Load()).To(MatchError("invalid ClusterConfig: nodeGroups[1] is not set"))
		})

		When("using zones and node-zones together", func() {
			var (
				flagSet *pflag.FlagSet
				testCmd *cobra.Command
			)
			BeforeEach(func() {
				flagSet = pflag.NewFlagSet("test", pflag.ContinueOnError)
				flagSet.StringSlice("zones", []string{}, "Zones")
				flagSet.StringSlice("node-zones", []string{}, "Node zones")
				testCmd = newCmd()
				testCmd.Flags().AddFlagSet(flagSet)
			})
			When("node-zones is a subset of zones", func() {
				It("does not error", func() {
					Expect(testCmd.ParseFlags([]string{"--zones", "zone1,zone2", "--node-zones", "zone1"})).To(Succeed())
					cmd := &Cmd{
						CobraCommand:   testCmd,
						ClusterConfig:  api.NewClusterConfig(),
						ProviderConfig: api.ProviderConfig{},
					}
					params := &CreateClusterCmdParams{WithoutNodeGroup: true, CreateManagedNGOptions: CreateManagedNGOptions{
						Managed: false,
					}}
					Expect(NewCreateClusterLoader(cmd, filter.NewNodeGroupFilter(), nil, params).Load()).To(Succeed())
				})
			})
			When("node-zones is not a subset of zones", func() {
				It("errors", func() {
					Expect(testCmd.ParseFlags([]string{"--zones", "zone1,zone2", "--node-zones", "zone3"})).To(Succeed())
					cmd := &Cmd{
						CobraCommand:   testCmd,
						ClusterConfig:  api.NewClusterConfig(),
						ProviderConfig: api.ProviderConfig{},
					}

					params := &CreateClusterCmdParams{WithoutNodeGroup: true, CreateManagedNGOptions: CreateManagedNGOptions{
						Managed: false,
					}}
					err := NewCreateClusterLoader(cmd, filter.NewNodeGroupFilter(), nil, params).Load()
					Expect(err).To(MatchError(ContainSubstring("node-zones [zone3] must be a subset of zones [zone1 zone2]; \"zone3\" was not found in zones")))
				})
			})
			When("node-zones is defined but zones isn't", func() {
				It("errors", func() {
					Expect(testCmd.ParseFlags([]string{"--node-zones", "zone3"})).To(Succeed())
					cmd := &Cmd{
						CobraCommand:   testCmd,
						ClusterConfig:  api.NewClusterConfig(),
						ProviderConfig: api.ProviderConfig{},
					}

					params := &CreateClusterCmdParams{WithoutNodeGroup: true, CreateManagedNGOptions: CreateManagedNGOptions{
						Managed: false,
					}}
					err := NewCreateClusterLoader(cmd, filter.NewNodeGroupFilter(), nil, params).Load()
					Expect(err).To(MatchError(ContainSubstring("zones must be defined if node-zones is provided and must be a superset of node-zones")))
				})
			})
		})

		When("using ipv6", func() {
			It("should default VPC.NAT to nil", func() {
				cmd := &Cmd{
					CobraCommand:      newCmd(),
					ClusterConfigFile: filepath.Join(examplesDir, "30-vpc-with-ip-family.yaml"),
					ClusterConfig:     api.NewClusterConfig(),
					ProviderConfig:    api.ProviderConfig{},
				}
				params := &CreateClusterCmdParams{WithoutNodeGroup: true, CreateManagedNGOptions: CreateManagedNGOptions{
					Managed: false,
				}}
				Expect(NewCreateClusterLoader(cmd, filter.NewNodeGroupFilter(), nil, params).Load()).To(Succeed())
				Expect(cmd.ClusterConfig.VPC.NAT).To(BeNil())
			})

			When("using pre-existing VPC", func() {
				It("should default VPC.NAT to nil", func() {
					cmd := &Cmd{
						CobraCommand:      newCmd(),
						ClusterConfigFile: filepath.Join("test_data", "ipv6-existing-vpc.yaml"),
						ClusterConfig:     api.NewClusterConfig(),
						ProviderConfig:    api.ProviderConfig{},
					}
					params := &CreateClusterCmdParams{WithoutNodeGroup: true, CreateManagedNGOptions: CreateManagedNGOptions{
						Managed: false,
					}}
					Expect(NewCreateClusterLoader(cmd, filter.NewNodeGroupFilter(), nil, params).Load()).To(Succeed())
					Expect(cmd.ClusterConfig.VPC.NAT).To(BeNil())
				})
			})
		})

		It("loader should handle named and unnamed nodegroups without config file", func() {
			unnamedNG := api.NewNodeGroup()

			namedNG := api.NewNodeGroup()
			namedNG.Name = "ng-1"

			loaderParams := []struct {
				ng               *api.NodeGroup
				withoutNodeGroup bool
				managed          bool
			}{
				{unnamedNG, false, false},
				{unnamedNG, true, false},
				{namedNG, false, false},
				{namedNG, true, false},
				{unnamedNG, false, true},
				{unnamedNG, true, true},
				{namedNG, false, true},
			}

			assertMatchesNg := func(ng *api.NodeGroup, mNg *api.ManagedNodeGroup) {
				Expect(ng.Name).To(Equal(mNg.Name))
				Expect(ng.IAM).To(Equal(mNg.IAM))
				Expect(ng.VolumeSize).To(Equal(mNg.VolumeSize))
				Expect(ng.MinSize).To(Equal(mNg.MinSize))
				Expect(ng.MaxSize).To(Equal(mNg.MaxSize))
				Expect(ng.DesiredCapacity).To(Equal(mNg.DesiredCapacity))
				Expect(ng.AMIFamily).To(Equal(mNg.AMIFamily))
				Expect(ng.InstanceType).To(Equal(mNg.InstanceType))
				Expect(ng.Tags).To(Equal(mNg.Tags))
				Expect(ng.Labels).To(Equal(mNg.Labels))
				Expect(ng.AvailabilityZones).To(Equal(mNg.AvailabilityZones))
				Expect(ng.SSH).To(Equal(mNg.SSH))
			}

			for _, loaderTest := range loaderParams {
				cmd := &Cmd{
					CobraCommand:   newCmd(),
					ClusterConfig:  api.NewClusterConfig(),
					ProviderConfig: api.ProviderConfig{},
				}

				ngFilter := filter.NewNodeGroupFilter()

				Expect(cmd.ClusterConfig.NodeGroups).To(HaveLen(0))

				params := &CreateClusterCmdParams{
					WithoutNodeGroup: loaderTest.withoutNodeGroup,
					CreateManagedNGOptions: CreateManagedNGOptions{
						Managed: loaderTest.managed,
					},
				}
				Expect(NewCreateClusterLoader(cmd, ngFilter, loaderTest.ng, params).Load()).To(Succeed())

				Expect(ngFilter.GetExcludeAll()).To(Equal(loaderTest.withoutNodeGroup))

				if loaderTest.withoutNodeGroup {
					Expect(cmd.ClusterConfig.NodeGroups).To(HaveLen(0))
				} else {
					if loaderTest.managed {
						Expect(cmd.ClusterConfig.NodeGroups).To(BeEmpty())
						Expect(cmd.ClusterConfig.ManagedNodeGroups).To(HaveLen(1))
						assertMatchesNg(loaderTest.ng, cmd.ClusterConfig.ManagedNodeGroups[0])
						Expect(cmd.ClusterConfig.ManagedNodeGroups[0].Name).To(Equal(loaderTest.ng.Name))
					} else {
						Expect(cmd.ClusterConfig.ManagedNodeGroups).To(BeEmpty())
						Expect(cmd.ClusterConfig.NodeGroups).To(HaveLen(1))
						Expect(cmd.ClusterConfig.NodeGroups[0]).To(Equal(loaderTest.ng))
					}
				}
			}
		})

		It("loader should handle nodegroup exclusion with config file", func() {
			loaderParams := []struct {
				configFile       string
				nodeGroupCount   int
				withoutNodeGroup bool
				// determines whether this ClusterConfig contains managed nodegroups and not the managedFlag argument
				// which is not used when using a config file
				managed bool
			}{
				{"01-simple-cluster.yaml", 1, true, false},
				{"01-simple-cluster.yaml", 1, false, false},
				{"02-custom-vpc-cidr-no-nodes.yaml", 0, true, false},
				{"02-custom-vpc-cidr-no-nodes.yaml", 0, true, false},
				{"03-two-nodegroups.yaml", 2, true, false},
				{"03-two-nodegroups.yaml", 2, false, false},
				{"05-advanced-nodegroups.yaml", 3, true, false},
				{"05-advanced-nodegroups.yaml", 3, false, false},
				{"07-ssh-keys.yaml", 7, true, false},
				{"07-ssh-keys.yaml", 7, false, false},
				{"15-managed-nodes.yaml", 4, true, true},
				{"15-managed-nodes.yaml", 4, false, true},
				{"20-bottlerocket.yaml", 2, false, false},
			}

			for _, loaderTest := range loaderParams {
				cmd := &Cmd{
					CobraCommand:      newCmd(),
					ClusterConfigFile: filepath.Join(examplesDir, loaderTest.configFile),
					ClusterConfig:     api.NewClusterConfig(),
					ProviderConfig:    api.ProviderConfig{},
				}

				ngFilter := filter.NewNodeGroupFilter()

				params := &CreateClusterCmdParams{
					WithoutNodeGroup: loaderTest.withoutNodeGroup,
					CreateManagedNGOptions: CreateManagedNGOptions{
						Managed: loaderTest.managed,
					},
				}
				Expect(NewCreateClusterLoader(cmd, ngFilter, nil, params).Load()).To(Succeed())

				Expect(ngFilter.GetExcludeAll()).To(Equal(loaderTest.withoutNodeGroup))

				if loaderTest.managed {
					Expect(cmd.ClusterConfig.ManagedNodeGroups).To(HaveLen(loaderTest.nodeGroupCount))
					Expect(cmd.ClusterConfig.NodeGroups).To(BeEmpty())
				} else {
					Expect(cmd.ClusterConfig.NodeGroups).To(HaveLen(loaderTest.nodeGroupCount))
					Expect(cmd.ClusterConfig.ManagedNodeGroups).To(BeEmpty())
				}
			}

		})

		Describe("should set defaults for cluster endpoint access", func() {

			testClusterEndpointAccessDefaults := func(configFilePath string, expectedPrivAccess, expectedPubAccess bool) {
				cmd := &Cmd{
					CobraCommand:      newCmd(),
					ClusterConfigFile: configFilePath,
					ClusterConfig:     api.NewClusterConfig(),
					ProviderConfig:    api.ProviderConfig{},
				}

				params := &CreateClusterCmdParams{
					WithoutNodeGroup: true,
					CreateManagedNGOptions: CreateManagedNGOptions{
						Managed: false,
					},
				}

				Expect(NewCreateClusterLoader(cmd, filter.NewNodeGroupFilter(), nil, params).Load()).To(Succeed())
				cfg := cmd.ClusterConfig
				assertValidClusterEndpoint(cfg.VPC.ClusterEndpoints, expectedPrivAccess, expectedPubAccess)
			}

			It("when VPC is imported and no access is defined", func() {
				testClusterEndpointAccessDefaults("test_data/cluster-with-vpc.yaml", false, true)
			})

			It("when VPC is created by eksctl and no access is defined", func() {
				testClusterEndpointAccessDefaults("test_data/cluster-without-vpc.yaml", false, true)
			})

			It("when VPC is created by eksctl and private endpoint is enabled", func() {
				testClusterEndpointAccessDefaults("test_data/cluster-without-vpc-private-access.yaml", true, true)
			})

			It("when VPC is imported and private endpoint is enabled", func() {
				testClusterEndpointAccessDefaults("test_data/cluster-with-vpc-private-access.yaml", true, true)
			})
		})

		type bareClusterEntry struct {
			updateClusterConfig func(*api.ClusterConfig)
			expectErr           bool
		}

		DescribeTable("Bare Cluster validation", func(e bareClusterEntry) {
			cmd := &Cmd{
				CobraCommand:      newCmd(),
				ClusterConfigFile: "-",
				ClusterConfig:     api.NewClusterConfig(),
				ProviderConfig:    api.ProviderConfig{},
			}
			clusterConfig := api.NewClusterConfig()
			clusterConfig.Metadata.Name = "cluster"
			clusterConfig.Metadata.Region = api.DefaultRegion
			clusterConfig.AddonsConfig.DisableDefaultAddons = true
			clusterConfig.Addons = []*api.Addon{
				{
					Name: api.CoreDNSAddon,
				},
			}
			e.updateClusterConfig(clusterConfig)
			err := NewCreateClusterLoader(cmd, filter.NewNodeGroupFilter(), nil, &CreateClusterCmdParams{
				ConfigReader: clusterutils.Reader(clusterConfig),
			}).Load()
			if e.expectErr {
				Expect(err).To(MatchError("fields nodeGroups, managedNodeGroups, fargateProfiles, karpenter, gitops, iam.serviceAccounts, " +
					"and iam.podIdentityAssociations are not supported during cluster creation in a cluster without VPC CNI if Auto Mode is disabled; " +
					"please remove these fields and add them back after cluster creation is successful"))
			} else {
				Expect(err).NotTo(HaveOccurred())
			}
		},
			Entry("nodeGroups", bareClusterEntry{
				updateClusterConfig: func(c *api.ClusterConfig) {
					ng := api.NewNodeGroup()
					ng.Name = "ng"
					ng.DesiredCapacity = aws.Int(1)
					c.NodeGroups = []*api.NodeGroup{ng}
				},
				expectErr: true,
			}),
			Entry("managedNodeGroups", bareClusterEntry{
				updateClusterConfig: func(c *api.ClusterConfig) {
					ng := api.NewManagedNodeGroup()
					ng.Name = "mng"
					ng.DesiredCapacity = aws.Int(1)
					c.ManagedNodeGroups = []*api.ManagedNodeGroup{ng}
				},
				expectErr: true,
			}),
			Entry("fargateProfiles", bareClusterEntry{
				updateClusterConfig: func(c *api.ClusterConfig) {
					c.FargateProfiles = []*api.FargateProfile{
						{
							Name: "test",
							Selectors: []api.FargateProfileSelector{
								{
									Namespace: "default",
								},
							},
						},
					}
				},
				expectErr: true,
			}),
			Entry("gitops", bareClusterEntry{
				updateClusterConfig: func(c *api.ClusterConfig) {
					c.GitOps = &api.GitOps{
						Flux: &api.Flux{
							GitProvider: "github",
							Flags: api.FluxFlags{
								"owner": "aws",
							},
						},
					}
				},
				expectErr: true,
			}),
			Entry("karpenter", bareClusterEntry{
				updateClusterConfig: func(c *api.ClusterConfig) {
					c.Karpenter = &api.Karpenter{}
				},
				expectErr: true,
			}),
			Entry("iam.serviceAccounts", bareClusterEntry{
				updateClusterConfig: func(c *api.ClusterConfig) {
					c.IAM.WithOIDC = api.Enabled()
					c.IAM.ServiceAccounts = []*api.ClusterIAMServiceAccount{
						{
							ClusterIAMMeta: api.ClusterIAMMeta{
								Name:      "test",
								Namespace: "test",
							},
							AttachPolicyARNs: []string{"arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"},
						},
					}
				},
				expectErr: true,
			}),
			Entry("iam.podIdentityAssociations", bareClusterEntry{
				updateClusterConfig: func(c *api.ClusterConfig) {
					c.Addons = append(c.Addons, &api.Addon{Name: api.PodIdentityAgentAddon})
					c.IAM.PodIdentityAssociations = []api.PodIdentityAssociation{
						{
							Namespace:            "test",
							PermissionPolicyARNs: []string{"arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"},
						},
					}
				},
				expectErr: true,
			}),
			Entry("no unsupported field set", bareClusterEntry{
				updateClusterConfig: func(c *api.ClusterConfig) {},
			}),
		)
	})

	Describe("SetLabelLoader", func() {
		It("should load the right data", func() {
			cmd := &Cmd{
				CobraCommand:      newCmd(),
				ClusterConfigFile: filepath.Join("test_data", "cluster-with-labels.yaml"),
				ClusterConfig:     api.NewClusterConfig(),
				ProviderConfig:    api.ProviderConfig{},
			}

			Expect(NewSetLabelLoader(cmd, "ng-1", nil).Load()).To(Succeed())
			cfg := cmd.ClusterConfig
			Expect(cfg.Metadata.Name).To(Equal("test-labels-1"))
			Expect(cfg.ManagedNodeGroups[0].Name).To(Equal("ng-1"))
		})
		When("no config file is provided and no labels", func() {
			It("errors", func() {
				cmd := &Cmd{
					CobraCommand:   newCmd(),
					ClusterConfig:  api.NewClusterConfig(),
					ProviderConfig: api.ProviderConfig{},
				}

				err := NewSetLabelLoader(cmd, "", nil).Load()
				Expect(err).To(MatchError(ContainSubstring("--labels must be set")))
			})
		})
		When("config file with no nodegroup name is provided", func() {
			It("should load all nodegroups", func() {
				cmd := &Cmd{
					CobraCommand:      newCmd(),
					ClusterConfigFile: filepath.Join("test_data", "cluster-with-labels.yaml"),
					ClusterConfig:     api.NewClusterConfig(),
					ProviderConfig:    api.ProviderConfig{},
				}

				Expect(NewSetLabelLoader(cmd, "", nil).Load()).To(Succeed())
				cfg := cmd.ClusterConfig
				Expect(cfg.Metadata.Name).To(Equal("test-labels-1"))
				Expect(cfg.ManagedNodeGroups[0].Name).To(Equal("ng-1"))
				Expect(cfg.ManagedNodeGroups[1].Name).To(Equal("ng-2"))
			})
		})
		When("config file with missing nodegroup name is provided", func() {
			It("errors", func() {
				cmd := &Cmd{
					CobraCommand:      newCmd(),
					ClusterConfigFile: filepath.Join("test_data", "cluster-with-labels.yaml"),
					ClusterConfig:     api.NewClusterConfig(),
					ProviderConfig:    api.ProviderConfig{},
				}

				err := NewSetLabelLoader(cmd, "invalid", nil).Load()
				Expect(err).To(MatchError(ContainSubstring("nodegroup with name invalid not found in the config file")))
			})
		})
	})

	Describe("NewCreateIAMServiceAccountLoader", func() {
		When("subject-pattern flag is used with config file", func() {
			It("should return an error", func() {
				cfg := api.NewClusterConfig()
				cobraCmd := newCmd()
				cobraCmd.Flags().String("subject-pattern", "", "")
				cobraCmd.Flags().String("cluster", "", "")
				Expect(cobraCmd.ParseFlags([]string{"--subject-pattern", "app-*"})).To(Succeed())

				cmd := &Cmd{
					ClusterConfig:     cfg,
					CobraCommand:      cobraCmd,
					ClusterConfigFile: examplesDir + "01-simple-cluster.yaml",
					ProviderConfig:    api.ProviderConfig{},
				}

				err := NewCreateIAMServiceAccountLoader(cmd, filter.NewIAMServiceAccountFilter()).Load()
				Expect(err).To(HaveOccurred())
				Expect(err.Error()).To(ContainSubstring("--subject-pattern"))
				Expect(err.Error()).To(ContainSubstring("cannot use --subject-pattern when --config-file/-f is set"))
			})
		})

		When("subject-pattern flag is used without config file", func() {
			It("should succeed", func() {
				cfg := api.NewClusterConfig()
				cfg.Metadata.Name = "test-cluster"
				serviceAccount := &api.ClusterIAMServiceAccount{
					ClusterIAMMeta: api.ClusterIAMMeta{
						Name:      "test-sa",
						Namespace: "default",
					},
					AttachPolicyARNs: []string{"arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"},
					SubjectPattern:   "app-*",
				}
				cfg.IAM.ServiceAccounts = []*api.ClusterIAMServiceAccount{serviceAccount}

				cobraCmd := newCmd()
				cobraCmd.Flags().String("cluster", "", "")
				cobraCmd.Flags().String("subject-pattern", "", "")

				cmd := &Cmd{
					ClusterConfig:  cfg,
					CobraCommand:   cobraCmd,
					ProviderConfig: api.ProviderConfig{},
				}

				err := NewCreateIAMServiceAccountLoader(cmd, filter.NewIAMServiceAccountFilter()).Load()
				Expect(err).NotTo(HaveOccurred())
			})
		})
	})

	Context("makeManagedNodegroup with node repair config", func() {
		var (
			ng      *api.NodeGroup
			options CreateManagedNGOptions
		)

		BeforeEach(func() {
			ng = &api.NodeGroup{
				NodeGroupBase: &api.NodeGroupBase{
					Name: "test-ng",
				},
			}
			options = CreateManagedNGOptions{}
		})

		It("should create managed nodegroup without node repair config when not enabled", func() {
			options.NodeRepairEnabled = false

			mng := makeManagedNodegroup(ng, options)

			Expect(mng.NodeRepairConfig).To(BeNil())
		})

		It("should create managed nodegroup with basic node repair config when enabled", func() {
			options.NodeRepairEnabled = true

			mng := makeManagedNodegroup(ng, options)

			Expect(mng.NodeRepairConfig).NotTo(BeNil())
			Expect(mng.NodeRepairConfig.Enabled).NotTo(BeNil())
			Expect(*mng.NodeRepairConfig.Enabled).To(BeTrue())
			Expect(mng.NodeRepairConfig.MaxUnhealthyNodeThresholdPercentage).To(BeNil())
			Expect(mng.NodeRepairConfig.MaxUnhealthyNodeThresholdCount).To(BeNil())
			Expect(mng.NodeRepairConfig.MaxParallelNodesRepairedPercentage).To(BeNil())
			Expect(mng.NodeRepairConfig.MaxParallelNodesRepairedCount).To(BeNil())
		})

		It("should create managed nodegroup with threshold percentage", func() {
			options.NodeRepairEnabled = true
			options.NodeRepairMaxUnhealthyPercentage = aws.Int(25)

			mng := makeManagedNodegroup(ng, options)

			Expect(mng.NodeRepairConfig).NotTo(BeNil())
			Expect(mng.NodeRepairConfig.Enabled).NotTo(BeNil())
			Expect(*mng.NodeRepairConfig.Enabled).To(BeTrue())
			Expect(mng.NodeRepairConfig.MaxUnhealthyNodeThresholdPercentage).NotTo(BeNil())
			Expect(*mng.NodeRepairConfig.MaxUnhealthyNodeThresholdPercentage).To(Equal(25))
		})

		It("should create managed nodegroup with threshold count", func() {
			options.NodeRepairEnabled = true
			options.NodeRepairMaxUnhealthyCount = aws.Int(5)

			mng := makeManagedNodegroup(ng, options)

			Expect(mng.NodeRepairConfig).NotTo(BeNil())
			Expect(mng.NodeRepairConfig.Enabled).NotTo(BeNil())
			Expect(*mng.NodeRepairConfig.Enabled).To(BeTrue())
			Expect(mng.NodeRepairConfig.MaxUnhealthyNodeThresholdCount).NotTo(BeNil())
			Expect(*mng.NodeRepairConfig.MaxUnhealthyNodeThresholdCount).To(Equal(5))
		})

		It("should create managed nodegroup with parallel percentage", func() {
			options.NodeRepairEnabled = true
			options.NodeRepairMaxParallelPercentage = aws.Int(20)

			mng := makeManagedNodegroup(ng, options)

			Expect(mng.NodeRepairConfig).NotTo(BeNil())
			Expect(mng.NodeRepairConfig.Enabled).NotTo(BeNil())
			Expect(*mng.NodeRepairConfig.Enabled).To(BeTrue())
			Expect(mng.NodeRepairConfig.MaxParallelNodesRepairedPercentage).NotTo(BeNil())
			Expect(*mng.NodeRepairConfig.MaxParallelNodesRepairedPercentage).To(Equal(20))
		})

		It("should create managed nodegroup with parallel count", func() {
			options.NodeRepairEnabled = true
			options.NodeRepairMaxParallelCount = aws.Int(3)

			mng := makeManagedNodegroup(ng, options)

			Expect(mng.NodeRepairConfig).NotTo(BeNil())
			Expect(mng.NodeRepairConfig.Enabled).NotTo(BeNil())
			Expect(*mng.NodeRepairConfig.Enabled).To(BeTrue())
			Expect(mng.NodeRepairConfig.MaxParallelNodesRepairedCount).NotTo(BeNil())
			Expect(*mng.NodeRepairConfig.MaxParallelNodesRepairedCount).To(Equal(3))
		})

		It("should create managed nodegroup with all parameters", func() {
			options.NodeRepairEnabled = true
			options.NodeRepairMaxUnhealthyPercentage = aws.Int(30)
			options.NodeRepairMaxUnhealthyCount = aws.Int(10)
			options.NodeRepairMaxParallelPercentage = aws.Int(25)
			options.NodeRepairMaxParallelCount = aws.Int(4)

			mng := makeManagedNodegroup(ng, options)

			Expect(mng.NodeRepairConfig).NotTo(BeNil())
			Expect(mng.NodeRepairConfig.Enabled).NotTo(BeNil())
			Expect(*mng.NodeRepairConfig.Enabled).To(BeTrue())
			Expect(mng.NodeRepairConfig.MaxUnhealthyNodeThresholdPercentage).NotTo(BeNil())
			Expect(*mng.NodeRepairConfig.MaxUnhealthyNodeThresholdPercentage).To(Equal(30))
			Expect(mng.NodeRepairConfig.MaxUnhealthyNodeThresholdCount).NotTo(BeNil())
			Expect(*mng.NodeRepairConfig.MaxUnhealthyNodeThresholdCount).To(Equal(10))
			Expect(mng.NodeRepairConfig.MaxParallelNodesRepairedPercentage).NotTo(BeNil())
			Expect(*mng.NodeRepairConfig.MaxParallelNodesRepairedPercentage).To(Equal(25))
			Expect(mng.NodeRepairConfig.MaxParallelNodesRepairedCount).NotTo(BeNil())
			Expect(*mng.NodeRepairConfig.MaxParallelNodesRepairedCount).To(Equal(4))
		})

		It("should create node repair config when enabled is false but other parameters are set", func() {
			options.NodeRepairEnabled = false
			options.NodeRepairMaxUnhealthyPercentage = aws.Int(15)

			mng := makeManagedNodegroup(ng, options)

			Expect(mng.NodeRepairConfig).NotTo(BeNil())
			Expect(mng.NodeRepairConfig.Enabled).NotTo(BeNil())
			Expect(*mng.NodeRepairConfig.Enabled).To(BeFalse())
			Expect(mng.NodeRepairConfig.MaxUnhealthyNodeThresholdPercentage).NotTo(BeNil())
			Expect(*mng.NodeRepairConfig.MaxUnhealthyNodeThresholdPercentage).To(Equal(15))
		})

		It("should ignore zero values for optional parameters", func() {
			options.NodeRepairEnabled = true
			options.NodeRepairMaxUnhealthyPercentage = aws.Int(0)
			options.NodeRepairMaxParallelCount = aws.Int(0)

			mng := makeManagedNodegroup(ng, options)

			Expect(mng.NodeRepairConfig).NotTo(BeNil())
			Expect(mng.NodeRepairConfig.Enabled).NotTo(BeNil())
			Expect(*mng.NodeRepairConfig.Enabled).To(BeTrue())
			// Zero values should be ignored
			Expect(mng.NodeRepairConfig.MaxUnhealthyNodeThresholdPercentage).To(BeNil())
			Expect(mng.NodeRepairConfig.MaxParallelNodesRepairedCount).To(BeNil())
		})

		It("should handle nil pointers for optional parameters", func() {
			options.NodeRepairEnabled = true
			options.NodeRepairMaxUnhealthyPercentage = nil
			options.NodeRepairMaxUnhealthyCount = nil
			options.NodeRepairMaxParallelPercentage = nil
			options.NodeRepairMaxParallelCount = nil

			mng := makeManagedNodegroup(ng, options)

			Expect(mng.NodeRepairConfig).NotTo(BeNil())
			Expect(mng.NodeRepairConfig.Enabled).NotTo(BeNil())
			Expect(*mng.NodeRepairConfig.Enabled).To(BeTrue())
			Expect(mng.NodeRepairConfig.MaxUnhealthyNodeThresholdPercentage).To(BeNil())
			Expect(mng.NodeRepairConfig.MaxUnhealthyNodeThresholdCount).To(BeNil())
			Expect(mng.NodeRepairConfig.MaxParallelNodesRepairedPercentage).To(BeNil())
			Expect(mng.NodeRepairConfig.MaxParallelNodesRepairedCount).To(BeNil())
		})

		It("should not create node repair config when CLI flags have default values", func() {
			// Simulate CLI flag behavior where fs.Int() creates non-nil pointers to default values
			options.NodeRepairEnabled = false                     // default value
			options.NodeRepairMaxUnhealthyPercentage = aws.Int(0) // fs.Int() creates pointer to 0
			options.NodeRepairMaxUnhealthyCount = aws.Int(0)      // fs.Int() creates pointer to 0
			options.NodeRepairMaxParallelPercentage = aws.Int(0)  // fs.Int() creates pointer to 0
			options.NodeRepairMaxParallelCount = aws.Int(0)       // fs.Int() creates pointer to 0

			mng := makeManagedNodegroup(ng, options)

			Expect(mng.NodeRepairConfig).To(BeNil())
		})
	})
})

func assertValidClusterEndpoint(endpoints *api.ClusterEndpoints, privateAccess, publicAccess bool) {
	Expect(endpoints).To(Not(BeNil()))
	Expect(endpoints.PrivateAccess).To(Not(BeNil()))
	Expect(*endpoints.PrivateAccess).To(Equal(privateAccess))
	Expect(endpoints.PublicAccess).To(Not(BeNil()))
	Expect(*endpoints.PublicAccess).To(Equal(publicAccess))
}
