/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

	http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package internal

import (
	"fmt"
	"os"
	"path/filepath"
	"testing"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	"sigs.k8s.io/kubebuilder/v4/pkg/config/store"
	"sigs.k8s.io/kubebuilder/v4/pkg/model/resource"
	deployimagev1alpha1 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/deploy-image/v1alpha1"
	autoupdatev1alpha "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/autoupdate/v1alpha"
	"sigs.k8s.io/kubebuilder/v4/test/e2e/utils"
)

type fakeConfig struct {
	config.Config
	pluginChain []string
	domain      string
	repo        string
	projectName string
	multigroup  bool
	namespaced  bool
	resources   []resource.Resource
	pluginErr   error
	getResErr   error
	plugins     map[string]any
}

func (f *fakeConfig) GetPluginChain() []string { return f.pluginChain }
func (f *fakeConfig) GetDomain() string        { return f.domain }
func (f *fakeConfig) GetRepository() string    { return f.repo }
func (f *fakeConfig) GetProjectName() string   { return f.projectName }
func (f *fakeConfig) IsMultiGroup() bool       { return f.multigroup }
func (f *fakeConfig) IsNamespaced() bool       { return f.namespaced }
func (f *fakeConfig) GetResources() ([]resource.Resource, error) {
	if f.getResErr != nil {
		return nil, f.getResErr
	}
	return f.resources, nil
}

func (f *fakeConfig) DecodePluginConfig(key string, dst any) error {
	if len(f.plugins) == 0 {
		return config.PluginKeyNotFoundError{Key: key}
	}
	if f.pluginErr != nil {
		return f.pluginErr
	}
	// Check if the specific key exists
	val, exists := f.plugins[key]
	if !exists {
		return config.PluginKeyNotFoundError{Key: key}
	}
	// If the value is a struct, copy its fields to dst
	if val != nil && dst != nil {
		// Handle different plugin config types
		switch d := dst.(type) {
		case *autoupdatev1alpha.PluginConfig:
			if v, ok := val.(autoupdatev1alpha.PluginConfig); ok {
				*d = v
			}
		case *deployimagev1alpha1.PluginConfig:
			if v, ok := val.(deployimagev1alpha1.PluginConfig); ok {
				*d = v
			}
		}
	}
	return nil
}

type fakeStore struct {
	store.Store
	cfg *fakeConfig
}

func (f *fakeStore) Config() config.Config { return f.cfg }

const (
	exampleDomain      = "example.com"
	fooKind            = "Foo"
	exampleKind        = "Example"
	fixtureTest        = "test"
	certManagerAPIPath = "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
	certificateKind    = "Certificate"
	certManagerGroup   = "cert-manager"
	examplesDir        = "examples"
	exampleRepo        = "github.com/example/repo"
)

func TestGenerateHelpers(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Generate helpers Suite")
}

// setupKubebuilderMockEnvironment sets up a mock for kubebuilder testing
func setupKubebuilderMockEnvironment(kbc *utils.TestContext) string {
	// Save current working directory for restoration
	originalDir, err := os.Getwd()
	Expect(err).NotTo(HaveOccurred())

	// Create a PROJECT file in the test directory with proper YAML format
	projectFilePath := filepath.Join(kbc.Dir, "PROJECT")
	projectFileContent := []byte(`# Code generated by tool. DO NOT EDIT.
# This file is used to track the info used to scaffold your project
# and allow the plugins properly work.
# More info: https://book.kubebuilder.io/reference/project-config.html
domain: example.com
layout:
- go.kubebuilder.io/v4
projectName: test-project
repo: github.com/example/test-project
version: "3"
`)
	Expect(os.WriteFile(projectFilePath, projectFileContent, 0o644)).To(Succeed())

	// Create mock kubebuilder binary
	mockKubebuilderPath := filepath.Join(kbc.Dir, "kubebuilder")
	mockScript := `#!/bin/bash
# Log all commands to a file for debugging
log_file="` + filepath.Join(kbc.Dir, "kubebuilder.log") + `"
echo "$@" >> "$log_file"

if [[ "$1" == "init" ]]; then
	echo "kubebuilder init mock executed"
	exit 0
elif [[ "$1" == "create" && "$2" == "api" ]]; then
	echo "kubebuilder create api mock executed"
	exit 0
elif [[ "$1" == "create" && "$2" == "webhook" ]]; then
	echo "kubebuilder create webhook mock executed"
	exit 0
elif [[ "$1" == "edit" ]]; then
	echo "kubebuilder edit mock executed"
	exit 0
else
	echo "kubebuilder mock executed with args: $@"
	exit 0
fi`
	Expect(os.WriteFile(mockKubebuilderPath, []byte(mockScript), 0o755)).To(Succeed())

	// Create mock make binary
	mockMakePath := filepath.Join(kbc.Dir, "make")
	makeScript := `#!/bin/bash
	# Log all commands to a file for debugging
	log_file="` + filepath.Join(kbc.Dir, "make.log") + `"
	echo "$@" >> "$log_file"
	echo "make mock executed with target: $@"
	exit 0`
	Expect(os.WriteFile(mockMakePath, []byte(makeScript), 0o755)).To(Succeed())

	// Add the test directory to PATH so the mock binaries are found
	oldPath := os.Getenv("PATH")
	newPath := fmt.Sprintf("%s:%s", kbc.Dir, oldPath)
	Expect(os.Setenv("PATH", newPath)).To(Succeed())

	// Change to the test directory so kubebuilder can find the PROJECT file
	Expect(os.Chdir(kbc.Dir)).To(Succeed())

	// Return the original directory for restoration
	return originalDir
}

