package pluginfakes

import (
	"archive/zip"
	"context"
	"fmt"
	"io/fs"
	"sync"

	"github.com/grafana/grafana-plugin-sdk-go/backend"
	"go.opentelemetry.io/otel/trace"
	"go.opentelemetry.io/otel/trace/noop"

	"github.com/grafana/grafana/pkg/plugins"
	"github.com/grafana/grafana/pkg/plugins/auth"
	"github.com/grafana/grafana/pkg/plugins/backendplugin"
	"github.com/grafana/grafana/pkg/plugins/log"
	"github.com/grafana/grafana/pkg/plugins/pluginassets"
	"github.com/grafana/grafana/pkg/plugins/repo"
	"github.com/grafana/grafana/pkg/plugins/storage"
)

type FakePluginInstaller struct {
	AddFunc func(ctx context.Context, pluginID, version string, opts plugins.AddOpts) error
	// Remove removes a plugin from the store.
	RemoveFunc func(ctx context.Context, pluginID, version string) error
}

func (i *FakePluginInstaller) Add(ctx context.Context, pluginID, version string, opts plugins.AddOpts) error {
	if i.AddFunc != nil {
		return i.AddFunc(ctx, pluginID, version, opts)
	}
	return nil
}

func (i *FakePluginInstaller) Remove(ctx context.Context, pluginID, version string) error {
	if i.RemoveFunc != nil {
		return i.RemoveFunc(ctx, pluginID, version)
	}
	return nil
}

type FakeLoader struct {
	LoadFunc   func(_ context.Context, _ plugins.PluginSource) ([]*plugins.Plugin, error)
	UnloadFunc func(_ context.Context, _ *plugins.Plugin) (*plugins.Plugin, error)
}

func (l *FakeLoader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) {
	if l.LoadFunc != nil {
		return l.LoadFunc(ctx, src)
	}
	return nil, nil
}

func (l *FakeLoader) Unload(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) {
	if l.UnloadFunc != nil {
		return l.UnloadFunc(ctx, p)
	}
	return nil, nil
}

type FakePluginClient struct {
	ID      string
	Managed bool
	Log     log.Logger

	startCount     int
	stopCount      int
	exited         bool
	decommissioned bool
	backend.CollectMetricsHandlerFunc
	backend.CheckHealthHandlerFunc
	backend.QueryDataHandlerFunc
	backend.CallResourceHandlerFunc
	backend.MutateAdmissionFunc
	backend.ValidateAdmissionFunc
	backend.ConvertObjectsFunc
	mutex sync.RWMutex

	backendplugin.Plugin
}

func (pc *FakePluginClient) PluginID() string {
	return pc.ID
}

func (pc *FakePluginClient) Logger() log.Logger {
	return pc.Log
}

func (pc *FakePluginClient) Start(_ context.Context) error {
	pc.mutex.Lock()
	defer pc.mutex.Unlock()
	pc.exited = false
	pc.startCount++
	return nil
}

func (pc *FakePluginClient) Stop(_ context.Context) error {
	pc.mutex.Lock()
	defer pc.mutex.Unlock()
	pc.stopCount++
	pc.exited = true
	return nil
}

func (pc *FakePluginClient) IsManaged() bool {
	return pc.Managed
}

func (pc *FakePluginClient) Exited() bool {
	pc.mutex.RLock()
	defer pc.mutex.RUnlock()
	return pc.exited
}

func (pc *FakePluginClient) Decommission() error {
	pc.mutex.Lock()
	defer pc.mutex.Unlock()
	pc.decommissioned = true
	return nil
}

func (pc *FakePluginClient) IsDecommissioned() bool {
	pc.mutex.RLock()
	defer pc.mutex.RUnlock()
	return pc.decommissioned
}

func (pc *FakePluginClient) Target() backendplugin.Target {
	return "test-target"
}

func (pc *FakePluginClient) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) {
	if pc.CollectMetricsHandlerFunc != nil {
		return pc.CollectMetricsHandlerFunc(ctx, req)
	}

	return nil, plugins.ErrMethodNotImplemented
}

