package clientmiddleware

import (
	"context"
	"encoding/json"
	"net/http"
	"testing"

	"github.com/grafana/grafana-plugin-sdk-go/backend"
	"github.com/grafana/grafana-plugin-sdk-go/backend/handlertest"
	"github.com/grafana/grafana/pkg/services/caching"
	"github.com/grafana/grafana/pkg/services/contexthandler"
	"github.com/grafana/grafana/pkg/services/featuremgmt"
	"github.com/grafana/grafana/pkg/services/user"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestCachingMiddleware(t *testing.T) {
	t.Run("When QueryData is called", func(t *testing.T) {
		req, err := http.NewRequest(http.MethodGet, "/query", nil)
		require.NoError(t, err)

		cs := caching.NewFakeOSSCachingService()
		cachingServiceClient := caching.ProvideCachingServiceClient(cs, nil)
		cdt := handlertest.NewHandlerMiddlewareTest(t,
			WithReqContext(req, &user.SignedInUser{}),
			handlertest.WithMiddlewares(NewCachingMiddleware(cachingServiceClient)),
		)

		jsonDataMap := map[string]any{}
		jsonDataBytes, err := json.Marshal(&jsonDataMap)
		require.NoError(t, err)

		pluginCtx := backend.PluginContext{
			DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{
				JSONData: jsonDataBytes,
			},
		}

		// Populated by clienttest.WithReqContext
		reqCtx := contexthandler.FromContext(req.Context())
		require.NotNil(t, reqCtx)

		qdr := &backend.QueryDataRequest{
			PluginContext: pluginCtx,
		}

		// Track whether the update cache fn was called, depending on what the response headers are in the cache request
		var updateCacheCalled bool
		dataResponse := caching.CachedQueryDataResponse{
			Response: &backend.QueryDataResponse{},
			UpdateCacheFn: func(ctx context.Context, qdr *backend.QueryDataResponse) {
				updateCacheCalled = true
			},
		}

		t.Run("If cache returns a hit, no queries are issued", func(t *testing.T) {
			t.Cleanup(func() {
				updateCacheCalled = false
				cs.Reset()
			})

			cs.ReturnHit = true
			cs.ReturnQueryResponse = dataResponse

			resp, err := cdt.MiddlewareHandler.QueryData(req.Context(), qdr)
			assert.NoError(t, err)
			// Cache service is called once
			cs.AssertCalls(t, "HandleQueryRequest", 1)
			// Equals the mocked response
			assert.NotNil(t, resp)
			assert.Equal(t, dataResponse.Response, resp)
			// Cache was not updated by the middleware
			assert.False(t, updateCacheCalled)
		})

		t.Run("If cache returns a miss, queries are issued and the update cache function is called", func(t *testing.T) {
			origShouldCacheQuery := caching.ShouldCacheQuery
			var shouldCacheQueryCalled bool
			caching.ShouldCacheQuery = func(resp *backend.QueryDataResponse) bool {
				shouldCacheQueryCalled = true
				return true
			}

			t.Cleanup(func() {
				updateCacheCalled = false
				shouldCacheQueryCalled = false
				caching.ShouldCacheQuery = origShouldCacheQuery
				cs.Reset()
			})

			cs.ReturnHit = false
			cs.ReturnQueryResponse = dataResponse

			resp, err := cdt.MiddlewareHandler.QueryData(req.Context(), qdr)
			assert.NoError(t, err)
			// Cache service is called once
			cs.AssertCalls(t, "HandleQueryRequest", 1)
			// Equals nil (returned by the decorator test)
			assert.Nil(t, resp)
			// Since it was a miss, the middleware called the update func
			assert.True(t, updateCacheCalled)
			// Since the feature flag was not set, the middleware did not call shouldCacheQuery
			assert.False(t, shouldCacheQueryCalled)
		})

		t.Run("with async queries", func(t *testing.T) {
			cachingServiceClient := caching.ProvideCachingServiceClient(cs, featuremgmt.WithFeatures(featuremgmt.FlagAwsAsyncQueryCaching))
			asyncCdt := handlertest.NewHandlerMiddlewareTest(t,
				WithReqContext(req, &user.SignedInUser{}),
				handlertest.WithMiddlewares(
					NewCachingMiddleware(cachingServiceClient)),
			)
			t.Run("If shoudCacheQuery returns true update cache function is called", func(t *testing.T) {
				origShouldCacheQuery := caching.ShouldCacheQuery
				var shouldCacheQueryCalled bool
				caching.ShouldCacheQuery = func(resp *backend.QueryDataResponse) bool {
					shouldCacheQueryCalled = true
					return true
				}

				t.Cleanup(func() {
					updateCacheCalled = false
					shouldCacheQueryCalled = false
					caching.ShouldCacheQuery = origShouldCacheQuery
					cs.Reset()
				})

				cs.ReturnHit = false
				cs.ReturnQueryResponse = dataResponse

				resp, err := asyncCdt.MiddlewareHandler.QueryData(req.Context(), qdr)
				assert.NoError(t, err)
				// Cache service is called once
				cs.AssertCalls(t, "HandleQueryRequest", 1)
				// Equals nil (returned by the decorator test)
				assert.Nil(t, resp)
				// Since it was a miss, the middleware called the update func
				assert.True(t, updateCacheCalled)
				// Since the feature flag set, the middleware called shouldCacheQuery
				assert.True(t, shouldCacheQueryCalled)
			})

			t.Run("If shoudCacheQuery returns false update cache function is not called", func(t *testing.T) {
				origShouldCacheQuery := caching.ShouldCacheQuery
				var shouldCacheQueryCalled bool
				caching.ShouldCacheQuery = func(resp *backend.QueryDataResponse) bool {
					shouldCacheQueryCalled = true
					return false
				}

				t.Cleanup(func() {
					updateCacheCalled = false
					shouldCacheQueryCalled = false
					caching.ShouldCacheQuery = origShouldCacheQuery
					cs.Reset()
				})

				cs.ReturnHit = false
				cs.ReturnQueryResponse = dataResponse

				resp, err := asyncCdt.MiddlewareHandler.QueryData(req.Context(), qdr)
				assert.NoError(t, err)
				// Cache service is called once
				cs.AssertCalls(t, "HandleQueryRequest", 1)
				// Equals nil (returned by the decorator test)
				assert.Nil(t, resp)
				// Since it was a miss, the middleware called the update func
				assert.False(t, updateCacheCalled)
				// Since the feature flag set, the middleware called shouldCacheQuery
				assert.True(t, shouldCacheQueryCalled)
			})
		})
	})

	t.Run("When CallResource is called", func(t *testing.T) {
		req, err := http.NewRequest(http.MethodGet, "/resource/blah", nil)
		require.NoError(t, err)

		// This is the response returned by the HandleResourceRequest call
		// Track whether the update cache fn was called, depending on what the response headers are in the cache request
		var updateCacheCalled bool
		dataResponse := caching.CachedResourceDataResponse{
			Response: &backend.CallResourceResponse{
				Status: 200,
				Body:   []byte("bogus"),
			},
			UpdateCacheFn: func(ctx context.Context, rdr *backend.CallResourceResponse) {
				updateCacheCalled = true
			},
		}

		// This is the response sent via the passed-in sender when there is a cache miss
		simulatedPluginResponse := &backend.CallResourceResponse{
			Status: 201,
			Body:   []byte("bogus"),
		}

		cs := caching.NewFakeOSSCachingService()
		cachingServiceClient := caching.ProvideCachingServiceClient(cs, nil)
		cdt := handlertest.NewHandlerMiddlewareTest(t,
			WithReqContext(req, &user.SignedInUser{}),
			handlertest.WithMiddlewares(NewCachingMiddleware(cachingServiceClient)),
			handlertest.WithResourceResponses([]*backend.CallResourceResponse{simulatedPluginResponse}),
		)

		jsonDataMap := map[string]any{}
		jsonDataBytes, err := json.Marshal(&jsonDataMap)
		require.NoError(t, err)

		pluginCtx := backend.PluginContext{
			DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{
				JSONData: jsonDataBytes,
			},
		}

		// Populated by clienttest.WithReqContext
		reqCtx := contexthandler.FromContext(req.Context())
		require.NotNil(t, reqCtx)

		crr := &backend.CallResourceRequest{
			PluginContext: pluginCtx,
		}

		var sentResponse *backend.CallResourceResponse
		var storeOneResponseCallResourceSender = backend.CallResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error {
			sentResponse = res
			return nil
		})

		t.Run("If cache returns a hit, no resource call is issued", func(t *testing.T) {
			t.Cleanup(func() {
				sentResponse = nil
				cs.Reset()
			})

			cs.ReturnHit = true
			cs.ReturnResourceResponse = dataResponse

			err := cdt.MiddlewareHandler.CallResource(req.Context(), crr, storeOneResponseCallResourceSender)
			assert.NoError(t, err)
			// Cache service is called once
			cs.AssertCalls(t, "HandleResourceRequest", 1)
			// The mocked cached response was sent
			assert.NotNil(t, sentResponse)
			assert.Equal(t, dataResponse.Response, sentResponse)
			// Cache was not updated by the middleware
			assert.False(t, updateCacheCalled)
		})

		t.Run("If cache returns a miss, resource call is issued and the update cache function is called", func(t *testing.T) {
			t.Cleanup(func() {
				sentResponse = nil
				cs.Reset()
			})

			cs.ReturnHit = false
			cs.ReturnResourceResponse = dataResponse

			err := cdt.MiddlewareHandler.CallResource(req.Context(), crr, storeOneResponseCallResourceSender)
			assert.NoError(t, err)
			// Cache service is called once
			cs.AssertCalls(t, "HandleResourceRequest", 1)
			// Simulated plugin response was sent
			assert.NotNil(t, sentResponse)
			assert.Equal(t, simulatedPluginResponse, sentResponse)
			// Since it was a miss, the middleware called the update func
			assert.True(t, updateCacheCalled)
		})
	})

	t.Run("When RequestContext is nil", func(t *testing.T) {
		req, err := http.NewRequest(http.MethodGet, "/doesnt/matter", nil)
		require.NoError(t, err)

		cs := caching.NewFakeOSSCachingService()
		cachingServiceClient := caching.ProvideCachingServiceClient(cs, nil)
		cdt := handlertest.NewHandlerMiddlewareTest(t,
			// Skip the request context in this case
			handlertest.WithMiddlewares(NewCachingMiddleware(cachingServiceClient)),
		)
		reqCtx := contexthandler.FromContext(req.Context())
		require.Nil(t, reqCtx)

		jsonDataMap := map[string]any{}
		jsonDataBytes, err := json.Marshal(&jsonDataMap)
		require.NoError(t, err)

		pluginCtx := backend.PluginContext{
			DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{
				JSONData: jsonDataBytes,
			},
		}

		t.Run("Query caching is skipped", func(t *testing.T) {
			t.Cleanup(func() {
				cs.Reset()
			})

			qdr := &backend.QueryDataRequest{
				PluginContext: pluginCtx,
			}

			resp, err := cdt.MiddlewareHandler.QueryData(context.Background(), qdr)
			assert.NoError(t, err)
			// Cache service is never called
			cs.AssertCalls(t, "HandleQueryRequest", 0)
			// Equals nil (returned by the decorator test)
			assert.Nil(t, resp)
		})

		t.Run("Resource caching is skipped", func(t *testing.T) {
			t.Cleanup(func() {
				cs.Reset()
			})

			crr := &backend.CallResourceRequest{
				PluginContext: pluginCtx,
			}

			err := cdt.MiddlewareHandler.CallResource(req.Context(), crr, nopCallResourceSender)
			assert.NoError(t, err)
			// Cache service is never called
			cs.AssertCalls(t, "HandleResourceRequest", 0)
		})
	})
}