var _ = Describe("generate: validate", func() {
	var (
		kbc *utils.TestContext
		err error
	)

	BeforeEach(func() {
		// Initialize TestContext
		kbc, err = utils.NewTestContext("kubebuilder", "GO111MODULE=on")
		Expect(err).NotTo(HaveOccurred())
		Expect(kbc.Prepare()).To(Succeed())

		// Create a PROJECT file in the test directory
		projectFilePath := filepath.Join(kbc.Dir, "PROJECT")
		projectFileContent := []byte("domain: example.com\nrepo: github.com/example/repo\n")
		Expect(os.WriteFile(projectFilePath, projectFileContent, 0o644)).To(Succeed())
	})

	AfterEach(func() {
		By("cleaning up test artifacts")
		kbc.Destroy()
	})

	// Validate
	Context("Validate", func() {
		Context("Success", func() {
			It("succeeds", func() {
				g := &Generate{InputDir: kbc.Dir}
				Expect(g.Validate()).To(Succeed())
			})
		})

		Context("Failure", func() {
			It("returns error if GetInputPath fails", func() {
				g := &Generate{InputDir: filepath.Join(kbc.Dir, "notfound")}
				Expect(g.Validate()).NotTo(Succeed())
			})
		})
	})
})

var _ = Describe("generate: directory-helpers", func() {
	var (
		tmpDir string
		err    error
	)
	BeforeEach(func() {
		tmpDir, err = os.MkdirTemp("", "testdir")
		Expect(err).NotTo(HaveOccurred())
	})
	AfterEach(func() {
		Expect(os.RemoveAll(tmpDir)).To(Succeed())
	})

	// createDirectory
	Context("createDirectory", func() {
		It("creates directory successfully", func() {
			dir := filepath.Join(tmpDir, "testdir-generate-go")
			Expect(createDirectory(dir)).To(Succeed())
			_, err = os.Stat(dir)
			Expect(err).NotTo(HaveOccurred())
		})
		It("returns error for invalid path", func() {
			Expect(createDirectory("/dev/null/foo")).NotTo(Succeed())
		})
	})

	// changeWorkingDirectory
	Context("changeWorkingDirectory", func() {
		var originalDir string

		BeforeEach(func() {
			// Save current working directory
			originalDir, err = os.Getwd()
			Expect(err).NotTo(HaveOccurred())
		})

		AfterEach(func() {
			// Restore original working directory
			Expect(os.Chdir(originalDir)).To(Succeed())
		})

		It("changes working directory successfully", func() {
			// Create a test file in the target directory to verify we can access it after changing
			testFile := filepath.Join(tmpDir, "test-file.txt")
			testContent := "test content"
			Expect(os.WriteFile(testFile, []byte(testContent), 0o644)).To(Succeed())

			// Change to the directory
			Expect(changeWorkingDirectory(tmpDir)).To(Succeed())

			// Verify we're in the correct directory by checking we can read the file
			// using relative path (this proves we're in the right directory)
			content, err := os.ReadFile("test-file.txt")
			Expect(err).NotTo(HaveOccurred())
			Expect(string(content)).To(Equal(testContent))

			// Also verify we can create a new file in the current directory
			newFile := "new-test-file.txt"
			newContent := "new content"
			Expect(os.WriteFile(newFile, []byte(newContent), 0o644)).To(Succeed())

			// Verify the file was created in the expected location
			fullPath := filepath.Join(tmpDir, newFile)
			verifyContent, err := os.ReadFile(fullPath)
			Expect(err).NotTo(HaveOccurred())
			Expect(string(verifyContent)).To(Equal(newContent))
		})

		It("returns error for non-existent directory", func() {
			nonExistentDir := filepath.Join(tmpDir, "nonexistent")
			Expect(changeWorkingDirectory(nonExistentDir)).NotTo(Succeed())
		})

		It("returns error for invalid path", func() {
			Expect(changeWorkingDirectory("/dev/null/foo")).NotTo(Succeed())
		})
	})
})

var _ = Describe("generate: file-helpers", func() {
	var (
		tmpDir string
		err    error
	)
	BeforeEach(func() {
		tmpDir, err = os.MkdirTemp("", "testdir")
		Expect(err).NotTo(HaveOccurred())
	})
	AfterEach(func() {
		Expect(os.RemoveAll(tmpDir)).To(Succeed())
	})

	// copyFile
	Context("copyFile", func() {
		Context("success", func() {
			var src, dst string
			BeforeEach(func() {
				src = filepath.Join(tmpDir, "src.txt")
				dst = filepath.Join(tmpDir, "dst.txt")
				Expect(os.WriteFile(src, []byte("hello"), 0o644)).To(Succeed())
			})
			AfterEach(func() {
				Expect(os.Remove(src)).To(Succeed())
				Expect(os.Remove(dst)).To(Succeed())
			})

			It("copies file successfully", func() {
				Expect(copyFile(src, dst)).To(Succeed())
				b, err := os.ReadFile(dst)
				Expect(err).NotTo(HaveOccurred())
				Expect(string(b)).To(Equal("hello"))
			})
		})

		Context("failure", func() {
			It("returns error if src does not exist", func() {
				src := filepath.Join(tmpDir, "notfound")
				dst := filepath.Join(tmpDir, "nowhere")
				Expect(copyFile(src, dst)).NotTo(Succeed())
			})
		})
	})
})