func (pc *FakePluginClient) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
	if pc.CheckHealthHandlerFunc != nil {
		return pc.CheckHealthHandlerFunc(ctx, req)
	}

	return nil, plugins.ErrMethodNotImplemented
}

func (pc *FakePluginClient) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
	if pc.QueryDataHandlerFunc != nil {
		return pc.QueryDataHandlerFunc(ctx, req)
	}

	return nil, plugins.ErrMethodNotImplemented
}

func (pc *FakePluginClient) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
	if pc.CallResourceHandlerFunc != nil {
		return pc.CallResourceHandlerFunc(ctx, req, sender)
	}

	return plugins.ErrMethodNotImplemented
}

func (pc *FakePluginClient) SubscribeStream(_ context.Context, _ *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) {
	return nil, plugins.ErrMethodNotImplemented
}

func (pc *FakePluginClient) PublishStream(_ context.Context, _ *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) {
	return nil, plugins.ErrMethodNotImplemented
}

func (pc *FakePluginClient) RunStream(_ context.Context, _ *backend.RunStreamRequest, _ *backend.StreamSender) error {
	return plugins.ErrMethodNotImplemented
}

func (pc *FakePluginClient) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) {
	if pc.ValidateAdmissionFunc != nil {
		return pc.ValidateAdmissionFunc(ctx, req)
	}

	return nil, plugins.ErrMethodNotImplemented
}

func (pc *FakePluginClient) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) {
	if pc.MutateAdmissionFunc != nil {
		return pc.MutateAdmissionFunc(ctx, req)
	}

	return nil, plugins.ErrMethodNotImplemented
}

func (pc *FakePluginClient) ConvertObjects(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) {
	if pc.ConvertObjectsFunc != nil {
		return pc.ConvertObjectsFunc(ctx, req)
	}

	return nil, plugins.ErrMethodNotImplemented
}

type FakePluginRegistry struct {
	Store map[string]*plugins.Plugin
}

func NewFakePluginRegistry() *FakePluginRegistry {
	return &FakePluginRegistry{
		Store: make(map[string]*plugins.Plugin),
	}
}

func (f *FakePluginRegistry) Plugin(_ context.Context, id, _ string) (*plugins.Plugin, bool) {
	p, exists := f.Store[id]
	return p, exists
}

func (f *FakePluginRegistry) Plugins(_ context.Context) []*plugins.Plugin {
	res := make([]*plugins.Plugin, 0, len(f.Store))
	for _, p := range f.Store {
		res = append(res, p)
	}

	return res
}

func (f *FakePluginRegistry) Add(_ context.Context, p *plugins.Plugin) error {
	f.Store[p.ID] = p
	return nil
}

func (f *FakePluginRegistry) Remove(_ context.Context, id, _ string) error {
	delete(f.Store, id)
	return nil
}

type FakePluginRepo struct {
	GetPluginArchiveFunc      func(_ context.Context, pluginID, version string, _ repo.CompatOpts) (*repo.PluginArchive, error)
	GetPluginArchiveByURLFunc func(_ context.Context, archiveURL string, _ repo.CompatOpts) (*repo.PluginArchive, error)
	GetPluginArchiveInfoFunc  func(_ context.Context, pluginID, version string, _ repo.CompatOpts) (*repo.PluginArchiveInfo, error)
	PluginVersionFunc         func(_ context.Context, pluginID, version string, compatOpts repo.CompatOpts) (repo.VersionData, error)
}

// GetPluginArchive fetches the requested plugin archive.
func (r *FakePluginRepo) GetPluginArchive(ctx context.Context, pluginID, version string, opts repo.CompatOpts) (*repo.PluginArchive, error) {
	if r.GetPluginArchiveFunc != nil {
		return r.GetPluginArchiveFunc(ctx, pluginID, version, opts)
	}

	return &repo.PluginArchive{}, nil
}

