package updatemanager

import (
	"context"
	"io"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"testing"

	"github.com/open-feature/go-sdk/openfeature"
	"github.com/open-feature/go-sdk/openfeature/memprovider"
	"github.com/stretchr/testify/require"

	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/infra/tracing"
	"github.com/grafana/grafana/pkg/plugins"
	"github.com/grafana/grafana/pkg/plugins/manager/pluginfakes"
	"github.com/grafana/grafana/pkg/services/featuremgmt"
	"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"
)

type mockPluginPreinstall struct {
	pluginchecker.Preinstall
}

func (m *mockPluginPreinstall) IsPinned(pluginID string) bool {
	return false
}

func TestPluginUpdateChecker_HasUpdate(t *testing.T) {
	t.Run("update is available", func(t *testing.T) {
		updateCheckURL, _ := url.Parse("https://grafana.com/api/plugins/versioncheck")

		svc := PluginsService{
			log: log.New("test"),
			availableUpdates: map[string]availableUpdate{
				"test-ds": {
					localVersion:     "0.9.0",
					availableVersion: "1.0.0",
				},
			},
			pluginStore: &pluginstore.FakePluginStore{
				PluginList: []pluginstore.Plugin{
					{
						JSONData: plugins.JSONData{
							ID:   "test-ds",
							Info: plugins.Info{Version: "0.9.0"},
						},
					},
				},
			},
			updateCheckURL: updateCheckURL,
			updateChecker:  pluginchecker.ProvideService(managedplugins.NewNoop(), provisionedplugins.NewNoop(), &mockPluginPreinstall{}),
			features:       &featuremgmt.FeatureManager{},
		}

		update, exists := svc.HasUpdate(context.Background(), "test-ds")
		require.True(t, exists)
		require.Equal(t, "1.0.0", update)
	})

	t.Run("update is not available", func(t *testing.T) {
		updateCheckURL, _ := url.Parse("https://grafana.com/api/plugins/versioncheck")

		svc := PluginsService{
			availableUpdates: map[string]availableUpdate{
				"test-panel": {
					localVersion:     "0.9.0",
					availableVersion: "0.9.0",
				},
				"test-app": {
					localVersion:     "0.9.0",
					availableVersion: "0.9.0",
				},
			},
			pluginStore: &pluginstore.FakePluginStore{
				PluginList: []pluginstore.Plugin{
					{
						JSONData: plugins.JSONData{
							ID:   "test-ds",
							Info: plugins.Info{Version: "0.9.0"},
						},
					},
					{
						JSONData: plugins.JSONData{
							ID:   "test-panel",
							Info: plugins.Info{Version: "0.9.0"},
						},
					},
					{
						JSONData: plugins.JSONData{
							ID:   "test-app",
							Info: plugins.Info{Version: "0.9.0"},
						},
					},
				},
			},
			updateCheckURL: updateCheckURL,
			updateChecker:  pluginchecker.ProvideService(managedplugins.NewNoop(), provisionedplugins.NewNoop(), &mockPluginPreinstall{}),
		}

		update, exists := svc.HasUpdate(context.Background(), "test-ds")
		require.False(t, exists)
		require.Empty(t, update)

		update, exists = svc.HasUpdate(context.Background(), "test-panel")
		require.False(t, exists)
		require.Empty(t, update)

		update, exists = svc.HasUpdate(context.Background(), "test-app")
		require.False(t, exists)
		require.Empty(t, update)
	})

	t.Run("update is available but plugin is not in store", func(t *testing.T) {
		updateCheckURL, _ := url.Parse("https://grafana.com/api/plugins/versioncheck")

		svc := PluginsService{
			availableUpdates: map[string]availableUpdate{
				"test-panel": {
					localVersion:     "0.9.0",
					availableVersion: "0.9.0",
				},
			},
			pluginStore: &pluginstore.FakePluginStore{
				PluginList: []pluginstore.Plugin{
					{
						JSONData: plugins.JSONData{
							ID:   "test-ds",
							Info: plugins.Info{Version: "1.0.0"},
						},
					},
				},
			},
			updateCheckURL: updateCheckURL,
		}

		update, exists := svc.HasUpdate(context.Background(), "test-panel")
		require.False(t, exists)
		require.Empty(t, update)

		update, exists = svc.HasUpdate(context.Background(), "test-ds")
		require.False(t, exists)
		require.Empty(t, update)
	})
}