var _ = Describe("generate: get-args-helpers", func() {
	const (
		fooDomain = "foo.com"
		barRepo   = "bar"
	)

	// getInitArgs
	Describe("getInitArgs", func() {
		Context("for outdated plugins", func() {
			When("v3 plugin is used", func() {
				It("should return correct args for plugins, domain, repo", func() {
					cfg := &fakeConfig{pluginChain: []string{"go.kubebuilder.io/v3"}, domain: fooDomain, repo: barRepo}
					store := &fakeStore{cfg: cfg}
					args := getInitArgs(store, &Generate{SkipGoVersionCheck: true}, "")
					Expect(args).To(ContainElements("--skip-go-version-check", flagPlugins, ContainSubstring(pluginGoKubebuilderV4),
						"--domain", fooDomain, "--repo", barRepo))
				})
			})

			When("alpha plugin is used", func() {
				It("should return correct args for plugins, domain, repo", func() {
					cfg := &fakeConfig{pluginChain: []string{"go.kubebuilder.io/v3-alpha"}, domain: fooDomain, repo: barRepo}
					store := &fakeStore{cfg: cfg}
					args := getInitArgs(store, &Generate{SkipGoVersionCheck: true}, "")
					Expect(args).To(ContainElements("--skip-go-version-check", flagPlugins, ContainSubstring(pluginGoKubebuilderV4),
						"--domain", fooDomain, "--repo", barRepo))
				})
			})

			When("helm v1-alpha plugin is used", func() {
				It("should replace with helm v2-alpha", func() {
					cfg := &fakeConfig{
						pluginChain: []string{pluginGoKubebuilderV4, pluginHelmKubebuilderV1Alpha},
						domain:      fooDomain,
						repo:        barRepo,
					}
					store := &fakeStore{cfg: cfg}
					args := getInitArgs(store, &Generate{SkipGoVersionCheck: true}, "")
					Expect(args).To(ContainElements(
						"--skip-go-version-check",
						flagPlugins, ContainSubstring(pluginHelmKubebuilderV2Alpha),
						"--domain", fooDomain, "--repo", barRepo))
					Expect(args).NotTo(ContainElement(ContainSubstring(pluginHelmKubebuilderV1Alpha)))
				})
			})
		})

		Context("for latest plugins", func() {
			When("latest plugin (v4) is used", func() {
				It("returns correct args for plugins, domain, repo", func() {
					cfg := &fakeConfig{pluginChain: []string{pluginGoKubebuilderV4}, domain: fooDomain, repo: barRepo}
					store := &fakeStore{cfg: cfg}
					args := getInitArgs(store, &Generate{SkipGoVersionCheck: true}, "")
					Expect(args).To(ContainElements("--skip-go-version-check", flagPlugins, ContainSubstring(pluginGoKubebuilderV4),
						"--domain", fooDomain, "--repo", barRepo))
				})
			})

			When("project name is set", func() {
				It("returns correct args including project name", func() {
					cfg := &fakeConfig{
						pluginChain: []string{pluginGoKubebuilderV4},
						domain:      fooDomain,
						repo:        barRepo,
						projectName: "my-project",
					}
					store := &fakeStore{cfg: cfg}
					args := getInitArgs(store, &Generate{SkipGoVersionCheck: true}, "")
					Expect(args).To(ContainElements("--skip-go-version-check", flagPlugins, ContainSubstring(pluginGoKubebuilderV4),
						"--domain", fooDomain, "--repo", barRepo, "--project-name", "my-project"))
				})
			})

			When("multigroup flag is enabled", func() {
				It("includes --multigroup in init args", func() {
					cfg := &fakeConfig{
						pluginChain: []string{pluginGoKubebuilderV4},
						domain:      fooDomain,
						repo:        barRepo,
						multigroup:  true,
					}
					store := &fakeStore{cfg: cfg}
					args := getInitArgs(store, &Generate{SkipGoVersionCheck: true}, "")
					Expect(args).To(ContainElements("--skip-go-version-check", flagPlugins, ContainSubstring(pluginGoKubebuilderV4),
						"--domain", fooDomain, "--repo", barRepo, "--multigroup"))
				})
			})

			When("namespaced flag is enabled", func() {
				It("includes --namespaced in init args", func() {
					cfg := &fakeConfig{
						pluginChain: []string{pluginGoKubebuilderV4},
						domain:      fooDomain,
						repo:        barRepo,
						namespaced:  true,
					}
					store := &fakeStore{cfg: cfg}
					args := getInitArgs(store, &Generate{SkipGoVersionCheck: true}, "")
					Expect(args).To(ContainElements("--skip-go-version-check", flagPlugins, ContainSubstring(pluginGoKubebuilderV4),
						"--domain", fooDomain, "--repo", barRepo, "--namespaced"))
				})
			})

			When("both multigroup and namespaced are enabled", func() {
				It("includes both flags in init args", func() {
					cfg := &fakeConfig{
						pluginChain: []string{pluginGoKubebuilderV4},
						domain:      fooDomain,
						repo:        barRepo,
						multigroup:  true,
						namespaced:  true,
					}
					store := &fakeStore{cfg: cfg}
					args := getInitArgs(store, &Generate{SkipGoVersionCheck: true}, "")
					Expect(args).To(ContainElements("--skip-go-version-check", flagPlugins, ContainSubstring(pluginGoKubebuilderV4),
						"--domain", fooDomain, "--repo", barRepo, "--multigroup", "--namespaced"))
				})
			})
		})

		Context("when skipGoVersionCheck is false", func() {
			It("does not include --skip-go-version-check", func() {
				cfg := &fakeConfig{pluginChain: []string{pluginGoKubebuilderV4}, domain: fooDomain, repo: barRepo}
				store := &fakeStore{cfg: cfg}
				args := getInitArgs(store, &Generate{SkipGoVersionCheck: false}, "")
				Expect(args).NotTo(ContainElement("--skip-go-version-check"))
				Expect(args).To(ContainElements(flagPlugins, ContainSubstring(pluginGoKubebuilderV4),
					"--domain", fooDomain, "--repo", barRepo))
			})
		})
	})

	// getGVKFlags
	Context("getGVKFlags", func() {
		It("returns correct flags", func() {
			res := resource.Resource{Plural: "foos"}
			res.Group = exampleDomain
			res.Version = "v1"
			res.Kind = fooKind
			flags := getGVKFlags(res)
			Expect(flags).To(ContainElements(
				"--plural", "foos", "--group", exampleDomain, "--version", "v1", "--kind", fooKind,
			))
		})
	})

	// getGVKFlagsFromDeployImage
	Context("getGVKFlagsFromDeployImage", func() {
		It("returns correct flags", func() {
			rd := deployimagev1alpha1.ResourceData{Group: exampleDomain, Version: "v1", Kind: fooKind}
			flags := getGVKFlagsFromDeployImage(rd)
			Expect(flags).To(ContainElements("--group", exampleDomain, "--version", "v1", "--kind", fooKind))
		})
	})

	// getDeployImageOptions
	Context("getDeployImageOptions", func() {
		It("returns correct options", func() {
			rd := deployimagev1alpha1.ResourceData{}
			rd.Options.Image = "test-kubebuilder"
			rd.Options.ContainerCommand = "echo 'Hello'"
			rd.Options.ContainerPort = "8000"
			rd.Options.RunAsUser = fixtureTest
			opts := getDeployImageOptions(rd)
			Expect(opts).To(ContainElements("--image=test-kubebuilder",
				"--image-container-command=echo 'Hello'",
				"--image-container-port=8000",
				"--run-as-user="+fixtureTest,
				"--plugins=deploy-image.go.kubebuilder.io/v1-alpha"))
		})
	})

	// getAPIResourceFlags
	Context("getAPIResourceFlags", func() {
		var res resource.Resource
		BeforeEach(func() {
			res = resource.Resource{API: &resource.API{}}
		})

		Context("returns correct flags", func() {
			It("for nil API with Controller set", func() {
				res.Controller = true
				// Controllers are now created separately, so always --controller=false in API step
				Expect(getAPIResourceFlags(res)).To(ContainElements("--resource=false", "--controller=false"))
			})
			It("for non nil API (namespaced not set) with Controller not set", func() {
				res.API.CRDVersion = "v1"
				res.API.Namespaced = true
				Expect(getAPIResourceFlags(res)).To(ContainElements("--resource", "--namespaced", "--controller=false"))
			})
			It("for non nil API (namespaced set) with Controller not set", func() {
				res.API.CRDVersion = "v1"
				res.API.Namespaced = false
				Expect(getAPIResourceFlags(res)).To(ContainElements("--resource", "--namespaced=false", "--controller=false"))
			})
		})

		Context("for Server-Side Apply", func() {
			It("includes --ssa only when SSA is enabled on the resource", func() {
				res.API.CRDVersion = "v1"
				res.API.Namespaced = true
				res.API.SSA = true
				Expect(getAPIResourceFlags(res)).To(ContainElements(
					"--resource", "--namespaced", "--ssa", "--controller=false"))
			})

			It("omits --ssa when SSA is disabled on the resource", func() {
				res.API.CRDVersion = "v1"
				res.API.Namespaced = true
				res.API.SSA = false
				Expect(getAPIResourceFlags(res)).NotTo(ContainElement("--ssa"))
			})

			It("omits --ssa for resource-less entries (--resource=false)", func() {
				// A nil API means --resource=false; --ssa must never be emitted.
				res.API = nil
				flags := getAPIResourceFlags(res)
				Expect(flags).To(ContainElement("--resource=false"))
				Expect(flags).NotTo(ContainElement("--ssa"))
			})
		})
	})

	// getWebhookResourceFlags
	Context("getWebhookResourceFlags", func() {
		It("returns correct flags for specified resources", func() {
			res := resource.Resource{
				Path:     certManagerAPIPath,
				GVK:      resource.GVK{Group: exampleDomain, Version: "v1", Kind: exampleKind, Domain: fixtureTest},
				External: true,
				Webhooks: &resource.Webhooks{
					Validation: true,
					Defaulting: true,
					Conversion: true,
					Spoke:      []string{"v2"},
				},
			}
			flags := getWebhookResourceFlags(res)
			Expect(flags).To(ContainElements(
				"--external-api-path", certManagerAPIPath,
				"--external-api-domain", fixtureTest,
				"--programmatic-validation", "--defaulting", "--conversion", "--spoke", "v2",
			))
		})

		It("returns correct flags for external resources with module version", func() {
			res := resource.Resource{
				Path:     certManagerAPIPath,
				Module:   "github.com/cert-manager/cert-manager@v1.18.2",
				GVK:      resource.GVK{Group: certManagerGroup, Version: "v1", Kind: certificateKind, Domain: "io"},
				External: true,
				Webhooks: &resource.Webhooks{
					Defaulting: true,
				},
			}
			flags := getWebhookResourceFlags(res)
			Expect(flags).To(ContainElement("--external-api-path"))
			Expect(flags).To(ContainElement(certManagerAPIPath))
			Expect(flags).To(ContainElement("--external-api-domain"))
			Expect(flags).To(ContainElement("io"))
			Expect(flags).To(ContainElement("--external-api-module"))
			Expect(flags).To(ContainElement("github.com/cert-manager/cert-manager@v1.18.2"))
			Expect(flags).To(ContainElement("--defaulting"))
		})

		It("returns correct flags for external resources WITHOUT module version", func() {
			res := resource.Resource{
				Path:     certManagerAPIPath,
				Module:   "", // No module specified
				GVK:      resource.GVK{Group: certManagerGroup, Version: "v1", Kind: certificateKind, Domain: "io"},
				External: true,
				Webhooks: &resource.Webhooks{
					Defaulting: true,
				},
			}
			flags := getWebhookResourceFlags(res)
			Expect(flags).To(ContainElement("--external-api-path"))
			Expect(flags).To(ContainElement(certManagerAPIPath))
			Expect(flags).To(ContainElement("--external-api-domain"))
			Expect(flags).To(ContainElement("io"))
			Expect(flags).NotTo(ContainElement("--external-api-module"))
			Expect(flags).To(ContainElement("--defaulting"))
		})
	})
})

