package apistore

import (
	"context"
	"encoding/json"
	"fmt"
	"slices"
	"testing"

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

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

func TestSecureLifecycle(t *testing.T) {
	resourceWithSecureValues := func(sv common.InlineSecureValues) utils.GrafanaMetaAccessor {
		obj, err := utils.MetaAccessor(&unstructured.Unstructured{
			Object: map[string]any{
				"apiVersion": "something.grafana.app/v1beta1",
				"kind":       "CustomKind",
				"metadata": map[string]any{
					"namespace": "default",
					"name":      "test",
				},
				"secure": sv,
			},
		})
		require.NoError(t, err)
		return obj
	}

	t.Run("create secure values", func(t *testing.T) {
		secureStore := secret.NewMockInlineSecureValueSupport(t)
		secureStore.On("CreateInline", mock.Anything, mock.Anything, common.RawSecureValue("SecretAAA")).
			Return("NameForA", nil).Once()
		secureStore.On("CreateInline", mock.Anything, mock.Anything, common.RawSecureValue("SecretBBB")).
			Return("NameForB", nil).Once()

		info := &objectForStorage{}
		obj := resourceWithSecureValues(common.InlineSecureValues{
			"a": common.InlineSecureValue{Create: "SecretAAA"},
			"b": common.InlineSecureValue{Create: "SecretBBB"},
		})

		err := prepareSecureValues(context.Background(), secureStore, obj, nil, info)
		require.NoError(t, err)
		require.True(t, info.hasChanged)
		slices.Sort(info.createdSecureValues) // keep a predictable order
		require.Equal(t, []string{"NameForA", "NameForB"}, info.createdSecureValues)
		secure, err := obj.GetSecureValues()
		require.NoError(t, err)
		require.JSONEq(t, `{
			"a": {"name": "NameForA"},
			"b": {"name": "NameForB"}
		}`, asJSON(secure, true))
		secureStore.AssertExpectations(t)
	})

	t.Run("create secure values with errors", func(t *testing.T) {
		obj := resourceWithSecureValues(common.InlineSecureValues{
			"a": common.InlineSecureValue{Create: "SecretAAA"},
			"b": common.InlineSecureValue{Create: "SecretBBB"},
		})

		info := &objectForStorage{}
		expectError := fmt.Errorf("expected error")
		secureStore := secret.NewMockInlineSecureValueSupport(t)
		secureStore.On("CreateInline", mock.Anything, mock.Anything, common.RawSecureValue("SecretAAA")).
			Return("", expectError).Maybe()
		secureStore.On("CreateInline", mock.Anything, mock.Anything, common.RawSecureValue("SecretBBB")).
			Return("", expectError).Maybe()

		err := prepareSecureValues(context.Background(), secureStore, obj, nil, info)
		require.Error(t, err, "should error when secure value creation fails")
		require.Equal(t, expectError, err, "error should be propagated")
		secureStore.AssertExpectations(t)
	})

	t.Run("change name manually", func(t *testing.T) {
		secureStore := secret.NewMockInlineSecureValueSupport(t)

		info := &objectForStorage{}
		previous := resourceWithSecureValues(common.InlineSecureValues{
			"a": common.InlineSecureValue{Name: "111"},
			"b": common.InlineSecureValue{Name: "222"},
			"c": common.InlineSecureValue{Name: "333"},
		})
		obj := resourceWithSecureValues(common.InlineSecureValues{
			"a": common.InlineSecureValue{Name: "222"},
			"b": common.InlineSecureValue{Name: "333"}, // no change
			// "c" will be loaded from the previous object without changes
		})

		err := prepareSecureValues(context.Background(), secureStore, obj, previous, info)
		require.NoError(t, err)
		require.True(t, info.hasChanged)
		require.Empty(t, info.createdSecureValues)
		require.Equal(t, info.deleteSecureValues, []string{"111"}) // will be removed if storage succeeds
		secure, err := obj.GetSecureValues()
		require.NoError(t, err)
		require.JSONEq(t, `{
			"a": {"name": "222"},
			"b": {"name": "333"},
			"c": {"name": "333"}
		}`, asJSON(secure, true))
	})

	t.Run("update without secrets", func(t *testing.T) {
		secureStore := secret.NewMockInlineSecureValueSupport(t)

		info := &objectForStorage{}
		previousObject := resourceWithSecureValues(common.InlineSecureValues{
			"a": common.InlineSecureValue{Name: "NameForA"},
			"b": common.InlineSecureValue{Name: "NameForB"},
		})
		objWithoutSecrets := resourceWithSecureValues(nil)

		// Note that the secure values from the previous object are copied over
		err := prepareSecureValues(context.Background(), secureStore, objWithoutSecrets, previousObject, info)
		require.NoError(t, err)
		require.False(t, info.hasChanged)
		secure, err := objWithoutSecrets.GetSecureValues()
		require.NoError(t, err)
		require.JSONEq(t, `{
			"a": {"name": "NameForA"},
			"b": {"name": "NameForB"}
		}`, asJSON(secure, true))
	})

	t.Run("remove secure values", func(t *testing.T) {
		secureStore := secret.NewMockInlineSecureValueSupport(t)
		previous := resourceWithSecureValues(common.InlineSecureValues{
			"a": common.InlineSecureValue{Name: "NameForA"},
			"b": common.InlineSecureValue{Name: "NameForB"},
			"c": common.InlineSecureValue{Name: "NameForC"},
		})

		// Remove "b" with an explicit command
		obj := resourceWithSecureValues(common.InlineSecureValues{
			"a": common.InlineSecureValue{Name: "NameForA"}, // no change
			"b": common.InlineSecureValue{Remove: true},
			// "c" will be loaded from the previous object
		})

		// Prepare secure values does not change anything when removing
		info := &objectForStorage{}
		err := prepareSecureValues(context.Background(), secureStore, obj, previous, info)
		require.NoError(t, err)
		require.True(t, info.hasChanged)  // value was removed
		secureStore.AssertExpectations(t) // nothing called

		secure, err := obj.GetSecureValues()
		require.NoError(t, err)
		require.JSONEq(t, `{
			"a": {"name": "NameForA"},
			"c": {"name": "NameForC"}
		}`, asJSON(secure, true))

		// When there is not an error, the finish command will do a real delete
		owner := utils.ToObjectReference(obj)
		secureStore.On("DeleteWhenOwnedByResource", mock.Anything, owner, "NameForB").
			Return(nil).Once()
		err = info.finish(context.Background(), nil, secureStore)
		require.NoError(t, err)
		require.True(t, info.hasChanged)  // value was removed
		secureStore.AssertExpectations(t) // nothing called

		// When an error exists, no values will be deleted
		err = fmt.Errorf("expected error")
		outErr := info.finish(context.Background(), err, secureStore)
		require.Equal(t, err, outErr, "error should be passed through")
	})

	t.Run("remove invalid secure values", func(t *testing.T) {
		secureStore := secret.NewMockInlineSecureValueSupport(t)
		obj := resourceWithSecureValues(common.InlineSecureValues{
			"b": common.InlineSecureValue{Remove: true},
		})

		// Previous values must exist for remove to execute
		info := &objectForStorage{}
		err := prepareSecureValues(context.Background(), secureStore, obj, resourceWithSecureValues(nil), info)
		require.Error(t, err, "should error when previous secure values does not exist")
		require.Equal(t, "cannot remove secure value 'b', it did not exist in the previous value", err.Error())
		secureStore.AssertExpectations(t)
	})

	t.Run("delete resource", func(t *testing.T) {
		secureStore := secret.NewMockInlineSecureValueSupport(t)
		obj := resourceWithSecureValues(common.InlineSecureValues{
			"a": common.InlineSecureValue{Name: "NameForA"},
		})
		sv, err := obj.GetSecureValues()
		require.NoError(t, err)
		require.Len(t, sv, 1)

		owner := utils.ToObjectReference(obj)
		secureStore.On("DeleteWhenOwnedByResource", mock.Anything, owner, "NameForA").
			Return(nil).Once()

		err = handleSecureValuesDelete(context.Background(), secureStore, obj)
		require.NoError(t, err)
		secureStore.AssertExpectations(t)
		sv, err = obj.GetSecureValues()
		require.NoError(t, err)
		require.Empty(t, sv, "secure values should be empty after delete")

		// Delete should propagate deletion errors
		obj = resourceWithSecureValues(common.InlineSecureValues{
			"a": common.InlineSecureValue{Name: "NameForA"},
		})
		expectError := fmt.Errorf("expected error")
		secureStore = secret.NewMockInlineSecureValueSupport(t)
		secureStore.On("DeleteWhenOwnedByResource", mock.Anything, owner, "NameForA").
			Return(expectError).Once()
		err = handleSecureValuesDelete(context.Background(), secureStore, obj)
		require.Equal(t, expectError, err, "error should be passed through")
		secureStore.AssertExpectations(t)
	})

	t.Run("invalid states", func(t *testing.T) {
		secureStore := secret.NewMockInlineSecureValueSupport(t)

		info := &objectForStorage{}
		err := prepareSecureValues(context.Background(), secureStore, resourceWithSecureValues(common.InlineSecureValues{
			"a": common.InlineSecureValue{}, // MUST have Create, Remove or Name
		}), nil, info)
		require.Error(t, err)
	})

	t.Run("setup errors", func(t *testing.T) {
		objWithoutSecrets := resourceWithSecureValues(nil)
		objWithCreateSecret := resourceWithSecureValues(common.InlineSecureValues{
			"a": common.InlineSecureValue{Create: "SecretAAA"},
		})
		invalid, _ := utils.MetaAccessor(&unstructured.Unstructured{
			Object: map[string]any{
				"apiVersion": "something.grafana.app/v1beta1",
				"kind":       "CustomKind",
				"metadata": map[string]any{
					"namespace": "default",
					"name":      "test",
				},
				"secure": t, // something NOT a secure value
			},
		})
		info := &objectForStorage{}
		err := prepareSecureValues(context.Background(), nil, invalid, nil, info)
		require.Error(t, err, "should error when secure values are not a map")

		err = prepareSecureValues(context.Background(), nil, objWithCreateSecret, invalid, info)
		require.Error(t, err, "should error when previous secure values are not a map")

		err = prepareSecureValues(context.Background(), nil, objWithCreateSecret, nil, info)
		require.Error(t, err, "should error when secure value storage is not configured")

		err = prepareSecureValues(context.Background(), nil, objWithCreateSecret, objWithoutSecrets, info)
		require.Error(t, err, "should error when secure value storage is not configured")

		err = prepareSecureValues(context.Background(), nil, objWithoutSecrets, objWithCreateSecret, info)
		require.Error(t, err, "should error when previous value does not have a name")

		// DELETE Setup errors
		err = handleSecureValuesDelete(context.Background(), nil, invalid)
		require.Error(t, err, "should error when secure values are not a map")

		err = handleSecureValuesDelete(context.Background(), nil, objWithCreateSecret)
		require.Error(t, err, "should error when secure value storage is not configured")
	})
}

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)
}
