package kvstore

import (
	"context"
	"errors"
	"sync"
	"testing"

	"github.com/stretchr/testify/require"
	"google.golang.org/grpc"
	"gopkg.in/ini.v1"

	"github.com/grafana/grafana/pkg/infra/db"
	"github.com/grafana/grafana/pkg/infra/kvstore"
	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/plugins"
	"github.com/grafana/grafana/pkg/plugins/backendplugin"
	"github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin"
	pluginsLogger "github.com/grafana/grafana/pkg/plugins/log"
	"github.com/grafana/grafana/pkg/services/featuremgmt"
	"github.com/grafana/grafana/pkg/services/secrets/fakes"
	secretsmng "github.com/grafana/grafana/pkg/services/secrets/manager"
	"github.com/grafana/grafana/pkg/services/sqlstore"
	"github.com/grafana/grafana/pkg/setting"
)

func NewFakeSQLSecretsKVStore(t *testing.T, sqlStore *sqlstore.SQLStore) *SecretsKVStoreSQL {
	t.Helper()
	secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
	return NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
}

func NewFakePluginSecretsKVStore(t *testing.T, features featuremgmt.FeatureToggles, fallback SecretsKVStore) *SecretsKVStorePlugin {
	t.Helper()
	sqlStore := db.InitTestDB(t)
	secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
	store := kvstore.ProvideService(sqlStore)
	namespacedKVStore := GetNamespacedKVStore(store)
	manager := NewFakeSecretsPluginManager(t, false)
	plugin := manager.SecretsManager(context.Background()).SecretsManager
	return NewPluginSecretsKVStore(plugin, secretsService, namespacedKVStore, features, fallback, log.New("test.logger"))
}

// In memory kv store used for testing
type FakeSecretsKVStore struct {
	store    map[Key]string
	delError bool
	fallback SecretsKVStore
}

func NewFakeSecretsKVStore() *FakeSecretsKVStore {
	return &FakeSecretsKVStore{store: make(map[Key]string)}
}

func (f *FakeSecretsKVStore) DeletionError(shouldErr bool) {
	f.delError = shouldErr
}

func (f *FakeSecretsKVStore) Get(ctx context.Context, orgId int64, namespace string, typ string) (string, bool, error) {
	value := f.store[buildKey(orgId, namespace, typ)]
	found := value != ""
	return value, found, nil
}

func (f *FakeSecretsKVStore) Set(ctx context.Context, orgId int64, namespace string, typ string, value string) error {
	f.store[buildKey(orgId, namespace, typ)] = value
	return nil
}

func (f *FakeSecretsKVStore) Del(ctx context.Context, orgId int64, namespace string, typ string) error {
	if f.delError {
		return errors.New("mocked del error")
	}
	delete(f.store, buildKey(orgId, namespace, typ))
	return nil
}

// List all keys with an optional filter. If default values are provided, filter is not applied.
func (f *FakeSecretsKVStore) Keys(ctx context.Context, orgId int64, namespace string, typ string) ([]Key, error) {
	res := make([]Key, 0)
	for k := range f.store {
		if orgId == AllOrganizations && namespace == "" && typ == "" {
			res = append(res, k)
		} else if k.OrgId == orgId && k.Namespace == namespace && k.Type == typ {
			res = append(res, k)
		}
	}
	return res, nil
}

func (f *FakeSecretsKVStore) Rename(ctx context.Context, orgId int64, namespace string, typ string, newNamespace string) error {
	f.store[buildKey(orgId, newNamespace, typ)] = f.store[buildKey(orgId, namespace, typ)]
	delete(f.store, buildKey(orgId, namespace, typ))
	return nil
}

func (f *FakeSecretsKVStore) GetAll(ctx context.Context) ([]Item, error) {
	items := make([]Item, 0)
	for k := range f.store {
		orgId := k.OrgId
		namespace := k.Namespace
		typ := k.Type
		items = append(items, Item{
			OrgId:     &orgId,
			Namespace: &namespace,
			Type:      &typ,
			Value:     f.store[k],
		})
	}
	return items, nil
}

func (f *FakeSecretsKVStore) Fallback() SecretsKVStore {
	return f.fallback
}

func (f *FakeSecretsKVStore) SetFallback(store SecretsKVStore) error {
	f.fallback = store
	return nil
}