var _ = Describe("generate: create-helpers", func() {
	var (
		kbc         *utils.TestContext
		err         error
		originalDir string
	)

	BeforeEach(func() {
		// Initialize TestContext
		kbc, err = utils.NewTestContext("kubebuilder", "GO111MODULE=on")
		Expect(err).NotTo(HaveOccurred())
		Expect(kbc.Prepare()).To(Succeed())

		// Setup mock kubebuilder environment
		originalDir = setupKubebuilderMockEnvironment(kbc)
	})

	AfterEach(func() {
		By("cleaning up test artifacts")
		// Restore original working directory
		Expect(os.Chdir(originalDir)).To(Succeed())
		kbc.Destroy()
	})

	// createAPI
	Describe("createAPI", func() {
		Context("Without External flag", func() {
			It("runs kubebuilder create api successfully for a resource", func() {
				res := resource.Resource{
					GVK:        resource.GVK{Group: exampleDomain, Version: "v1", Kind: exampleKind, Domain: fixtureTest},
					Plural:     examplesDir,
					API:        &resource.API{Namespaced: true},
					Controller: true,
				}
				// Run createAPI and verify no errors
				Expect(createAPI(res)).To(Succeed())
			})
		})

		Context("With External flag set", func() {
			It("runs kubebuilder create api successfully for a resource", func() {
				res := resource.Resource{
					GVK:        resource.GVK{Group: exampleDomain, Version: "v1", Kind: exampleKind, Domain: "external"},
					Plural:     examplesDir,
					API:        &resource.API{Namespaced: true},
					Controller: true,
					External:   true,
					Path:       certManagerAPIPath,
				}
				// Run createAPI and verify no errors
				Expect(createAPI(res)).To(Succeed())
			})

			It("runs kubebuilder create api successfully with module version", func() {
				res := resource.Resource{
					GVK:        resource.GVK{Group: certManagerGroup, Version: "v1", Kind: certificateKind, Domain: "io"},
					Plural:     "certificates",
					API:        nil, // External resources typically don't scaffold API
					Controller: true,
					External:   true,
					Path:       certManagerAPIPath,
					Module:     "github.com/cert-manager/cert-manager@v1.18.2",
				}
				// Run createAPI and verify no errors
				Expect(createAPI(res)).To(Succeed())
			})

			It("runs kubebuilder create api successfully WITHOUT module version", func() {
				res := resource.Resource{
					GVK:        resource.GVK{Group: certManagerGroup, Version: "v1", Kind: certificateKind, Domain: "io"},
					Plural:     "certificates",
					API:        nil,
					Controller: true,
					External:   true,
					Path:       certManagerAPIPath,
					Module:     "", // No module specified
				}
				// Run createAPI and verify no errors
				Expect(createAPI(res)).To(Succeed())
			})
		})
	})

	// createWebhook
	Describe("createWebhook", func() {
		It("runs kubebuilder create webhook successfully for a resource", func() {
			res := resource.Resource{
				GVK:      resource.GVK{Group: exampleDomain, Version: "v1", Kind: exampleKind, Domain: fixtureTest},
				Plural:   examplesDir,
				Webhooks: &resource.Webhooks{WebhookVersion: "v1"},
			}
			// Run createWebhook and verify no errors
			Expect(createWebhook(res)).To(Succeed())
		})

		It("ignores web creation if webhook resource is empty", func() {
			res := resource.Resource{
				GVK:      resource.GVK{Group: exampleDomain, Version: "v1", Kind: exampleKind, Domain: fixtureTest},
				Plural:   examplesDir,
				Webhooks: &resource.Webhooks{},
			}
			// Run createWebhook and verify no errors
			Expect(createWebhook(res)).To(Succeed())
		})
	})

	Describe("createAPIWithDeployImage", func() {
		It("runs kubebuilder create api successfully with deploy image", func() {
			resourceData := deployimagev1alpha1.ResourceData{
				Group:   exampleDomain,
				Version: "v1",
				Kind:    exampleKind,
			}
			resourceData.Options.Image = "example-image"
			resourceData.Options.ContainerCommand = "run"
			resourceData.Options.ContainerPort = "8080"
			resourceData.Options.Image = "test"
			// Run createAPIWithDeployImage and verify no errors
			Expect(createAPIWithDeployImage(resourceData)).To(Succeed())
		})

		It("validates deploy-image works with external APIs without release version", func() {
			// This test validates that deploy-image plugin can work with external APIs
			// even without pinned versions (backward compatibility)
			resourceData := deployimagev1alpha1.ResourceData{
				Group:   certManagerGroup,
				Domain:  "io",
				Version: "v1",
				Kind:    certificateKind,
			}
			resourceData.Options.Image = "busybox:1.36.1"
			resourceData.Options.RunAsUser = "1001"
			// Run createAPIWithDeployImage and verify no errors
			Expect(createAPIWithDeployImage(resourceData)).To(Succeed())
		})

		It("validates deploy-image can be used alongside external APIs with release version", func() {
			// This test validates that when external APIs with release versions are used,
			// deploy-image plugin still works correctly
			// Note: The release field is stored in the Resource, not in DeployImage's ResourceData
			resourceData := deployimagev1alpha1.ResourceData{
				Group:   exampleDomain,
				Version: "v1",
				Kind:    "Memcached",
			}
			resourceData.Options.Image = "memcached:1.6.26"
			resourceData.Options.ContainerPort = "11211"
			// Run createAPIWithDeployImage and verify no errors
			Expect(createAPIWithDeployImage(resourceData)).To(Succeed())
		})
	})
})

