package plugininstaller

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/grafana/grafana/pkg/plugins"
	"github.com/grafana/grafana/pkg/plugins/manager/pluginfakes"
	"github.com/grafana/grafana/pkg/plugins/manager/registry"
	"github.com/grafana/grafana/pkg/plugins/repo"
	"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
	"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker"
	"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
	"github.com/grafana/grafana/pkg/services/pluginsintegration/provisionedplugins"
	"github.com/grafana/grafana/pkg/setting"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/stretchr/testify/require"
)

func TestService_IsDisabled(t *testing.T) {
	tests := []struct {
		name                   string
		preinstallPluginsSync  []setting.InstallPlugin
		preinstallPluginsAsync []setting.InstallPlugin
		expectedDisabled       bool
	}{
		{
			name:                   "enabled when async plugins are configured",
			preinstallPluginsAsync: []setting.InstallPlugin{{ID: "myplugin"}},
			expectedDisabled:       false,
		},
		{
			name:                  "enabled when only sync plugins are configured",
			preinstallPluginsSync: []setting.InstallPlugin{{ID: "myplugin"}},
			expectedDisabled:      false,
		},
		{
			name:             "disabled when no plugins are configured",
			expectedDisabled: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s, err := ProvideService(
				&setting.Cfg{
					PreinstallPluginsSync:  tt.preinstallPluginsSync,
					PreinstallPluginsAsync: tt.preinstallPluginsAsync,
				},
				pluginstore.New(registry.NewInMemory(), &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{}),
				&pluginfakes.FakePluginInstaller{},
				prometheus.NewRegistry(),
				&pluginfakes.FakePluginRepo{},
				&pluginchecker.FakePluginUpdateChecker{},
			)
			require.NoError(t, err)
			require.Equal(t, tt.expectedDisabled, s.IsDisabled())
		})
	}
}