func buildKey(orgId int64, namespace string, typ string) Key {
	return Key{
		OrgId:     orgId,
		Namespace: namespace,
		Type:      typ,
	}
}

func internalToProtoKey(k Key) *secretsmanagerplugin.Key {
	return &secretsmanagerplugin.Key{
		OrgId:     k.OrgId,
		Namespace: k.Namespace,
		Type:      k.Type,
	}
}

// Fake feature toggle - only need to check the backwards compatibility disabled flag
type fakeFeatureToggles struct {
	returnValue bool
}

func NewFakeFeatureToggles(t *testing.T, returnValue bool) featuremgmt.FeatureToggles {
	t.Helper()
	return fakeFeatureToggles{
		returnValue: returnValue,
	}
}

func (f fakeFeatureToggles) IsEnabledGlobally(feature string) bool {
	return f.returnValue
}

func (f fakeFeatureToggles) IsEnabled(ctx context.Context, feature string) bool {
	return f.returnValue
}

func (f fakeFeatureToggles) GetEnabled(ctx context.Context) map[string]bool {
	return map[string]bool{}
}

// Fake grpc secrets plugin impl
type fakeGRPCSecretsPlugin struct {
	kv map[Key]string
}

func (c *fakeGRPCSecretsPlugin) GetSecret(ctx context.Context, in *secretsmanagerplugin.GetSecretRequest, opts ...grpc.CallOption) (*secretsmanagerplugin.GetSecretResponse, error) {
	val, ok := c.kv[buildKey(in.KeyDescriptor.OrgId, in.KeyDescriptor.Namespace, in.KeyDescriptor.Type)]
	return &secretsmanagerplugin.GetSecretResponse{
		DecryptedValue: val,
		Exists:         ok,
	}, nil
}

func (c *fakeGRPCSecretsPlugin) SetSecret(ctx context.Context, in *secretsmanagerplugin.SetSecretRequest, opts ...grpc.CallOption) (*secretsmanagerplugin.SetSecretResponse, error) {
	c.kv[buildKey(in.KeyDescriptor.OrgId, in.KeyDescriptor.Namespace, in.KeyDescriptor.Type)] = in.Value
	return &secretsmanagerplugin.SetSecretResponse{}, nil
}

func (c *fakeGRPCSecretsPlugin) DeleteSecret(ctx context.Context, in *secretsmanagerplugin.DeleteSecretRequest, opts ...grpc.CallOption) (*secretsmanagerplugin.DeleteSecretResponse, error) {
	delete(c.kv, buildKey(in.KeyDescriptor.OrgId, in.KeyDescriptor.Namespace, in.KeyDescriptor.Type))
	return &secretsmanagerplugin.DeleteSecretResponse{}, nil
}

func (c *fakeGRPCSecretsPlugin) ListSecrets(ctx context.Context, in *secretsmanagerplugin.ListSecretsRequest, opts ...grpc.CallOption) (*secretsmanagerplugin.ListSecretsResponse, error) {
	res := make([]*secretsmanagerplugin.Key, 0)
	for k := range c.kv {
		if in.KeyDescriptor.OrgId == AllOrganizations && in.KeyDescriptor.Namespace == "" && in.KeyDescriptor.Type == "" {
			res = append(res, internalToProtoKey(k))
		} else if k.OrgId == in.KeyDescriptor.OrgId && k.Namespace == in.KeyDescriptor.Namespace && k.Type == in.KeyDescriptor.Type {
			res = append(res, internalToProtoKey(k))
		}
	}
	return &secretsmanagerplugin.ListSecretsResponse{
		Keys: res,
	}, nil
}

func (c *fakeGRPCSecretsPlugin) RenameSecret(ctx context.Context, in *secretsmanagerplugin.RenameSecretRequest, opts ...grpc.CallOption) (*secretsmanagerplugin.RenameSecretResponse, error) {
	oldKey := buildKey(in.KeyDescriptor.OrgId, in.KeyDescriptor.Namespace, in.KeyDescriptor.Type)
	val := c.kv[oldKey]
	delete(c.kv, oldKey)
	c.kv[buildKey(in.KeyDescriptor.OrgId, in.NewNamespace, in.KeyDescriptor.Type)] = val
	return &secretsmanagerplugin.RenameSecretResponse{}, nil
}