var _ = Describe("generate: kubebuilder", func() {
	var (
		kbc                           *utils.TestContext
		err                           error
		originalDir                   string
		originalGetExecutablePathFunc func() (string, error)
	)

	BeforeEach(func() {
		// Save the original function
		originalGetExecutablePathFunc = getExecutablePathFunc

		// Initialize TestContext
		kbc, err = utils.NewTestContext("kubebuilder", "GO111MODULE=on")
		Expect(err).NotTo(HaveOccurred())
		Expect(kbc.Prepare()).To(Succeed())

		// Setup mock kubebuilder environment
		originalDir = setupKubebuilderMockEnvironment(kbc)

		// Mock getExecutablePathFunc to return the mock kubebuilder binary path
		getExecutablePathFunc = func() (string, error) {
			return filepath.Join(kbc.Dir, "kubebuilder"), nil
		}
	})

	AfterEach(func() {
		// Restore the original getExecutablePath function
		getExecutablePathFunc = originalGetExecutablePathFunc

		// Restore original working directory
		Expect(os.Chdir(originalDir)).To(Succeed())

		// Clean up test artifacts
		kbc.Destroy()
	})

	Context("kubebuilderInit", func() {
		It("runs kubebuilder init successfully", func() {
			cfg := &fakeConfig{
				pluginChain: []string{pluginGoKubebuilderV4},
				domain:      exampleDomain,
				repo:        exampleRepo,
			}
			store := &fakeStore{cfg: cfg}
			Expect(kubebuilderInit(store, &Generate{SkipGoVersionCheck: true}, "")).To(Succeed())
		})
	})

	Context("kubebuilderCreate", func() {
		It("runs kubebuilder create successfully for resources", func() {
			cfg := &fakeConfig{
				resources: []resource.Resource{
					{Plural: "foos", GVK: resource.GVK{Group: exampleDomain, Version: "v1", Kind: fooKind}},
					{Plural: "bars", GVK: resource.GVK{Group: exampleDomain, Version: "v1", Kind: "Bar"}},
				},
			}
			store := &fakeStore{cfg: cfg}
			Expect(kubebuilderCreate(store)).To(Succeed())
		})
	})

	Context("kubebuilderGrafanaEdit", func() {
		It("runs kubebuilder edit successfully for Grafana plugin", func() {
			// Run kubebuilderGrafanaEdit and verify no errors
			Expect(kubebuilderGrafanaEdit()).To(Succeed())
		})
	})

	Context("kubebuilderHelmEdit", func() {
		It("runs kubebuilder edit successfully for Helm plugin", func() {
			// Run kubebuilderHelmEdit and verify no errors
			Expect(kubebuilderHelmEdit(true)).To(Succeed())
		})
	})
})