// GetPluginArchiveByURL fetches the requested plugin from the specified URL.
func (r *FakePluginRepo) GetPluginArchiveByURL(ctx context.Context, archiveURL string, opts repo.CompatOpts) (*repo.PluginArchive, error) {
	if r.GetPluginArchiveByURLFunc != nil {
		return r.GetPluginArchiveByURLFunc(ctx, archiveURL, opts)
	}

	return &repo.PluginArchive{}, nil
}

// GetPluginArchiveInfo fetches information for downloading the requested plugin.
func (r *FakePluginRepo) GetPluginArchiveInfo(ctx context.Context, pluginID, version string, opts repo.CompatOpts) (*repo.PluginArchiveInfo, error) {
	if r.GetPluginArchiveInfoFunc != nil {
		return r.GetPluginArchiveInfoFunc(ctx, pluginID, version, opts)
	}
	return &repo.PluginArchiveInfo{}, nil
}

func (r *FakePluginRepo) PluginVersion(ctx context.Context, pluginID, version string, compatOpts repo.CompatOpts) (repo.VersionData, error) {
	if r.PluginVersionFunc != nil {
		return r.PluginVersionFunc(ctx, pluginID, version, compatOpts)
	}
	return repo.VersionData{}, nil
}

func (r *FakePluginRepo) PluginInfo(ctx context.Context, pluginID string, compatOpts repo.CompatOpts) (*repo.PluginInfo, error) {
	return &repo.PluginInfo{}, nil
}

func (r *FakePluginRepo) GetPluginsInfo(ctx context.Context, options repo.GetPluginsInfoOptions, compatOpts repo.CompatOpts) ([]repo.PluginInfo, error) {
	return []repo.PluginInfo{}, nil
}

type fakeTracerProvider struct {
	noop.TracerProvider
}

func InitializeNoopTracerForTest() trace.Tracer {
	return fakeTracerProvider{}.Tracer("test")
}

type FakePluginStorage struct {
	ExtractFunc func(_ context.Context, pluginID string, dirNameFunc storage.DirNameGeneratorFunc, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error)
}

func NewFakePluginStorage() *FakePluginStorage {
	return &FakePluginStorage{}
}

func (s *FakePluginStorage) Extract(ctx context.Context, pluginID string, dirNameFunc storage.DirNameGeneratorFunc, z *zip.ReadCloser) (*storage.ExtractedPluginArchive, error) {
	if s.ExtractFunc != nil {
		return s.ExtractFunc(ctx, pluginID, dirNameFunc, z)
	}
	return &storage.ExtractedPluginArchive{}, nil
}

type FakePluginEnvProvider struct {
	PluginEnvVarsFunc func(ctx context.Context, plugin *plugins.Plugin) []string
}

func NewFakePluginEnvProvider() *FakePluginEnvProvider {
	return &FakePluginEnvProvider{}
}

func (p *FakePluginEnvProvider) PluginEnvVars(ctx context.Context, plugin *plugins.Plugin) []string {
	if p.PluginEnvVarsFunc != nil {
		return p.PluginEnvVarsFunc(ctx, plugin)
	}
	return []string{}
}

type FakeProcessManager struct {
	StartFunc func(_ context.Context, p *plugins.Plugin) error
	StopFunc  func(_ context.Context, p *plugins.Plugin) error
	Started   map[string]int
	Stopped   map[string]int
}

func NewFakeProcessManager() *FakeProcessManager {
	return &FakeProcessManager{
		Started: make(map[string]int),
		Stopped: make(map[string]int),
	}
}

func (m *FakeProcessManager) Start(ctx context.Context, p *plugins.Plugin) error {
	m.Started[p.ID]++
	if m.StartFunc != nil {
		return m.StartFunc(ctx, p)
	}
	return nil
}

func (m *FakeProcessManager) Stop(ctx context.Context, p *plugins.Plugin) error {
	m.Stopped[p.ID]++
	if m.StopFunc != nil {
		return m.StopFunc(ctx, p)
	}
	return nil
}

