package testutils

import (
	"context"
	"testing"
	"time"

	"github.com/grafana/authlib/authn"
	"github.com/grafana/authlib/types"
	"github.com/madflojo/testcerts"
	"github.com/stretchr/testify/require"
	"go.opentelemetry.io/otel/trace/noop"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"

	secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
	decryptcontracts "github.com/grafana/grafana/apps/secret/pkg/decrypt"
	"github.com/grafana/grafana/pkg/apimachinery/identity"
	"github.com/grafana/grafana/pkg/infra/usagestats"
	"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
	"github.com/grafana/grafana/pkg/registry/apis/secret/decrypt"
	cipher "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/cipher/service"
	osskmsproviders "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/kmsproviders"
	"github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager"
	"github.com/grafana/grafana/pkg/registry/apis/secret/garbagecollectionworker"
	"github.com/grafana/grafana/pkg/registry/apis/secret/mutator"
	"github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper/sqlkeeper"
	"github.com/grafana/grafana/pkg/registry/apis/secret/service"
	"github.com/grafana/grafana/pkg/registry/apis/secret/validator"
	"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
	"github.com/grafana/grafana/pkg/services/accesscontrol"
	"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
	"github.com/grafana/grafana/pkg/services/sqlstore"
	"github.com/grafana/grafana/pkg/setting"
	"github.com/grafana/grafana/pkg/storage/secret/database"
	encryptionstorage "github.com/grafana/grafana/pkg/storage/secret/encryption"

	"github.com/grafana/grafana/pkg/storage/secret/metadata"
	"github.com/grafana/grafana/pkg/storage/secret/migrator"
)

type SetupConfig struct {
	KeeperService            contracts.KeeperService
	DataKeyMigrationExecutor contracts.EncryptedValueMigrationExecutor
	RunSecretsDBMigrations   bool
	RunDataKeyMigration      bool
}

func defaultSetupCfg() SetupConfig {
	return SetupConfig{}
}

func WithKeeperService(keeperService contracts.KeeperService) func(*SetupConfig) {
	return func(setupCfg *SetupConfig) {
		setupCfg.KeeperService = keeperService
	}
}

func WithMutateCfg(f func(*SetupConfig)) func(*SetupConfig) {
	return func(cfg *SetupConfig) {
		f(cfg)
	}
}