var _ = Describe("generate: hasHelmPlugin", func() {
	It("returns true if v2-alpha plugin present", func() {
		cfg := &fakeConfig{plugins: map[string]any{pluginHelmKubebuilderV2Alpha: true}}
		store := &fakeStore{cfg: cfg}
		hasPlugin, isV2Alpha := hasHelmPlugin(store)
		Expect(hasPlugin).To(BeTrue())
		Expect(isV2Alpha).To(BeTrue())
	})

	It("returns true if v1-alpha plugin present", func() {
		cfg := &fakeConfig{plugins: map[string]any{pluginHelmKubebuilderV1Alpha: true}}
		store := &fakeStore{cfg: cfg}
		hasPlugin, isV2Alpha := hasHelmPlugin(store)
		Expect(hasPlugin).To(BeTrue())
		Expect(isV2Alpha).To(BeFalse())
	})

	It("returns false if both plugins not found", func() {
		cfg := &fakeConfig{pluginErr: &config.PluginKeyNotFoundError{Key: "helm.kubebuilder.io/v2-beta"}}
		store := &fakeStore{cfg: cfg}
		hasPlugin, isV2Alpha := hasHelmPlugin(store)
		Expect(hasPlugin).To(BeFalse())
		Expect(isV2Alpha).To(BeFalse())
	})
})