type FakeBackendProcessProvider struct {
	Requested          map[string]int
	Invoked            map[string]int
	BackendFactoryFunc func(context.Context, *plugins.Plugin) backendplugin.PluginFactoryFunc
}

func NewFakeBackendProcessProvider() *FakeBackendProcessProvider {
	f := &FakeBackendProcessProvider{
		Requested: make(map[string]int),
		Invoked:   make(map[string]int),
	}
	f.BackendFactoryFunc = func(ctx context.Context, p *plugins.Plugin) backendplugin.PluginFactoryFunc {
		f.Requested[p.ID]++
		return func(pluginID string, _ log.Logger, _ trace.Tracer, _ func() []string) (backendplugin.Plugin, error) {
			f.Invoked[pluginID]++
			return &FakePluginClient{}, nil
		}
	}
	return f
}

func (pr *FakeBackendProcessProvider) BackendFactory(ctx context.Context, p *plugins.Plugin) backendplugin.PluginFactoryFunc {
	return pr.BackendFactoryFunc(ctx, p)
}

type FakeLicensingService struct {
	LicenseEdition string
	TokenRaw       string
	LicensePath    string
	LicenseAppURL  string
	CDNPrefix      string
}

func NewFakeLicensingService() *FakeLicensingService {
	return &FakeLicensingService{}
}

func (s *FakeLicensingService) Edition() string {
	return s.LicenseEdition
}

func (s *FakeLicensingService) Path() string {
	return s.LicensePath
}

func (s *FakeLicensingService) AppURL() string {
	return s.LicenseAppURL
}

func (s *FakeLicensingService) Environment() []string {
	return []string{fmt.Sprintf("GF_ENTERPRISE_LICENSE_TEXT=%s", s.TokenRaw)}
}

func (s *FakeLicensingService) ContentDeliveryPrefix() string {
	return s.CDNPrefix
}

type FakeRoleRegistry struct {
	ExpectedErr error
}

func NewFakeRoleRegistry() *FakeRoleRegistry {
	return &FakeRoleRegistry{}
}

func (f *FakeRoleRegistry) DeclarePluginRoles(_ context.Context, _ string, _ string, _ []plugins.RoleRegistration) error {
	return f.ExpectedErr
}

type FakeActionSetRegistry struct {
	ExpectedErr error
}

func NewFakeActionSetRegistry() *FakeActionSetRegistry {
	return &FakeActionSetRegistry{}
}

func (f *FakeActionSetRegistry) RegisterActionSets(_ context.Context, _ string, _ []plugins.ActionSet) error {
	return f.ExpectedErr
}

type FakePluginFS struct {
	OpenFunc   func(name string) (fs.File, error)
	RemoveFunc func() error
	TypeFunc   func() plugins.FSType
	RelFunc    func(string) (string, error)

	base string
}

func NewFakePluginFS(base string) *FakePluginFS {
	return &FakePluginFS{
		base: base,
	}
}

func (f *FakePluginFS) Open(name string) (fs.File, error) {
	if f.OpenFunc != nil {
		return f.OpenFunc(name)
	}
	return nil, nil
}

func (f *FakePluginFS) Rel(_ string) (string, error) {
	if f.RelFunc != nil {
		return f.RelFunc(f.base)
	}
	return "", nil
}

func (f *FakePluginFS) Type() plugins.FSType {
	if f.TypeFunc != nil {
		return f.TypeFunc()
	}
	return "fake"
}

func (f *FakePluginFS) Base() string {
	return f.base
}

func (f *FakePluginFS) Files() ([]string, error) {
	return []string{}, nil
}

func (f *FakePluginFS) Remove() error {
	if f.RemoveFunc != nil {
		return f.RemoveFunc()
	}
	return nil
}

type FakeSourceRegistry struct {
	ListFunc func(_ context.Context) []plugins.PluginSource
}