func TestPluginUpdateChecker_checkForUpdates(t *testing.T) {
	t.Run("update is available", func(t *testing.T) {
		jsonResp := `[
		  {
			"slug": "test-ds",
			"version": "1.0.12"
		  },
		  {
			"slug": "test-panel",
			"version": "2.5.7"
		  },
		  {
			"slug": "test-core-panel",
			"version": "1.0.0"
		  }
		]`

		updateCheckURL, _ := url.Parse("https://grafana.com/api/plugins/versioncheck")

		svc := PluginsService{
			availableUpdates: map[string]availableUpdate{
				"test-app": {
					localVersion:     "0.5.0",
					availableVersion: "1.0.0",
				},
			},
			pluginStore: &pluginstore.FakePluginStore{
				PluginList: []pluginstore.Plugin{
					{
						JSONData: plugins.JSONData{
							ID:   "test-ds",
							Info: plugins.Info{Version: "0.9.0"},
							Type: plugins.TypeDataSource,
						},
						Class: plugins.ClassExternal,
					},
					{
						JSONData: plugins.JSONData{
							ID:   "test-app",
							Info: plugins.Info{Version: "0.5.0"},
							Type: plugins.TypeApp,
						},
						Class: plugins.ClassExternal,
					},
					{
						JSONData: plugins.JSONData{
							ID:   "test-panel",
							Info: plugins.Info{Version: "2.5.7"},
							Type: plugins.TypePanel,
						},
						Class: plugins.ClassExternal,
					},
					{
						JSONData: plugins.JSONData{
							ID:   "test-core-panel",
							Info: plugins.Info{Version: "0.0.1"},
							Type: plugins.TypePanel,
						},
						Class: plugins.ClassCore,
					},
				},
			},
			httpClient: &fakeHTTPClient{
				fakeResp: jsonResp,
			},
			log:            log.NewNopLogger(),
			tracer:         tracing.InitializeTracerForTest(),
			updateCheckURL: updateCheckURL,
			updateChecker:  pluginchecker.ProvideService(managedplugins.NewNoop(), provisionedplugins.NewNoop(), &mockPluginPreinstall{}),
			features:       &featuremgmt.FeatureManager{},
		}

		svc.instrumentedCheckForUpdates(context.Background())

		require.Equal(t, 1, len(svc.availableUpdates))

		require.Equal(t, "1.0.12", svc.availableUpdates["test-ds"].availableVersion)
		update, exists := svc.HasUpdate(context.Background(), "test-ds")
		require.True(t, exists)
		require.Equal(t, "1.0.12", update)

		require.Empty(t, svc.availableUpdates["test-app"])
		update, exists = svc.HasUpdate(context.Background(), "test-app")
		require.False(t, exists)
		require.Empty(t, update)

		require.Empty(t, svc.availableUpdates["test-panel"])
		update, exists = svc.HasUpdate(context.Background(), "test-panel")
		require.False(t, exists)
		require.Empty(t, update)

		require.Empty(t, svc.availableUpdates["test-core-panel"])
	})
}