var _ = Describe("generate: boilerplate preservation", func() {
	Describe("getInitArgs with boilerplate handling", func() {
		It("should include --license none when no boilerplate exists", func() {
			cfg := &fakeConfig{
				pluginChain: []string{pluginGoKubebuilderV4},
				domain:      exampleDomain,
				repo:        exampleRepo,
			}
			store := &fakeStore{cfg: cfg}
			args := getInitArgs(store, &Generate{}, "") // tempLicenseFile = "" (no boilerplate)

			Expect(args).To(ContainElement("--license"))
			// Find --license and verify next element is "none"
			foundLicenseNone := false
			for i, arg := range args {
				if arg == "--license" && i+1 < len(args) && args[i+1] == "none" {
					foundLicenseNone = true
					break
				}
			}
			Expect(foundLicenseNone).To(BeTrue(), "Expected --license none when no boilerplate exists")
		})

		It("should use --license-file when temp file is provided", func() {
			cfg := &fakeConfig{
				pluginChain: []string{pluginGoKubebuilderV4},
				domain:      exampleDomain,
				repo:        exampleRepo,
			}
			store := &fakeStore{cfg: cfg}
			args := getInitArgs(store, &Generate{}, "/tmp/preserved-license.txt") // tempLicenseFile provided

			// Should have --license-file flag (not --license none)
			Expect(args).To(ContainElement("--license-file"))
			Expect(args).To(ContainElement("/tmp/preserved-license.txt"))

			// Should NOT have --license none
			foundLicenseNone := false
			for i, arg := range args {
				if arg == "--license" && i+1 < len(args) && args[i+1] == "none" {
					foundLicenseNone = true
					break
				}
			}
			Expect(foundLicenseNone).To(BeFalse(), "Should not use --license none when temp file is provided")
		})

		It("should always include either --license-file or --license none", func() {
			cfg := &fakeConfig{
				pluginChain: []string{pluginGoKubebuilderV4},
				domain:      exampleDomain,
				repo:        exampleRepo,
			}
			store := &fakeStore{cfg: cfg}

			// With temp file: should have --license-file
			argsWithFile := getInitArgs(store, &Generate{}, "/tmp/custom-license.txt")
			Expect(argsWithFile).To(ContainElement("--license-file"))
			Expect(argsWithFile).To(ContainElement("/tmp/custom-license.txt"))

			// Without temp file: should have --license none
			argsWithoutFile := getInitArgs(store, &Generate{}, "")
			Expect(argsWithoutFile).To(ContainElement("--license"))
			Expect(argsWithoutFile).To(ContainElement("none"))
		})
	})
})

