package utils_test

import (
	"encoding/json"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"

	common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
	"github.com/grafana/grafana/pkg/apimachinery/utils"
)

type TestResource struct {
	metav1.TypeMeta `json:",inline"`
	// Standard object's metadata
	// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Spec Spec `json:"spec,omitempty"`

	// Read/write raw status
	Status Spec `json:"status,omitempty"`

	// Secure values as map
	Secure common.InlineSecureValues `json:"secure,omitempty"`
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TestResource) DeepCopyInto(out *TestResource) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
	in.Spec.DeepCopyInto(&out.Spec)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Playlist.
func (in *TestResource) DeepCopy() *TestResource {
	if in == nil {
		return nil
	}
	out := new(TestResource)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TestResource) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// Spec defines model for Spec.
type Spec struct {
	// Name of the object.
	Title string `json:"title"`
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Spec) DeepCopyInto(out *Spec) {
	*out = *in
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Spec.
func (in *Spec) DeepCopy() *Spec {
	if in == nil {
		return nil
	}
	out := new(Spec)
	in.DeepCopyInto(out)
	return out
}

type TestResource2 struct {
	metav1.TypeMeta `json:",inline"`
	// Standard object's metadata
	// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Spec Spec2 `json:"spec,omitempty"`

	// Exercise read/write pointer status
	Status *Spec `json:"status,omitempty"`

	// This time defined with a strict struct
	Secure ExplicitSecureValues `json:"secure,omitempty"`
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TestResource2) DeepCopyInto(out *TestResource2) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
	in.Spec.DeepCopyInto(&out.Spec)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Playlist.
func (in *TestResource2) DeepCopy() *TestResource2 {
	if in == nil {
		return nil
	}
	out := new(TestResource2)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TestResource2) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// Spec defines model for Spec.
type ExplicitSecureValues struct {
	// Non-pointer
	Value1 common.InlineSecureValue `json:"v1,omitempty"`

	// Pointer value
	Value2 common.InlineSecureValue `json:"v2,omitempty"`
}

// Spec defines model for Spec.
type Spec2 struct{}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Spec2) DeepCopyInto(out *Spec2) {
	*out = *in
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Spec.
func (in *Spec2) DeepCopy() *Spec2 {
	if in == nil {
		return nil
	}
	out := new(Spec2)
	in.DeepCopyInto(out)
	return out
}

func TestMetaAccessor(t *testing.T) {
	repoInfo := utils.ManagerProperties{
		Kind:     utils.ManagerKindRepo,
		Identity: "test",
	}
	sourceInfo := utils.SourceProperties{
		Path:     "a/b/c",
		Checksum: "kkk",
	}

	t.Run("fails for non resource objects", func(t *testing.T) {
		_, err := utils.MetaAccessor(nil)
		require.Error(t, err)

		_, err = utils.MetaAccessor("hello")
		require.Error(t, err)

		_, err = utils.MetaAccessor(unstructured.Unstructured{})
		require.Error(t, err) // Not a pointer!

		_, err = utils.MetaAccessor(&unstructured.Unstructured{})
		require.NoError(t, err) // Must be a pointer

		_, err = utils.MetaAccessor(&TestResource{
			Spec: Spec{
				Title: "HELLO",
			},
		})
		require.NoError(t, err) // Must be a pointer
	})

	t.Run("get and set properties", func(t *testing.T) {
		obj, err := utils.MetaAccessor(&unstructured.Unstructured{
			Object: map[string]any{},
		})
		require.NoError(t, err)
		rv := obj.GetResourceVersion()
		require.Equal(t, "", rv)

		_, ok := obj.GetRuntimeObject()
		require.True(t, ok)
		anno := obj.GetAnnotations()
		require.Nil(t, anno)

		rvInt, err := obj.GetResourceVersionInt64()
		require.NoError(t, err)
		require.Equal(t, int64(0), rvInt)

		obj.SetResourceVersion("not a number")
		rv = obj.GetResourceVersion()
		require.Equal(t, "not a number", rv)
		rvInt, err = obj.GetResourceVersionInt64()
		require.Error(t, err)
		require.Equal(t, int64(0), rvInt)

		obj.SetUpdatedBy("updatedBy")
		require.Equal(t, "updatedBy", obj.GetUpdatedBy())
		anno = obj.GetAnnotations()
		require.Len(t, anno, 1) // One key
		obj.SetAnnotation(utils.AnnoKeyUpdatedBy, "")
		anno = obj.GetAnnotations()
		require.Empty(t, anno) // removed the key

		obj.SetCreatedBy("createdBy")
		require.Equal(t, "createdBy", obj.GetCreatedBy())

		obj.SetFolder("folder")
		require.Equal(t, "folder", obj.GetFolder())
	})

	t.Run("get and set grafana labels (unstructured)", func(t *testing.T) {
		res := &unstructured.Unstructured{
			Object: map[string]any{},
		}
		meta, err := utils.MetaAccessor(res)
		require.NoError(t, err)

		// should return 0 when not set
		require.Equal(t, meta.GetDeprecatedInternalID(), int64(0))

		// 0 is not allowed
		meta.SetDeprecatedInternalID(0)
		require.Equal(t, map[string]string(nil), res.GetLabels())

		// should be able to set and get
		meta.SetDeprecatedInternalID(1)
		require.Equal(t, map[string]string{
			"grafana.app/deprecatedInternalID": "1",
		}, res.GetLabels())
		require.Equal(t, meta.GetDeprecatedInternalID(), int64(1))
	})

	t.Run("get and set grafana metadata (unstructured)", func(t *testing.T) {
		// Error reading spec+status when missing
		res := &unstructured.Unstructured{
			Object: map[string]any{},
		}
		meta, err := utils.MetaAccessor(res)
		require.NoError(t, err)
		spec, err := meta.GetSpec()
		require.Error(t, err)
		require.Nil(t, spec)
		status, err := meta.GetStatus()
		require.Error(t, err)
		require.Nil(t, status)

		// Now set a spec and status
		res.Object = map[string]any{
			"spec": map[string]any{
				"hello": "world",
				"title": "Title",
			},
			"status": map[string]any{
				"sloth": "🦥",
			},
		}
		require.Equal(t, "", meta.GetFolder())
		require.Equal(t, "", meta.GetAnnotation("missing annotation"))

		meta.SetManagerProperties(repoInfo)
		meta.SetFolder("folderUID")

		require.Equal(t, map[string]string{
			"grafana.app/managedBy": "repo",
			"grafana.app/managerId": "test",
			"grafana.app/folder":    "folderUID",
		}, res.GetAnnotations())

		meta.SetNamespace("aaa")
		meta.SetResourceVersionInt64(12345)
		require.Equal(t, "aaa", res.GetNamespace())
		require.Equal(t, "aaa", meta.GetNamespace())

		rv, err := meta.GetResourceVersionInt64()
		require.NoError(t, err)
		require.Equal(t, int64(12345), rv)
		require.Equal(t, "Title", meta.FindTitle(""))

		// Make sure access to spec works for Unstructured
		spec, err = meta.GetSpec()
		require.NoError(t, err)
		require.Equal(t, res.Object["spec"], spec)
		spec = &map[string]string{"a": "b"}
		err = meta.SetSpec(spec)
		require.NoError(t, err)
		spec, err = meta.GetSpec()
		require.NoError(t, err)
		require.Equal(t, res.Object["spec"], spec)

		// Make sure access to spec works for Unstructured
		status, err = meta.GetStatus()
		require.NoError(t, err)
		require.Equal(t, res.Object["status"], status)
		status = &map[string]string{"a": "b"}
		err = meta.SetStatus(status)
		require.NoError(t, err)
		status, err = meta.GetStatus()
		require.NoError(t, err)
		require.Equal(t, res.Object["status"], status)

		// Check write/read on unstructured object
		err = meta.SetSecureValues(common.InlineSecureValues{
			"a": {Name: "bbbb"},
		})
		require.NoError(t, err)
		secure, err := meta.GetSecureValues()
		require.NoError(t, err)
		require.JSONEq(t, `{"a": {"name": "bbbb"}}`, asJSON(secure, true))
	})

	t.Run("get and set grafana metadata (TestResource)", func(t *testing.T) {
		res := &TestResource{
			Spec: Spec{
				Title: "test",
			},
			Secure: common.InlineSecureValues{
				"x": common.InlineSecureValue{
					Create: "hello",
				},
			},
			// Status is empty, but not nil!
		}
		meta, err := utils.MetaAccessor(res)
		require.NoError(t, err)

		meta.SetManagerProperties(repoInfo)
		meta.SetSourceProperties(sourceInfo)
		meta.SetFolder("folderUID")

		require.Equal(t, map[string]string{
			"grafana.app/managedBy":      "repo",
			"grafana.app/managerId":      "test",
			"grafana.app/sourcePath":     "a/b/c",
			"grafana.app/sourceChecksum": "kkk",
			"grafana.app/folder":         "folderUID",
		}, res.GetAnnotations())

		meta.SetNamespace("aaa")
		meta.SetResourceVersionInt64(12345)
		require.Equal(t, "aaa", res.GetNamespace())
		require.Equal(t, "aaa", meta.GetNamespace())

		rv, err := meta.GetResourceVersionInt64()
		require.NoError(t, err)
		require.Equal(t, int64(12345), rv)

		// Make sure access to spec works for Unstructured
		spec, err := meta.GetSpec()
		require.NoError(t, err)
		require.Equal(t, res.Spec, spec)
		err = meta.SetSpec(Spec{Title: "t2"})
		require.NoError(t, err)
		spec, err = meta.GetSpec()
		require.NoError(t, err)
		require.Equal(t, res.Spec, spec)
		require.Equal(t, `{"title":"t2"}`, asJSON(spec, false))

		// Check read/write status
		status, err := meta.GetStatus()
		require.NoError(t, err)
		require.NotNil(t, status)
		err = meta.SetStatus(Spec{Title: "111"})
		require.NoError(t, err)
		status, err = meta.GetStatus()
		require.NoError(t, err)
		require.Equal(t, res.Status, status)
		require.Equal(t, "111", res.Status.Title)
		require.Equal(t, `{"title":"111"}`, asJSON(status, false))

		// Check read/write secure values
		secure, err := meta.GetSecureValues()
		require.NoError(t, err)
		require.JSONEq(t, `{"x": {"create": "[REDACTED]"}}`, asJSON(secure, true))

		err = meta.SetSecureValues(common.InlineSecureValues{
			"a": {Name: "bbbb"},
		})
		require.NoError(t, err)
		secure, err = meta.GetSecureValues()
		require.NoError(t, err)
		require.JSONEq(t, `{"a": {"name": "bbbb"}}`, asJSON(secure, true))
	})

	t.Run("get and set grafana metadata (TestResource2)", func(t *testing.T) {
		res := &TestResource2{
			Spec:   Spec2{},
			Status: &Spec{Title: "X"},
			Secure: ExplicitSecureValues{
				Value1: common.InlineSecureValue{Name: "hello"},
			},
		}
		meta, err := utils.MetaAccessor(res)
		require.NoError(t, err)

		meta.SetManagerProperties(repoInfo)
		meta.SetFolder("folderUID")

		require.Equal(t, map[string]string{
			"grafana.app/managedBy": "repo",
			"grafana.app/managerId": "test",
			"grafana.app/folder":    "folderUID",
		}, res.GetAnnotations())

		meta.SetNamespace("aaa")
		meta.SetResourceVersionInt64(12345)
		require.Equal(t, "aaa", res.GetNamespace())
		require.Equal(t, "aaa", meta.GetNamespace())

		rv, err := meta.GetResourceVersionInt64()
		require.NoError(t, err)
		require.Equal(t, int64(12345), rv)

		// Make sure access to spec works for TestResource2
		spec, err := meta.GetSpec()
		require.NoError(t, err)
		require.Equal(t, res.Spec, spec)
		err = meta.SetSpec(Spec2{})
		require.NoError(t, err)
		spec, err = meta.GetSpec()
		require.NoError(t, err)
		require.Equal(t, res.Spec, spec)

		// Make sure access to spec works for TestResource2
		status, err := meta.GetStatus()
		require.NoError(t, err)
		require.Equal(t, res.Status, status)
		err = meta.SetStatus(&Spec{Title: "ZZ"})
		require.NoError(t, err)
		status, err = meta.GetStatus()
		require.NoError(t, err)
		require.Equal(t, res.Status, status)
		require.Equal(t, "ZZ", res.Status.Title)

		secure, err := meta.GetSecureValues()
		require.NoError(t, err)
		require.JSONEq(t, `{"v1": {"name": "hello"}}`, asJSON(secure, true))

		// Check that we can write
		err = meta.SetSecureValues(common.InlineSecureValues{
			"v2": {Name: "bbb"},
		})
		require.NoError(t, err)
		secure, err = meta.GetSecureValues()
		require.NoError(t, err)
		require.JSONEq(t, `{"v2": {"name": "bbb"}}`, asJSON(secure, true)) // NOTE: v1 was removed

		err = meta.SetSecureValues(common.InlineSecureValues{
			"UNKNOWN": {Name: "bbb"}, // not a valid name
		})
		require.Error(t, err)
	})

	t.Run("test reading old repo fields (now manager+source)", func(t *testing.T) {
		res := &TestResource2{
			ObjectMeta: metav1.ObjectMeta{
				Annotations: map[string]string{
					"grafana.app/repoName": "test",
					"grafana.app/repoPath": "a/b/c",
					"grafana.app/repoHash": "zzz",
					"grafana.app/folder":   "folderUID",
				},
			},
			Spec: Spec2{},
		}
		meta, err := utils.MetaAccessor(res)
		require.NoError(t, err)

		manager, ok := meta.GetManagerProperties()
		require.True(t, ok)
		require.Equal(t, utils.ManagerKindRepo, manager.Kind)
		require.Equal(t, "test", manager.Identity)

		source, ok := meta.GetSourceProperties()
		require.True(t, ok)
		require.Equal(t, "a/b/c", source.Path)
		require.Equal(t, "zzz", source.Checksum)
	})

	t.Run("blob info", func(t *testing.T) {
		info := &utils.BlobInfo{UID: "AAA", Size: 123, Hash: "xyz", MimeType: "application/json", Charset: "utf-8"}
		anno := info.String()
		require.Equal(t, "AAA; size=123; hash=xyz; mime=application/json; charset=utf-8", anno)
		copy := utils.ParseBlobInfo(anno)
		require.Equal(t, info, copy)
	})

	t.Run("find titles", func(t *testing.T) {
		// with a k8s object that has Spec.Title
		obj := &TestResource{
			TypeMeta: metav1.TypeMeta{
				Kind:       "TestKIND",
				APIVersion: "aaa/v1alpha2",
			},
			Spec: Spec{
				Title: "HELLO",
			},
		}

		meta, err := utils.MetaAccessor(obj)
		require.NoError(t, err)
		meta.SetManagerProperties(repoInfo)
		meta.SetSourceProperties(sourceInfo)
		meta.SetFolder("folderUID")

		require.Equal(t, map[string]string{
			"grafana.app/managedBy":      "repo",
			"grafana.app/managerId":      "test",
			"grafana.app/sourcePath":     "a/b/c",
			"grafana.app/sourceChecksum": "kkk",
			"grafana.app/folder":         "folderUID",
		}, obj.GetAnnotations())

		require.Equal(t, "HELLO", obj.Spec.Title)
		require.Equal(t, "HELLO", meta.FindTitle(""))
		obj.Spec.Title = ""
		require.Equal(t, "", meta.FindTitle("xxx"))

		gvk := meta.GetGroupVersionKind()
		require.Equal(t, "aaa/v1alpha2, Kind=TestKIND", gvk.String())

		// with a k8s object without Spec.Title
		obj2 := &TestResource2{}

		meta, err = utils.MetaAccessor(obj2)
		require.NoError(t, err)
		meta.SetManagerProperties(repoInfo)
		meta.SetFolder("folderUID")

		require.Equal(t, map[string]string{
			"grafana.app/managedBy": "repo",
			"grafana.app/managerId": "test",
			"grafana.app/folder":    "folderUID",
		}, obj2.GetAnnotations())

		require.Equal(t, "xxx", meta.FindTitle("xxx"))

		rt, ok := meta.GetRuntimeObject()
		require.Equal(t, obj2, rt)
		require.True(t, ok)

		spec, err := meta.GetSpec()
		require.Equal(t, obj2.Spec, spec)
		require.NoError(t, err)
	})

	t.Run("ManagerProperties", func(t *testing.T) {
		tests := []struct {
			name           string
			setProperties  *utils.ManagerProperties
			wantProperties utils.ManagerProperties
			wantOK         bool
		}{
			{
				name: "get default values",
				wantProperties: utils.ManagerProperties{
					Identity:    "",
					Kind:        utils.ManagerKindUnknown,
					AllowsEdits: false,
					Suspended:   false,
				},
				wantOK: false,
			},
			{
				name: "set and get valid values",
				setProperties: &utils.ManagerProperties{
					Identity:    "identity",
					Kind:        utils.ManagerKindTerraform,
					AllowsEdits: false,
					Suspended:   false,
				},
				wantProperties: utils.ManagerProperties{
					Identity:    "identity",
					Kind:        utils.ManagerKindTerraform,
					AllowsEdits: false,
					Suspended:   false,
				},
				wantOK: true,
			},
			{
				name: "set empty identity returns default values",
				setProperties: &utils.ManagerProperties{
					Identity:    "",
					Kind:        utils.ManagerKindRepo,
					AllowsEdits: false,
					Suspended:   false,
				},
				wantProperties: utils.ManagerProperties{
					Identity:    "",
					Kind:        utils.ManagerKindUnknown,
					AllowsEdits: false,
					Suspended:   false,
				},
				wantOK: false,
			},
			{
				name: "invalid kind falls back to generic kind",
				setProperties: &utils.ManagerProperties{
					Identity:    "identity",
					Kind:        utils.ManagerKind("invalid"),
					AllowsEdits: false,
					Suspended:   true,
				},
				wantProperties: utils.ManagerProperties{
					Identity:    "identity",
					Kind:        utils.ManagerKindUnknown,
					AllowsEdits: false,
					Suspended:   true,
				},
				wantOK: true,
			},
		}

		for _, tt := range tests {
			t.Run(tt.name, func(t *testing.T) {
				res := &TestResource2{}
				meta, err := utils.MetaAccessor(res)
				require.NoError(t, err)

				if tt.setProperties != nil {
					meta.SetManagerProperties(*tt.setProperties)
				}

				mp, ok := meta.GetManagerProperties()
				require.Equal(t, tt.wantOK, ok)
				require.Equal(t, tt.wantProperties, mp)
			})
		}
	})

	t.Run("manage secure values", func(t *testing.T) {
		raw := &unstructured.Unstructured{
			Object: map[string]any{},
		}
		obj, _ := utils.MetaAccessor(raw)
		sv, err := obj.GetSecureValues()
		require.NoError(t, err)
		require.Nil(t, sv)

		err = obj.SetSecureValues(common.InlineSecureValues{
			"A": common.InlineSecureValue{Name: "NameForA"},
		})
		require.NoError(t, err)
		require.NotNil(t, raw.Object["secure"])
		sv, err = obj.GetSecureValues()
		require.NoError(t, err)
		require.Equal(t, "NameForA", sv["A"].Name)

		// Manually set secure to an invalid property:
		raw.Object["secure"] = t
		sv, err = obj.GetSecureValues()
		require.Error(t, err)
		require.Nil(t, sv)
		delete(raw.Object, "secure")
	})

	t.Run("SourceProperties", func(t *testing.T) {
		tests := []struct {
			name           string
			setProperties  *utils.SourceProperties
			wantProperties utils.SourceProperties
			wantOK         bool
		}{
			{
				name:           "get default values",
				wantProperties: utils.SourceProperties{},
				wantOK:         false,
			},
			{
				name: "set and get valid values",
				setProperties: &utils.SourceProperties{
					Path:            "path",
					Checksum:        "hash",
					TimestampMillis: time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(),
				},
				wantProperties: utils.SourceProperties{
					Path:            "path",
					Checksum:        "hash",
					TimestampMillis: time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC).UnixMilli(),
				},
				wantOK: true,
			},
		}

		for _, tt := range tests {
			t.Run(tt.name, func(t *testing.T) {
				res := &TestResource2{}
				meta, err := utils.MetaAccessor(res)
				require.NoError(t, err)

				if tt.setProperties != nil {
					meta.SetSourceProperties(*tt.setProperties)
				}

				sp, ok := meta.GetSourceProperties()
				require.Equal(t, tt.wantProperties, sp)
				require.Equal(t, tt.wantOK, ok)
			})
		}
	})
}

func asJSON(v any, pretty bool) string {
	if v == nil {
		return ""
	}
	if pretty {
		bytes, _ := json.MarshalIndent(v, "", "  ")
		return string(bytes)
	}
	bytes, _ := json.Marshal(v)
	return string(bytes)
}
