package expr

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

	"github.com/grafana/grafana-plugin-sdk-go/backend"
	"github.com/grafana/grafana-plugin-sdk-go/data"
	"github.com/stretchr/testify/require"

	"github.com/grafana/grafana/pkg/expr/ml"
	"github.com/grafana/grafana/pkg/plugins"
	"github.com/grafana/grafana/pkg/services/user"
)

func TestMLNodeExecute(t *testing.T) {
	timeNow := time.Now()
	expectedOrgID := int64(123)
	timeRange := RelativeTimeRange{
		From: -10 * time.Hour,
		To:   0,
	}
	request := &Request{
		Headers: map[string]string{
			"test": "test",
		},
		Debug:   false,
		OrgId:   expectedOrgID,
		Queries: nil,
		User: &user.SignedInUser{
			UserID: 1,
		},
	}

	expectedResponse := &backend.CallResourceResponse{
		Status:  200,
		Headers: nil,
		Body:    []byte("test-response"),
	}

	pluginsClient := &recordingCallResourceHandler{
		response: expectedResponse,
	}

	pluginCtx := &fakePluginContextProvider{
		result: map[string]*backend.AppInstanceSettings{
			mlPluginID: {
				JSONData: json.RawMessage(`{ "initialized": true }`),
			},
		},
	}

	s := &Service{
		cfg:           nil,
		dataService:   nil,
		pCtxProvider:  pluginCtx,
		features:      nil,
		pluginsClient: pluginsClient,
		tracer:        nil,
		metrics:       newMetrics(nil),
	}

	cmdResponse := data.NewFrame("test",
		data.NewField("Time", nil, []time.Time{time.Unix(1, 0)}),
		data.NewField("Value", nil, []*float64{fp(1)}),
	)

	cmd := &ml.FakeCommand{
		Method:  http.MethodPost,
		Path:    "/test/ml",
		Payload: []byte(`{}`),
		Response: &backend.QueryDataResponse{
			Responses: map[string]backend.DataResponse{
				"A": {
					Frames: data.Frames{
						cmdResponse,
					},
					Status: backend.StatusOK,
				},
			},
		},
		Error: nil,
	}

	node := &MLNode{
		baseNode:  baseNode{},
		command:   cmd,
		TimeRange: timeRange,
		request:   request,
	}

	result, err := node.Execute(context.Background(), timeNow, nil, s)

	require.NoError(t, err)
	require.NotNil(t, result)
	require.NotEmpty(t, result.Values)

	t.Run("should get plugin context", func(t *testing.T) {
		require.NotEmpty(t, pluginCtx.recordings)
		require.Equal(t, "Get", pluginCtx.recordings[0].method)
		require.Equal(t, mlPluginID, pluginCtx.recordings[0].params[0])
		require.Equal(t, request.User, pluginCtx.recordings[0].params[1])
	})

	t.Run("should call command execute with correct parameters", func(t *testing.T) {
		require.NotEmpty(t, cmd.Recordings)
		rec := cmd.Recordings[0]
		require.Equal(t, timeRange.AbsoluteTime(timeNow).From, rec.From)
		require.Equal(t, timeRange.AbsoluteTime(timeNow).To, rec.To)
		require.NotNil(t, rec.Response)
		require.Equal(t, expectedResponse.Status, rec.Response.Status())
		require.Equal(t, expectedResponse.Body, rec.Response.Body())
	})

	t.Run("should call plugin API", func(t *testing.T) {
		require.NotEmpty(t, pluginsClient.recordings)
		req := pluginsClient.recordings[0]
		require.Equal(t, cmd.Payload, req.Body)
		require.Equal(t, cmd.Path, req.Path)
		require.Equal(t, cmd.Method, req.Method)

		require.NotNil(t, req.PluginContext)
		require.Equal(t, mlPluginID, req.PluginContext.PluginID)

		t.Run("should append request headers to API call", func(t *testing.T) {
			for key, value := range request.Headers {
				require.Contains(t, req.Headers, key)
				require.Equal(t, value, req.Headers[key][0])
			}
		})
	})

	t.Run("should fail if plugin is not installed", func(t *testing.T) {
		s := &Service{
			cfg:         nil,
			dataService: nil,
			pCtxProvider: &fakePluginContextProvider{
				errorResult: plugins.ErrPluginNotRegistered,
			},
			features:      nil,
			pluginsClient: nil,
			tracer:        nil,
			metrics:       nil,
		}

		_, err := node.Execute(context.Background(), timeNow, nil, s)
		require.ErrorIs(t, err, errMLPluginDoesNotExist)
	})

	t.Run("should fail if plugin settings cannot be retrieved", func(t *testing.T) {
		expectedErr := errors.New("test-error")
		s := &Service{
			cfg:         nil,
			dataService: nil,
			pCtxProvider: &fakePluginContextProvider{
				errorResult: expectedErr,
			},
			features:      nil,
			pluginsClient: nil,
			tracer:        nil,
			metrics:       nil,
		}

		_, err := node.Execute(context.Background(), timeNow, nil, s)
		require.ErrorIs(t, err, expectedErr)
	})

	t.Run("should fail if plugin is not initialized", func(t *testing.T) {
		s := &Service{
			cfg:         nil,
			dataService: nil,
			pCtxProvider: &fakePluginContextProvider{
				result: map[string]*backend.AppInstanceSettings{
					mlPluginID: {
						JSONData: json.RawMessage(`{}`),
					},
				},
			},
			features:      nil,
			pluginsClient: nil,
			tracer:        nil,
			metrics:       nil,
		}

		_, err := node.Execute(context.Background(), timeNow, nil, s)
		require.ErrorIs(t, err, errMLPluginDoesNotExist)
	})

	t.Run("should return QueryError if command failed", func(t *testing.T) {
		s := &Service{
			cfg:           nil,
			dataService:   nil,
			pCtxProvider:  pluginCtx,
			features:      nil,
			pluginsClient: pluginsClient,
			tracer:        nil,
			metrics:       newMetrics(nil),
		}

		cmd := &ml.FakeCommand{
			Error: errors.New("failed to execute command"),
		}

		node := &MLNode{
			baseNode:  baseNode{},
			command:   cmd,
			TimeRange: timeRange,
			request:   request,
		}

		_, err := node.Execute(context.Background(), timeNow, nil, s)
		require.IsType(t, err, MakeQueryError("A", "", expectedError{}))
		require.ErrorIs(t, err, cmd.Error)
	})
}