func (s *FakeSourceRegistry) List(ctx context.Context) []plugins.PluginSource {
	if s.ListFunc != nil {
		return s.ListFunc(ctx)
	}
	return []plugins.PluginSource{}
}

type FakePluginSource struct {
	PluginClassFunc      func(ctx context.Context) plugins.Class
	DiscoverFunc         func(ctx context.Context) ([]*plugins.FoundBundle, error)
	DefaultSignatureFunc func(ctx context.Context) (plugins.Signature, bool)
}

func (s *FakePluginSource) PluginClass(ctx context.Context) plugins.Class {
	if s.PluginClassFunc != nil {
		return s.PluginClassFunc(ctx)
	}
	return ""
}

func (s *FakePluginSource) Discover(ctx context.Context) ([]*plugins.FoundBundle, error) {
	if s.DiscoverFunc != nil {
		return s.DiscoverFunc(ctx)
	}
	return []*plugins.FoundBundle{}, nil
}

func (s *FakePluginSource) DefaultSignature(ctx context.Context, _ string) (plugins.Signature, bool) {
	if s.DefaultSignatureFunc != nil {
		return s.DefaultSignatureFunc(ctx)
	}
	return plugins.Signature{}, false
}

type FakePluginFileStore struct {
	FileFunc func(ctx context.Context, pluginID, pluginVersion, filename string) (*plugins.File, error)
}

func (f *FakePluginFileStore) File(ctx context.Context, pluginID, pluginVersion, filename string) (*plugins.File, error) {
	if f.FileFunc != nil {
		return f.FileFunc(ctx, pluginID, pluginVersion, filename)
	}
	return nil, nil
}

type FakeAuthService struct {
	Result *auth.ExternalService
}

func (f *FakeAuthService) HasExternalService(ctx context.Context, pluginID string) (bool, error) {
	return f.Result != nil, nil
}

func (f *FakeAuthService) RegisterExternalService(ctx context.Context, pluginID string, pType string, svc *auth.IAM) (*auth.ExternalService, error) {
	return f.Result, nil
}

func (f *FakeAuthService) RemoveExternalService(ctx context.Context, pluginID string) error {
	return nil
}

type FakeDiscoverer struct {
	DiscoverFunc func(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error)
}

func (f *FakeDiscoverer) Discover(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) {
	if f.DiscoverFunc != nil {
		return f.DiscoverFunc(ctx, src)
	}
	return []*plugins.FoundBundle{}, nil
}

type FakeBootstrapper struct {
	BootstrapFunc func(ctx context.Context, src plugins.PluginSource, bundle *plugins.FoundBundle) ([]*plugins.Plugin, error)
}

func (f *FakeBootstrapper) Bootstrap(ctx context.Context, src plugins.PluginSource, bundle *plugins.FoundBundle) ([]*plugins.Plugin, error) {
	if f.BootstrapFunc != nil {
		return f.BootstrapFunc(ctx, src, bundle)
	}
	return []*plugins.Plugin{}, nil
}

type FakeValidator struct {
	ValidateFunc func(ctx context.Context, ps *plugins.Plugin) error
}

func (f *FakeValidator) Validate(ctx context.Context, ps *plugins.Plugin) error {
	if f.ValidateFunc != nil {
		return f.ValidateFunc(ctx, ps)
	}
	return nil
}

type FakeInitializer struct {
	IntializeFunc func(ctx context.Context, ps *plugins.Plugin) (*plugins.Plugin, error)
}

func (f *FakeInitializer) Initialize(ctx context.Context, ps *plugins.Plugin) (*plugins.Plugin, error) {
	if f.IntializeFunc != nil {
		return f.IntializeFunc(ctx, ps)
	}
	return ps, nil
}

type FakeTerminator struct {
	TerminateFunc func(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error)
}

func (f *FakeTerminator) Terminate(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) {
	if f.TerminateFunc != nil {
		return f.TerminateFunc(ctx, p)
	}
	return nil, nil
}