func (c *fakeGRPCSecretsPlugin) GetAllSecrets(ctx context.Context, in *secretsmanagerplugin.GetAllSecretsRequest, opts ...grpc.CallOption) (*secretsmanagerplugin.GetAllSecretsResponse, error) {
	items := make([]*secretsmanagerplugin.Item, 0)
	for k, v := range c.kv {
		items = append(items, &secretsmanagerplugin.Item{
			Key:   internalToProtoKey(k),
			Value: v,
		})
	}
	return &secretsmanagerplugin.GetAllSecretsResponse{
		Items: items,
	}, nil
}

var _ SecretsKVStore = &FakeSecretsKVStore{}
var _ secretsmanagerplugin.SecretsManagerPlugin = &fakeGRPCSecretsPlugin{}

// Fake plugin manager
type fakePluginManager struct {
	shouldFailOnStart bool
	plugin            *plugins.Plugin
}

func (mg *fakePluginManager) SecretsManager(_ context.Context) *plugins.Plugin {
	if mg.plugin != nil {
		return mg.plugin
	}
	p := &plugins.Plugin{
		SecretsManager: &fakeGRPCSecretsPlugin{
			kv: make(map[Key]string),
		},
	}
	p.RegisterClient(&fakePluginClient{
		shouldFailOnStart: mg.shouldFailOnStart,
	})
	mg.plugin = p
	return p
}

func NewFakeSecretsPluginManager(t *testing.T, shouldFailOnStart bool) plugins.SecretsPluginManager {
	t.Helper()
	return &fakePluginManager{
		shouldFailOnStart: shouldFailOnStart,
	}
}

// Fake plugin client
type fakePluginClient struct {
	shouldFailOnStart bool
	backendplugin.Plugin
}

func (pc *fakePluginClient) Start(_ context.Context) error {
	if pc.shouldFailOnStart {
		return errors.New("mocked failed to start")
	}
	return nil
}

func (pc *fakePluginClient) Stop(_ context.Context) error {
	return nil
}

func (pc *fakePluginClient) Logger() pluginsLogger.Logger {
	return pluginsLogger.NewTestLogger()
}

func SetupFatalCrashTest(
	t *testing.T,
	shouldFailOnStart bool,
	isPluginErrorFatal bool,
	isBackwardsCompatDisabled bool,
) (fatalCrashTestFields, error) {
	t.Helper()
	fatalFlagOnce = sync.Once{}
	startupOnce = sync.Once{}
	cfg := SetupTestConfig(t)
	sqlStore := db.InitTestDB(t)
	secretService := fakes.FakeSecretsService{}
	kvstore := kvstore.ProvideService(sqlStore)
	if isPluginErrorFatal {
		_ = SetPluginStartupErrorFatal(context.Background(), GetNamespacedKVStore(kvstore), true)
	}
	features := NewFakeFeatureToggles(t, isBackwardsCompatDisabled)
	manager := NewFakeSecretsPluginManager(t, shouldFailOnStart)
	svc, err := ProvideService(sqlStore, secretService, manager, kvstore, features, cfg)
	t.Cleanup(ResetPlugin)
	return fatalCrashTestFields{
		SecretsKVStore: svc,
		PluginManager:  manager,
		KVStore:        kvstore,
		SqlStore:       sqlStore,
	}, err
}

type fatalCrashTestFields struct {
	SecretsKVStore SecretsKVStore
	PluginManager  plugins.SecretsPluginManager
	KVStore        kvstore.KVStore
	SqlStore       db.DB
}

func SetupTestConfig(t *testing.T) *setting.Cfg {
	t.Helper()
	rawCfg := `
		[secrets]
		use_plugin = true
		`
	raw, err := ini.Load([]byte(rawCfg))
	require.NoError(t, err)
	return &setting.Cfg{Raw: raw}
}

func ReplaceFallback(t *testing.T, kv SecretsKVStore, fb SecretsKVStore) error {
	t.Helper()
	if store, ok := kv.(*CachedKVStore); ok {
		kv = store.store
	}
	if store, ok := kv.(*SecretsKVStorePlugin); ok {
		store.fallbackStore = fb
		return nil
	}
	return errors.New("not a plugin store")
}