func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut {
	setupCfg := defaultSetupCfg()
	for _, opt := range opts {
		opt(&setupCfg)
	}

	tracer := noop.NewTracerProvider().Tracer("test")

	testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New()))

	database := database.ProvideDatabase(testDB, tracer)

	keeperMetadataStorage, err := metadata.ProvideKeeperMetadataStorage(database, tracer, nil)
	require.NoError(t, err)

	clock := NewFakeClock()

	secureValueMetadataStorage, err := metadata.ProvideSecureValueMetadataStorage(clock, database, tracer, nil)
	require.NoError(t, err)

	// Initialize access client + access control
	accessControl := acimpl.ProvideAccessControl(nil)
	accessClient := accesscontrol.NewLegacyAccessClient(accessControl, accesscontrol.ResourceAuthorizerOptions{
		Resource: "securevalues",
		Attr:     "uid",
	})

	defaultKey := "SdlklWklckeLS"
	cfg := setting.NewCfg()
	cfg.SecretsManagement = setting.SecretsManagerSettings{
		CurrentEncryptionProvider:     "secret_key.v1",
		ConfiguredKMSProviders:        map[string]map[string]string{"secret_key.v1": {"secret_key": defaultKey}},
		GCWorkerEnabled:               false,
		RunSecretsDBMigrations:        setupCfg.RunSecretsDBMigrations,
		RunDataKeyMigration:           setupCfg.RunDataKeyMigration,
		GCWorkerMaxBatchSize:          2,
		GCWorkerMaxConcurrentCleanups: 2,
	}
	store, err := encryptionstorage.ProvideDataKeyStorage(database, tracer, nil)
	require.NoError(t, err)

	globalDataKeyStore, err := encryptionstorage.ProvideGlobalDataKeyStorage(database, tracer, nil)
	require.NoError(t, err)

	usageStats := &usagestats.UsageStatsMock{T: t}

	enc, err := cipher.ProvideAESGCMCipherService(tracer, usageStats)
	require.NoError(t, err)

	ossProviders, err := osskmsproviders.ProvideOSSKMSProviders(cfg, enc)
	require.NoError(t, err)

	encryptionManager, err := manager.ProvideEncryptionManager(
		tracer,
		store,
		usageStats,
		enc,
		ossProviders,
	)
	require.NoError(t, err)

	// Initialize encrypted value storage with a fake db
	encryptedValueStorage, err := encryptionstorage.ProvideEncryptedValueStorage(database, tracer)
	require.NoError(t, err)

	// Initialize global encrypted value storage with a fake db
	globalEncryptedValueStorage, err := encryptionstorage.ProvideGlobalEncryptedValueStorage(database, tracer)
	require.NoError(t, err)

	// Initialize a noop migration executor for the sql keeper so it doesn't interfere with initialization, or use the one provided
	fakeMigrationExecutor := setupCfg.DataKeyMigrationExecutor
	if fakeMigrationExecutor == nil {
		fakeMigrationExecutor = &NoopMigrationExecutor{}
	}
	sqlKeeper, err := sqlkeeper.NewSQLKeeper(tracer, encryptionManager, encryptedValueStorage, fakeMigrationExecutor, nil, cfg)
	require.NoError(t, err)

	// Initialize a real migration executor for test
	realMigrationExecutor, err := encryptionstorage.ProvideEncryptedValueMigrationExecutor(database, tracer, encryptedValueStorage, globalEncryptedValueStorage)
	require.NoError(t, err)

	var keeperService contracts.KeeperService = newKeeperServiceWrapper(sqlKeeper)

	if setupCfg.KeeperService != nil {
		keeperService = setupCfg.KeeperService
	}

	secureValueValidator := validator.ProvideSecureValueValidator()
	secureValueMutator := mutator.ProvideSecureValueMutator()

	secureValueService := service.ProvideSecureValueService(tracer, accessClient, secureValueMetadataStorage, secureValueValidator, secureValueMutator, keeperMetadataStorage, keeperService, nil)

	decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer, nil)

	decryptStorage, err := metadata.ProvideDecryptStorage(tracer, keeperService, keeperMetadataStorage, secureValueMetadataStorage, decryptAuthorizer, nil)
	require.NoError(t, err)

	testCfg := setting.NewCfg()

	decryptService, err := decrypt.ProvideDecryptService(testCfg, tracer, decryptStorage)
	require.NoError(t, err)

	consolidationService := service.ProvideConsolidationService(tracer, globalDataKeyStore, encryptedValueStorage, globalEncryptedValueStorage, encryptionManager)

	garbageCollectionWorker := garbagecollectionworker.ProvideWorker(
		cfg,
		secureValueMetadataStorage,
		keeperMetadataStorage,
		keeperService)

	return Sut{
		SecureValueService:              secureValueService,
		SecureValueMetadataStorage:      secureValueMetadataStorage,
		DecryptStorage:                  decryptStorage,
		DecryptService:                  decryptService,
		EncryptedValueStorage:           encryptedValueStorage,
		GlobalEncryptedValueStorage:     globalEncryptedValueStorage,
		EncryptedValueMigrationExecutor: realMigrationExecutor,
		SQLKeeper:                       sqlKeeper,
		Database:                        database,
		AccessClient:                    accessClient,
		ConsolidationService:            consolidationService,
		EncryptionManager:               encryptionManager,
		GlobalDataKeyStore:              globalDataKeyStore,
		GarbageCollectionWorker:         garbageCollectionWorker,
		Clock:                           clock,
		KeeperService:                   keeperService,
		KeeperMetadataStorage:           keeperMetadataStorage,
	}
}

type Sut struct {
	SecureValueService              contracts.SecureValueService
	SecureValueMetadataStorage      contracts.SecureValueMetadataStorage
	DecryptStorage                  contracts.DecryptStorage
	DecryptService                  decryptcontracts.DecryptService
	EncryptedValueStorage           contracts.EncryptedValueStorage
	GlobalEncryptedValueStorage     contracts.GlobalEncryptedValueStorage
	EncryptedValueMigrationExecutor contracts.EncryptedValueMigrationExecutor
	SQLKeeper                       *sqlkeeper.SQLKeeper
	Database                        *database.Database
	AccessClient                    types.AccessClient
	ConsolidationService            contracts.ConsolidationService
	EncryptionManager               contracts.EncryptionManager
	GlobalDataKeyStore              contracts.GlobalDataKeyStorage
	GarbageCollectionWorker         *garbagecollectionworker.Worker
	// The fake clock passed to implementations to make testing easier
	Clock                 *FakeClock
	KeeperService         contracts.KeeperService
	KeeperMetadataStorage contracts.KeeperMetadataStorage
}

type CreateSvConfig struct {
	Sv *secretv1beta1.SecureValue
}

func CreateSvWithSv(sv *secretv1beta1.SecureValue) func(*CreateSvConfig) {
	return func(cfg *CreateSvConfig) {
		cfg.Sv = sv
	}
}