type FakeBackendPlugin struct {
	Managed bool

	StartCount     int
	StopCount      int
	Decommissioned bool
	Running        bool

	// ExitedCheckDoneOrStopped is used to signal that the Exited() or Stop() method has been called.
	ExitedCheckDoneOrStopped chan struct{}

	mutex sync.RWMutex
	backendplugin.Plugin
}

func NewFakeBackendPlugin(managed bool) *FakeBackendPlugin {
	return &FakeBackendPlugin{
		Managed:                  managed,
		ExitedCheckDoneOrStopped: make(chan struct{}),
	}
}

func (p *FakeBackendPlugin) Start(_ context.Context) error {
	p.mutex.Lock()
	defer p.mutex.Unlock()
	p.Running = true
	p.StartCount++
	return nil
}

func (p *FakeBackendPlugin) Stop(_ context.Context) error {
	p.mutex.Lock()
	defer p.mutex.Unlock()
	p.Running = false
	p.StopCount++
	go func() { p.ExitedCheckDoneOrStopped <- struct{}{} }()
	return nil
}

func (p *FakeBackendPlugin) Decommission() error {
	p.mutex.Lock()
	defer p.mutex.Unlock()
	p.Decommissioned = true
	return nil
}

func (p *FakeBackendPlugin) IsDecommissioned() bool {
	p.mutex.RLock()
	defer p.mutex.RUnlock()
	return p.Decommissioned
}

func (p *FakeBackendPlugin) IsManaged() bool {
	p.mutex.RLock()
	defer p.mutex.RUnlock()
	return p.Managed
}

func (p *FakeBackendPlugin) Exited() bool {
	p.mutex.RLock()
	defer p.mutex.RUnlock()
	go func() { p.ExitedCheckDoneOrStopped <- struct{}{} }()
	return !p.Running
}

func (p *FakeBackendPlugin) Kill() {
	p.mutex.Lock()
	defer p.mutex.Unlock()
	p.Running = false
}

func (p *FakeBackendPlugin) Target() backendplugin.Target {
	return "test-target"
}

func (p *FakeBackendPlugin) Logger() log.Logger {
	return log.NewTestLogger()
}

type AssetProvider struct {
	ModuleFunc    func(plugin pluginassets.PluginInfo) (string, error)
	AssetPathFunc func(plugin pluginassets.PluginInfo, assetPath ...string) (string, error)
}

func NewFakeAssetProvider() *AssetProvider {
	return &AssetProvider{}
}

func (p *AssetProvider) Module(plugin pluginassets.PluginInfo) (string, error) {
	if p.ModuleFunc != nil {
		return p.ModuleFunc(plugin)
	}
	return "", nil
}

func (p *AssetProvider) AssetPath(plugin pluginassets.PluginInfo, assetPath ...string) (string, error) {
	if p.AssetPathFunc != nil {
		return p.AssetPathFunc(plugin, assetPath...)
	}
	return "", nil
}

type FakeErrorTracker struct {
	RecordFunc func(ctx context.Context, err *plugins.Error)
	ClearFunc  func(ctx context.Context, pluginID string)
	ErrorsFunc func(ctx context.Context) []*plugins.Error
}

func NewFakeErrorTracker() *FakeErrorTracker {
	return &FakeErrorTracker{}
}

func (t *FakeErrorTracker) Record(ctx context.Context, err *plugins.Error) {
	if t.RecordFunc != nil {
		t.RecordFunc(ctx, err)
		return
	}
}

func (t *FakeErrorTracker) Clear(ctx context.Context, pluginID string) {
	if t.ClearFunc != nil {
		t.ClearFunc(ctx, pluginID)
		return
	}
}

func (t *FakeErrorTracker) Errors(ctx context.Context) []*plugins.Error {
	if t.ErrorsFunc != nil {
		return t.ErrorsFunc(ctx)
	}
	return nil
}

func (t *FakeErrorTracker) Error(ctx context.Context, pluginID string) *plugins.Error {
	return &plugins.Error{}
}