var _ = Describe("generate: migrate-plugins", func() {
	const (
		grafanaPluginKey     = "grafana.kubebuilder.io/v1-alpha"
		autoupdatePluginKey  = "autoupdate.kubebuilder.io/v1-alpha"
		deployImagePluginKey = "deploy-image.kubebuilder.io/v1-alpha"
	)

	var (
		kbc         *utils.TestContext
		tmpDir      string
		err         error
		originalDir string
	)

	BeforeEach(func() {
		// Initialize TestContext
		kbc, err = utils.NewTestContext("kubebuilder", "GO111MODULE=on")
		Expect(err).NotTo(HaveOccurred())
		Expect(kbc.Prepare()).To(Succeed())

		// Setup mock kubebuilder environment
		originalDir = setupKubebuilderMockEnvironment(kbc)

		tmpDir = kbc.Dir
	})

	AfterEach(func() {
		By("cleaning up test artifacts")
		// Restore original working directory
		Expect(os.Chdir(originalDir)).To(Succeed())
		kbc.Destroy()
	})

	Context("migrateGrafanaPlugin", func() {
		It("skips migration as Grafana plugin not found", func() {
			cfg := &fakeConfig{pluginErr: &config.PluginKeyNotFoundError{Key: grafanaPluginKey}}
			store := &fakeStore{cfg: cfg}
			Expect(migrateGrafanaPlugin(store, "src", "dest")).To(Succeed())
		})

		It("returns error if decoding Grafana plugin config fails", func() {
			cfg := &fakeConfig{
				pluginErr: fmt.Errorf("decoding error"),
				plugins:   map[string]any{grafanaPluginKey: true},
			}
			store := &fakeStore{cfg: cfg}
			Expect(migrateGrafanaPlugin(store, "src", "dest")).NotTo(Succeed())
		})

		Context("success", func() {
			var src, dest string
			BeforeEach(func() {
				src = filepath.Join(tmpDir, "src")
				dest = filepath.Join(tmpDir, "dest")
				Expect(os.MkdirAll(filepath.Join(src, "grafana/custom-metrics"), 0o755)).To(Succeed())
				Expect(os.WriteFile(filepath.Join(src, "grafana/custom-metrics/config.yaml"),
					[]byte("config"), 0o755)).To(Succeed())
				Expect(os.MkdirAll(filepath.Join(dest, "grafana/custom-metrics"), 0o755)).To(Succeed())
			})

			AfterEach(func() {
				Expect(os.RemoveAll(src)).To(Succeed())
				Expect(os.RemoveAll(dest)).To(Succeed())
			})

			It("migrates Grafana plugin successfully", func() {
				cfg := &fakeConfig{plugins: map[string]any{grafanaPluginKey: true}}
				store := &fakeStore{cfg: cfg}
				Expect(migrateGrafanaPlugin(store, src, dest)).To(Succeed())
				b, err := os.ReadFile(filepath.Join(dest, "grafana/custom-metrics/config.yaml"))
				Expect(err).NotTo(HaveOccurred())
				Expect(string(b)).To(Equal("config"))
			})
		})
	})

	Context("migrateAutoUpdatePlugin", func() {
		It("skips migration as AutoUpdate plugin not found", func() {
			cfg := &fakeConfig{pluginErr: &config.PluginKeyNotFoundError{Key: autoupdatePluginKey}}
			store := &fakeStore{cfg: cfg}
			Expect(migrateAutoUpdatePlugin(store)).To(Succeed())
		})

		It("returns error if failed to decode Auto Update plugin", func() {
			cfg := &fakeConfig{
				pluginErr: fmt.Errorf("decoding error"),
				plugins:   map[string]any{autoupdatePluginKey: true},
			}
			store := &fakeStore{cfg: cfg}
			Expect(migrateAutoUpdatePlugin(store)).NotTo(Succeed())
		})

		It("migrates Auto Update plugin successfully", func() {
			cfg := &fakeConfig{
				plugins: map[string]any{
					autoupdatePluginKey: autoupdatev1alpha.PluginConfig{},
				},
			}
			store := &fakeStore{cfg: cfg}
			Expect(migrateAutoUpdatePlugin(store)).To(Succeed())
		})
	})

	Context("migrateDeployImagePlugin", func() {
		It("returns error if failed to decode Deploy Image plugin", func() {
			cfg := &fakeConfig{pluginErr: &config.PluginKeyNotFoundError{Key: deployImagePluginKey}}
			store := &fakeStore{cfg: cfg}
			Expect(migrateDeployImagePlugin(store)).To(Succeed())
		})

		It("returns error if decoding Deploy Image plugin config fails", func() {
			cfg := &fakeConfig{
				pluginErr: fmt.Errorf("decoding error"),
				plugins:   map[string]any{deployImagePluginKey: true},
			}
			store := &fakeStore{cfg: cfg}
			Expect(migrateDeployImagePlugin(store)).NotTo(Succeed())
		})

		It("migrates Deploy Image plugin successfully", func() {
			cfg := &fakeConfig{plugins: map[string]any{deployImagePluginKey: true}}
			store := &fakeStore{cfg: cfg}

			// Mock resources for the plugin
			resources := []deployimagev1alpha1.ResourceData{
				{
					Group:   exampleDomain,
					Version: "v1",
					Kind:    exampleKind,
				},
			}

			cfg.pluginChain = []string{deployImagePluginKey}
			store.cfg = cfg

			// Use the mocked resources
			for _, r := range resources {
				Expect(createAPIWithDeployImage(r)).To(Succeed())
			}
			Expect(migrateDeployImagePlugin(store)).To(Succeed())
		})
	})
})

var _ = Describe("Generate", func() {
	var (
		kbc                           *utils.TestContext
		err                           error
		g                             *Generate
		originalDir                   string
		originalGetExecutablePathFunc func() (string, error)
	)

	BeforeEach(func() {
		// Save the original function
		originalGetExecutablePathFunc = getExecutablePathFunc

		// Initialize TestContext
		kbc, err = utils.NewTestContext("kubebuilder", "GO111MODULE=on")
		Expect(err).NotTo(HaveOccurred())
		Expect(kbc.Prepare()).To(Succeed())

		// Setup mock kubebuilder environment
		originalDir = setupKubebuilderMockEnvironment(kbc)

		// Mock getExecutablePathFunc to return the mock kubebuilder binary path
		getExecutablePathFunc = func() (string, error) {
			return filepath.Join(kbc.Dir, "kubebuilder"), nil
		}

		// Initialize Generate
		g = &Generate{InputDir: kbc.Dir}
	})

	AfterEach(func() {
		// Restore the original getExecutablePath function
		getExecutablePathFunc = originalGetExecutablePathFunc

		// Restore original working directory
		Expect(os.Chdir(originalDir)).To(Succeed())

		By("cleaning up test artifacts")
		kbc.Destroy()
	})

	Context("outputDir is non empty", func() {
		It("scaffolds the project in output dir", func() {
			g.OutputDir = kbc.Dir
			Expect(g.Generate()).To(Succeed())
		})
	})
})