func (s *Sut) CreateSv(ctx context.Context, opts ...func(*CreateSvConfig)) (*secretv1beta1.SecureValue, error) {
	cfg := CreateSvConfig{
		Sv: &secretv1beta1.SecureValue{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "sv1",
				Namespace: "ns1",
			},
			Spec: secretv1beta1.SecureValueSpec{
				Description: "desc1",
				Value:       ptr.To(secretv1beta1.NewExposedSecureValue("v1")),
				Decrypters:  []string{"decrypter1"},
			},
			Status: secretv1beta1.SecureValueStatus{},
		},
	}
	for _, opt := range opts {
		opt(&cfg)
	}

	createdSv, err := s.SecureValueService.Create(ctx, cfg.Sv, "actor-uid")
	if err != nil {
		return nil, err
	}
	return createdSv, nil
}

func (s *Sut) UpdateSv(ctx context.Context, sv *secretv1beta1.SecureValue) (*secretv1beta1.SecureValue, error) {
	newSv, _, err := s.SecureValueService.Update(ctx, sv, "actor-uid")
	return newSv, err
}

func (s *Sut) DeleteSv(ctx context.Context, namespace, name string) (*secretv1beta1.SecureValue, error) {
	sv, err := s.SecureValueService.Delete(ctx, xkube.Namespace(namespace), name)
	return sv, err
}

type keeperServiceWrapper struct {
	keeper contracts.Keeper
}

func newKeeperServiceWrapper(keeper contracts.Keeper) *keeperServiceWrapper {
	return &keeperServiceWrapper{keeper: keeper}
}

func (wrapper *keeperServiceWrapper) KeeperForConfig(cfg secretv1beta1.KeeperConfig) (contracts.Keeper, error) {
	return wrapper.keeper, nil
}

func CreateUserAuthContext(ctx context.Context, namespace string, permissions map[string][]string) context.Context {
	orgID := int64(1)
	requester := &identity.StaticRequester{
		Namespace: namespace,
		Type:      types.TypeUser,
		UserID:    1,
		OrgID:     orgID,
		Permissions: map[int64]map[string][]string{
			orgID: permissions,
		},
	}

	return types.WithAuthInfo(ctx, requester)
}

func CreateServiceAuthContext(ctx context.Context, serviceIdentity string, namespace string, permissions []string) context.Context {
	requester := &identity.StaticRequester{
		Namespace: namespace,
		AccessTokenClaims: &authn.Claims[authn.AccessTokenClaims]{
			Rest: authn.AccessTokenClaims{
				Permissions:     permissions,
				ServiceIdentity: serviceIdentity,
			},
		},
	}

	return types.WithAuthInfo(ctx, requester)
}

// CreateOBOAuthContext emulates a context where the request is made on-behalf-of (OBO) a user, with an access token.
func CreateOBOAuthContext(
	ctx context.Context,
	serviceIdentity string,
	namespace string,
	userPermissions map[string][]string,
	delegatedPermissions []string,
) context.Context {
	requester := &identity.StaticRequester{
		Namespace: namespace,
		Type:      types.TypeUser,
		OrgID:     1,
		UserID:    1,
		Permissions: map[int64]map[string][]string{
			1: userPermissions,
		},
		AccessTokenClaims: &authn.Claims[authn.AccessTokenClaims]{
			Rest: authn.AccessTokenClaims{
				ServiceIdentity:      serviceIdentity,
				DelegatedPermissions: delegatedPermissions,
				Actor: &authn.ActorClaims{
					Subject: "user:1",
				},
			},
		},
	}

	return types.WithAuthInfo(ctx, requester)
}

type TestCertPaths struct {
	ClientCert string
	ClientKey  string
	ServerCert string
	ServerKey  string
	CA         string
}

func CreateX509TestDir(t *testing.T) TestCertPaths {
	t.Helper()

	tmpDir := t.TempDir()

	ca := testcerts.NewCA()
	caCertFile, _, err := ca.ToTempFile(tmpDir)
	require.NoError(t, err)

	serverKp, err := ca.NewKeyPair("localhost")
	require.NoError(t, err)

	serverCertFile, serverKeyFile, err := serverKp.ToTempFile(tmpDir)
	require.NoError(t, err)

	clientKp, err := ca.NewKeyPair()
	require.NoError(t, err)
	clientCertFile, clientKeyFile, err := clientKp.ToTempFile(tmpDir)
	require.NoError(t, err)

	return TestCertPaths{
		ClientCert: clientCertFile.Name(),
		ClientKey:  clientKeyFile.Name(),
		ServerCert: serverCertFile.Name(),
		ServerKey:  serverKeyFile.Name(),
		CA:         caCertFile.Name(),
	}
}

type FakeClock struct {
	Current time.Time
}

func NewFakeClock() *FakeClock {
	return &FakeClock{Current: time.Now()}
}

func (c *FakeClock) Now() time.Time {
	return c.Current
}

func (c *FakeClock) AdvanceBy(duration time.Duration) {
	c.Current = c.Current.Add(duration)
}

type NoopMigrationExecutor struct {
}

func (e *NoopMigrationExecutor) Execute(ctx context.Context) (int, error) {
	return 0, nil
}