func TestService_Run(t *testing.T) {
	tests := []struct {
		name                 string
		shouldInstall        bool
		shouldThrowError     bool
		pluginsToInstall     []setting.InstallPlugin
		pluginsToInstallSync []setting.InstallPlugin
		existingPlugins      []*plugins.Plugin
		pluginsToFail        []string
		latestPlugin         *repo.PluginArchiveInfo
	}{
		{
			name:             "Installs a plugin",
			shouldInstall:    true,
			pluginsToInstall: []setting.InstallPlugin{{ID: "myplugin"}},
		},
		{
			name:             "Install a plugin with version",
			shouldInstall:    true,
			pluginsToInstall: []setting.InstallPlugin{{ID: "myplugin", Version: "1.0.0"}},
		},
		{
			name:             "Skips already installed plugin",
			shouldThrowError: false,
			shouldInstall:    false,
			pluginsToInstall: []setting.InstallPlugin{{ID: "myplugin"}},
			existingPlugins:  []*plugins.Plugin{{JSONData: plugins.JSONData{ID: "myplugin"}}},
		},
		{
			name:             "Still installs a plugin if the plugin version does not match",
			shouldInstall:    true,
			pluginsToInstall: []setting.InstallPlugin{{ID: "myplugin", Version: "2.0.0"}},
			existingPlugins:  []*plugins.Plugin{{JSONData: plugins.JSONData{ID: "myplugin", Info: plugins.Info{Version: "1.0.0"}}}},
		},
		{
			name:             "Install multiple plugins",
			shouldInstall:    true,
			pluginsToInstall: []setting.InstallPlugin{{ID: "myplugin1"}, {ID: "myplugin2"}},
		},
		{
			name:             "Fails to install a plugin but install the rest",
			shouldInstall:    true,
			pluginsToInstall: []setting.InstallPlugin{{ID: "myplugin1"}, {ID: "myplugin2"}},
			pluginsToFail:    []string{"myplugin1"},
		},
		{
			name:                 "Install a plugin from sync list",
			shouldInstall:        true,
			pluginsToInstallSync: []setting.InstallPlugin{{ID: "myplugin"}},
		},
		{
			name:                 "when installation fails in sync mode, it should throw an error",
			shouldInstall:        false,
			shouldThrowError:     true,
			pluginsToInstallSync: []setting.InstallPlugin{{ID: "myplugin"}},
			pluginsToFail:        []string{"myplugin"},
		},
		{
			name:             "Updates a plugin",
			shouldInstall:    true,
			pluginsToInstall: []setting.InstallPlugin{{ID: "myplugin", Version: ""}},
			existingPlugins:  []*plugins.Plugin{{JSONData: plugins.JSONData{ID: "myplugin", Info: plugins.Info{Version: "1.0.0"}}}},
			latestPlugin:     &repo.PluginArchiveInfo{Version: "1.0.1"},
		},
		{
			name:             "Should not update a plugin if the latest version is installed",
			shouldInstall:    false,
			pluginsToInstall: []setting.InstallPlugin{{ID: "myplugin", Version: ""}},
			existingPlugins:  []*plugins.Plugin{{JSONData: plugins.JSONData{ID: "myplugin", Info: plugins.Info{Version: "1.0.0"}}}},
			latestPlugin:     &repo.PluginArchiveInfo{Version: "1.0.0"},
		},
		{
			name:             "Should not update a plugin if the latest version is a major version",
			shouldInstall:    false,
			pluginsToInstall: []setting.InstallPlugin{{ID: "myplugin", Version: ""}},
			existingPlugins:  []*plugins.Plugin{{JSONData: plugins.JSONData{ID: "myplugin", Info: plugins.Info{Version: "1.0.0"}}}},
			latestPlugin:     &repo.PluginArchiveInfo{Version: "2.0.0"},
		},
		{
			name:             "Should install a plugin with a URL",
			shouldInstall:    true,
			pluginsToInstall: []setting.InstallPlugin{{ID: "myplugin", URL: "https://example.com/myplugin.tar.gz"}},
		},
		{
			name:             "Should not update a plugin if the current version is greater than the latest version",
			shouldInstall:    false,
			pluginsToInstall: []setting.InstallPlugin{{ID: "myplugin", Version: ""}},
			existingPlugins:  []*plugins.Plugin{{JSONData: plugins.JSONData{ID: "myplugin", Info: plugins.Info{Version: "1.0.1"}}}},
			latestPlugin:     &repo.PluginArchiveInfo{Version: "1.0.0"},
		},
		{
			name:             "Should not update a plugin if the current version is equal to the latest version, ignoring the prerelease",
			shouldInstall:    false,
			pluginsToInstall: []setting.InstallPlugin{{ID: "myplugin", Version: ""}},
			existingPlugins:  []*plugins.Plugin{{JSONData: plugins.JSONData{ID: "myplugin", Info: plugins.Info{Version: "1.0.0"}}}},
			latestPlugin:     &repo.PluginArchiveInfo{Version: "1.0.0-rc.1"},
		},
		{
			name:                 "should install all plugins - sync and async",
			shouldInstall:        true,
			pluginsToInstallSync: []setting.InstallPlugin{{ID: "myplugin"}},
			pluginsToInstall:     []setting.InstallPlugin{{ID: "myplugin2"}},
		},
		{
			name:                 "should install a plugin with a URL regardless of versioning",
			shouldInstall:        true,
			pluginsToInstallSync: []setting.InstallPlugin{{ID: "our-plugin-datasource", URL: "https://s3.our.domain/grafana-plugins/our-plugin-datasource-1.2.1+linux.zip"}},
			existingPlugins:      []*plugins.Plugin{{JSONData: plugins.JSONData{ID: "our-plugin-datasource", Info: plugins.Info{Version: "1.2.2"}}}},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			preg := registry.NewInMemory()
			for _, plugin := range tt.existingPlugins {
				err := preg.Add(context.Background(), plugin)
				require.NoError(t, err)
			}
			installed := 0
			installedFromURL := 0
			store, err := pluginstore.NewPluginStoreForTest(preg, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{})
			require.NoError(t, err)
			s, err := ProvideService(
				&setting.Cfg{
					PreinstallPluginsAsync: tt.pluginsToInstall,
					PreinstallPluginsSync:  tt.pluginsToInstallSync,
					PreinstallAutoUpdate:   true,
				},
				store,
				&pluginfakes.FakePluginInstaller{
					AddFunc: func(ctx context.Context, pluginID string, version string, opts plugins.AddOpts) error {
						for _, plugin := range tt.pluginsToFail {
							if plugin == pluginID {
								return errors.New("Failed to install plugin")
							}
						}
						if !tt.shouldInstall {
							t.Fatal("Should not install plugin")
							return errors.New("Should not install plugin")
						}
						allPluginsToInstall := append(tt.pluginsToInstallSync, tt.pluginsToInstall...)
						for _, plugin := range allPluginsToInstall {
							if plugin.ID == pluginID && plugin.Version == version {
								if opts.URL() != "" {
									installedFromURL++
								} else {
									installed++
								}
							}
						}
						return nil
					},
				},
				prometheus.NewRegistry(),
				&pluginfakes.FakePluginRepo{
					GetPluginArchiveInfoFunc: func(_ context.Context, pluginID, version string, _ repo.CompatOpts) (*repo.PluginArchiveInfo, error) {
						return tt.latestPlugin, nil
					},
				},
				pluginchecker.ProvideService(
					managedplugins.NewNoop(),
					provisionedplugins.NewNoop(),
					&pluginchecker.FakePluginPreinstall{},
				),
			)
			require.NoError(t, err)

			t.Cleanup(func() {
				s.StopAsync()
				ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
				defer cancel()
				err := s.AwaitTerminated(ctx)
				if tt.shouldThrowError {
					require.ErrorContains(t, err, "Failed to install plugin")
					return
				}
				require.NoError(t, err)
			})

			err = s.StartAsync(context.Background())
			require.NoError(t, err)
			err = s.AwaitRunning(context.Background())
			if tt.shouldThrowError {
				require.ErrorContains(t, err, "Failed to install plugin")
				return
			}
			require.NoError(t, err)

			<-s.installComplete

			if tt.shouldInstall {
				expectedInstalled := 0
				expectedInstalledFromURL := 0
				allPluginsToInstall := append(tt.pluginsToInstallSync, tt.pluginsToInstall...)
				for _, plugin := range allPluginsToInstall {
					expectedFailed := false
					for _, pluginFail := range tt.pluginsToFail {
						if plugin.ID == pluginFail {
							expectedFailed = true
							break
						}
					}
					if expectedFailed {
						continue
					}
					if plugin.URL != "" {
						expectedInstalledFromURL++
					} else {
						expectedInstalled++
					}
				}
				require.Equal(t, expectedInstalled, installed)
				require.Equal(t, expectedInstalledFromURL, installedFromURL)
			}
		})
	}
}
