package test

import (
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"reflect"

	"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/fix"
	"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/test"
	"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/userinfo"
	"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/values"
	kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"sigs.k8s.io/yaml"
)

type options struct {
	fileName string
	save     bool
	force    bool
	compress bool
}

func (o options) validate(dirs ...string) error {
	if o.fileName == "" {
		return errors.New("file-name must not be set to an empty string")
	}
	if len(dirs) == 0 {
		return errors.New("at least one test directory is required")
	}
	return nil
}

func (o options) execute(out io.Writer, dirs ...string) error {
	var testCases []test.TestCase
	for _, arg := range dirs {
		tests, err := test.LoadTests(arg, o.fileName)
		if err != nil {
			return err
		}
		testCases = append(testCases, tests...)
	}
	for _, testCase := range testCases {
		fmt.Fprintf(out, "Processing test file (%s)...", testCase.Path)
		fmt.Fprintln(out)
		if testCase.Err != nil {
			fmt.Fprintf(out, "  ERROR: loading test file (%s): %s", testCase.Path, testCase.Err)
			fmt.Fprintln(out)
			continue
		}
		fixed := *testCase.Test
		if fixed.ObjectMeta.Name == "" && fixed.Name == "" {
			fmt.Fprintln(out, "  WARNING: name is not set")
			fixed.ObjectMeta.Name = filepath.Base(testCase.Path)
		}
		fixed, messages, err := fix.FixTest(fixed, o.compress)
		for _, warning := range messages {
			fmt.Fprintln(out, "  WARNING:", warning)
		}
		if err != nil {
			fmt.Fprintln(out, "  ERROR:", err)
			continue
		}
		needsSave := !reflect.DeepEqual(testCase.Test, &fixed)
		if o.save && (o.force || needsSave) {
			fmt.Fprintf(out, "  Saving test file (%s)...", testCase.Path)
			fmt.Fprintln(out)
			untyped, err := kubeutils.ObjToUnstructured(fixed)
			if err != nil {
				fmt.Fprintf(out, "    ERROR: converting to unstructured: %s", err)
				fmt.Fprintln(out)
				continue
			}
			unstructured.RemoveNestedField(untyped.UnstructuredContent(), "metadata", "creationTimestamp")
			unstructured.RemoveNestedField(untyped.UnstructuredContent(), "metadata", "generation")
			unstructured.RemoveNestedField(untyped.UnstructuredContent(), "metadata", "uid")
			jsonBytes, err := untyped.MarshalJSON()
			if err != nil {
				fmt.Fprintf(out, "    ERROR: converting to json: %s", err)
				fmt.Fprintln(out)
				continue
			}
			yamlBytes, err := yaml.JSONToYAML(jsonBytes)
			if err != nil {
				fmt.Fprintf(out, "    ERROR: converting to yaml: %s", err)
				fmt.Fprintln(out)
				continue
			}
			if err := os.WriteFile(testCase.Path, yamlBytes, os.ModePerm); err != nil {
				fmt.Fprintf(out, "    ERROR: saving test file (%s): %s", testCase.Path, err)
				fmt.Fprintln(out)
				continue
			}
			fmt.Fprintln(out, "    OK")
		}
		if testCase.Test.UserInfo != "" {
			fmt.Fprintf(out, "  Processing user info file (%s)...\n", testCase.Test.UserInfo)
			path := filepath.Join(testCase.Dir(), testCase.Test.UserInfo)
			info, err := userinfo.Load(nil, path, "")
			if err != nil {
				fmt.Fprintf(out, "    ERROR: failed to load user info: %s\n", err)
				continue
			}
			fixed, messages, err := fix.FixUserInfo(*info)
			for _, warning := range messages {
				fmt.Fprintln(out, "  WARNING:", warning)
			}
			if err != nil {
				fmt.Fprintln(out, "  ERROR:", err)
				continue
			}
			needsSave := !reflect.DeepEqual(info, &fixed)
			if o.save && (o.force || needsSave) {
				fmt.Fprintf(out, "  Saving user info file (%s)...\n", path)
				untyped, err := kubeutils.ObjToUnstructured(fixed)
				if err != nil {
					fmt.Fprintf(out, "    ERROR: converting to unstructured: %s\n", err)
					continue
				}
				unstructured.RemoveNestedField(untyped.UnstructuredContent(), "metadata", "creationTimestamp")
				unstructured.RemoveNestedField(untyped.UnstructuredContent(), "metadata", "generation")
				unstructured.RemoveNestedField(untyped.UnstructuredContent(), "metadata", "uid")
				if item, _, _ := unstructured.NestedMap(untyped.UnstructuredContent(), "metadata"); len(item) == 0 {
					unstructured.RemoveNestedField(untyped.UnstructuredContent(), "metadata")
				}
				jsonBytes, err := untyped.MarshalJSON()
				if err != nil {
					fmt.Fprintf(out, "    ERROR: converting to json: %s\n", err)
					continue
				}
				yamlBytes, err := yaml.JSONToYAML(jsonBytes)
				if err != nil {
					fmt.Fprintf(out, "    ERROR: converting to yaml: %s\n", err)
					continue
				}
				if err := os.WriteFile(path, yamlBytes, os.ModePerm); err != nil {
					fmt.Fprintf(out, "    ERROR: saving user info file (%s): %s\n", path, err)
					continue
				}
				fmt.Fprintln(out, "    OK")
			}
		}
		if testCase.Test.Variables != "" {
			fmt.Fprintf(out, "  Processing values file (%s)...\n", testCase.Test.Variables)
			path := filepath.Join(testCase.Dir(), testCase.Test.Variables)
			values, err := values.Load(nil, path)
			if err != nil {
				fmt.Fprintf(out, "    ERROR: failed to load values: %s\n", err)
				continue
			}
			fixed, messages, err := fix.FixValues(*values)
			for _, warning := range messages {
				fmt.Fprintln(out, "  WARNING:", warning)
			}
			if err != nil {
				fmt.Fprintln(out, "  ERROR:", err)
				continue
			}
			needsSave := !reflect.DeepEqual(values, &fixed)
			if o.save && (o.force || needsSave) {
				fmt.Fprintf(out, "  Saving values file (%s)...\n", path)
				untyped, err := kubeutils.ObjToUnstructured(fixed)
				if err != nil {
					fmt.Fprintf(out, "    ERROR: converting to unstructured: %s\n", err)
					continue
				}
				unstructured.RemoveNestedField(untyped.UnstructuredContent(), "metadata", "creationTimestamp")
				unstructured.RemoveNestedField(untyped.UnstructuredContent(), "metadata", "generation")
				unstructured.RemoveNestedField(untyped.UnstructuredContent(), "metadata", "uid")
				if item, _, _ := unstructured.NestedMap(untyped.UnstructuredContent(), "metadata"); len(item) == 0 {
					unstructured.RemoveNestedField(untyped.UnstructuredContent(), "metadata")
				}
				jsonBytes, err := untyped.MarshalJSON()
				if err != nil {
					fmt.Fprintf(out, "    ERROR: converting to json: %s\n", err)
					continue
				}
				yamlBytes, err := yaml.JSONToYAML(jsonBytes)
				if err != nil {
					fmt.Fprintf(out, "    ERROR: converting to yaml: %s\n", err)
					continue
				}
				if err := os.WriteFile(path, yamlBytes, os.ModePerm); err != nil {
					fmt.Fprintf(out, "    ERROR: saving values file (%s): %s\n", path, err)
					continue
				}
				fmt.Fprintln(out, "    OK")
			}
		}
		fmt.Fprintln(out)
	}
	fmt.Fprintln(out, "Done.")
	return nil
}