func TestPluginUpdateChecker_updateAll(t *testing.T) {
	t.Run("update is available", func(t *testing.T) {
		pluginsFakeStore := map[string]string{}
		availableUpdates := map[string]availableUpdate{
			"test-app-0": {
				localVersion:     "0.9.0",
				availableVersion: "1.0.0",
			},
			"test-app-1": {
				localVersion:     "0.9.0",
				availableVersion: "1.0.0",
			},
			"test-app-2": {
				localVersion:     "0.9.0",
				availableVersion: "1.0.0",
			},
		}

		svc := PluginsService{
			availableUpdates: availableUpdates,
			log:              log.NewNopLogger(),
			tracer:           tracing.InitializeTracerForTest(),
			pluginInstaller: &pluginfakes.FakePluginInstaller{
				AddFunc: func(ctx context.Context, pluginID, version string, opts plugins.AddOpts) error {
					pluginsFakeStore[pluginID] = version
					return nil
				},
				RemoveFunc: func(ctx context.Context, pluginID, version string) error {
					delete(pluginsFakeStore, pluginID)
					return nil
				},
			},
		}

		svc.updateAll(context.Background())

		require.Equal(t, 0, len(svc.availableUpdates))
		require.Equal(t, len(availableUpdates), len(pluginsFakeStore))

		for pluginID, availableUpdate := range availableUpdates {
			require.Equal(t, availableUpdate.availableVersion, pluginsFakeStore[pluginID])
		}
	})
}

type fakeHTTPClient struct {
	fakeResp string

	requestURL string
}

func (c *fakeHTTPClient) Do(req *http.Request) (*http.Response, error) {
	c.requestURL = req.URL.String()

	resp := &http.Response{
		Body: io.NopCloser(strings.NewReader(c.fakeResp)),
	}

	return resp, nil
}

func TestPluginsService_PluginsAutoUpdateFlag(t *testing.T) {
	updateCheckURL, _ := url.Parse("https://grafana.com/api/plugins/versioncheck")

	tests := []struct {
		name         string
		flagEnabled  bool
		expectUpdate bool
	}{
		{
			name:         "pluginsAutoUpdate enabled calls updateAll",
			flagEnabled:  true,
			expectUpdate: true,
		},
		{
			name:         "pluginsAutoUpdate disabled does not call updateAll",
			flagEnabled:  false,
			expectUpdate: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			setupOpenFeatureProvider(t, tt.flagEnabled)

			updateCallCount := 0

			availableUpdates := map[string]availableUpdate{
				"test-plugin": {
					localVersion:     "0.9.0",
					availableVersion: "1.0.0",
				},
			}

			svc := &PluginsService{
				availableUpdates: availableUpdates,
				httpClient: &fakeHTTPClient{
					fakeResp: `[]`,
				},
				log:            log.NewNopLogger(),
				tracer:         tracing.InitializeTracerForTest(),
				updateCheckURL: updateCheckURL,
				updateChecker:  pluginchecker.ProvideService(managedplugins.NewNoop(), provisionedplugins.NewNoop(), &mockPluginPreinstall{}),
				pluginStore:    &pluginstore.FakePluginStore{PluginList: []pluginstore.Plugin{}},
				pluginInstaller: &pluginfakes.FakePluginInstaller{
					AddFunc: func(ctx context.Context, pluginID, version string, opts plugins.AddOpts) error {
						updateCallCount++
						return nil
					},
				},
				grafanaVersion: "10.0.0",
			}

			ctx := context.Background()
			// Test the synchronous initialization work directly, without the long-running ticker loop
			svc.checkAndUpdate(ctx)

			if tt.expectUpdate {
				require.Equal(t, updateCallCount, 1, "updateAll should be called when flag is enabled")
			} else {
				require.Equal(t, 0, updateCallCount, "updateAll should not be called when flag is disabled")
			}
		})
	}
}

var openfeatureTestMutex sync.Mutex

func setupOpenFeatureProvider(t *testing.T, flagValue bool) {
	t.Helper()
	openfeatureTestMutex.Lock()

	flag := memprovider.InMemoryFlag{Key: featuremgmt.FlagPluginsAutoUpdate, Variants: map[string]any{"": flagValue}}

	staticFlags := map[string]memprovider.InMemoryFlag{
		featuremgmt.FlagPluginsAutoUpdate: flag,
	}

	// Create static provider with Grafana's standard flags
	provider, err := featuremgmt.CreateStaticProviderWithStandardFlags(staticFlags)
	require.NoError(t, err)

	err = openfeature.SetProviderAndWait(provider)
	require.NoError(t, err)

	t.Cleanup(func() {
		_ = openfeature.SetProviderAndWait(openfeature.NoopProvider{})
		openfeatureTestMutex.Unlock()
	})
}
