package ml

import (
	"errors"
	"fmt"
	"net/http"
	"slices"
	"testing"
	"time"

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

	"github.com/grafana/grafana/pkg/api/response"
)

func TestOutlierExec(t *testing.T) {
	outlier := OutlierCommand{
		config: OutlierCommandConfiguration{
			DatasourceType: "prometheus",
			DatasourceUID:  "a4ce599c-4c93-44b9-be5b-76385b8c01be",
			QueryParams: map[string]any{
				"expr":  "go_goroutines{}",
				"range": true,
				"refId": "A",
			},
			Algorithm: map[string]any{
				"name": "dbscan",
				"config": map[string]any{
					"epsilon": 7.667,
				},
				"sensitivity": 0.83,
			},
			ResponseType: "binary",
		},
		appURL:   "https://grafana.com",
		interval: 1000 * time.Second,
	}

	t.Run("should generate expected parameters for request", func(t *testing.T) {
		to := time.Now().UTC()
		from := to.Add(-10 * time.Hour)

		called := false
		_, err := outlier.Execute(from, to, func(method string, path string, payload []byte) (response.Response, error) {
			require.Equal(t, "POST", method)
			require.Equal(t, "/proxy/api/v1/outlier", path)

			assert.JSONEq(t, fmt.Sprintf(`{
			  "data": {
			    "attributes": {
			      "datasource_type": "prometheus",
			      "datasource_uid": "a4ce599c-4c93-44b9-be5b-76385b8c01be",
			      "query_params": {
			        "expr": "go_goroutines{}",
			        "range": true,
			        "refId": "A"
			      },
			      "algorithm": {
			        "config": {
			          "epsilon": 7.667
			        },
			        "name": "dbscan",
			        "sensitivity": 0.83
			      },
			      "response_type": "binary",
			      "grafana_url": "https://grafana.com",
			      "start_end_attributes": {
			        "start": "%s",
			        "end": "%s",
			        "interval": 1000000
			      }
			    }
			  }
			}`, from.Format(timeFormat), to.Format(timeFormat)), string(payload))

			called = true
			return nil, nil
		})
		require.Truef(t, called, "request function was not called")
		require.ErrorContains(t, err, "response is nil")
	})

	successResponse := `{"status":"success","data":{"results":{"A":{"status":200,"frames":[{"schema":{"name":"test","fields":[{"name":"Time","type":"time","typeInfo":{"frame":"time.Time"}},{"name":"Value","type":"number","typeInfo":{"frame":"float64"},"labels":{"instance":"test"}}]},"data":{"values":[[1686945300000],[0]]}}]}}}}`

	testCases := []struct {
		name     string
		response response.Response
		assert   func(t *testing.T, r *backend.QueryDataResponse, err error)
	}{
		{
			name:     "should return parsed frames when 200",
			response: response.CreateNormalResponse(nil, []byte(successResponse), http.StatusOK),
			assert: func(t *testing.T, r *backend.QueryDataResponse, err error) {
				require.NoError(t, err)
				require.NotNil(t, r)
				require.Contains(t, r.Responses, "A")
				require.NoError(t, r.Responses["A"].Error)
				require.Equal(t, backend.StatusOK, r.Responses["A"].Status)
				require.Len(t, r.Responses["A"].Frames, 1)
			},
		},
		{
			name:     "should return nil if 204",
			response: response.CreateNormalResponse(nil, []byte(`{"status": "success"}`), http.StatusNoContent),
			assert: func(t *testing.T, r *backend.QueryDataResponse, err error) {
				require.NoError(t, err)
				require.Nil(t, r)
			},
		},
		{
			name:     "should return error if any status and body has status error",
			response: response.CreateNormalResponse(nil, []byte(`{"status": "error"}`), 0),
			assert: func(t *testing.T, r *backend.QueryDataResponse, err error) {
				require.ErrorContains(t, err, "ML API responded with error")
				require.Nil(t, r)
			},
		},
		{
			name:     "should return error with explanations if any status and body has status error",
			response: response.CreateNormalResponse(nil, []byte(`{"status": "error", "error": "test-error"}`), 0),
			assert: func(t *testing.T, r *backend.QueryDataResponse, err error) {
				require.ErrorContains(t, err, "ML API responded with error")
				require.ErrorContains(t, err, "test-error")
				require.Nil(t, r)
			},
		},
		{
			name:     "should return error response is empty",
			response: response.CreateNormalResponse(nil, []byte(``), 0),
			assert: func(t *testing.T, r *backend.QueryDataResponse, err error) {
				require.ErrorContains(t, err, "unexpected format of the response from ML API")
				require.Nil(t, r)
			},
		},
		{
			name:     "should return error response is not a valid JSON",
			response: response.CreateNormalResponse(nil, []byte(`{`), 0),
			assert: func(t *testing.T, r *backend.QueryDataResponse, err error) {
				require.ErrorContains(t, err, "unexpected format of the response from ML API")
				require.Nil(t, r)
			},
		},
		{
			name:     "should error if response is nil and no error",
			response: nil,
			assert: func(t *testing.T, r *backend.QueryDataResponse, err error) {
				require.ErrorContains(t, err, "response is nil")
				require.Nil(t, r)
			},
		},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			to := time.Now()
			from := to.Add(-10 * time.Hour)

			resp, err := outlier.Execute(from, to, func(method string, path string, payload []byte) (response.Response, error) {
				return tc.response, nil
			})
			tc.assert(t, resp, err)
		})
	}

	t.Run("should return error if status is not known", func(t *testing.T) {
		knownStatuses := []int{http.StatusOK, http.StatusNoContent}

		for status := 100; status < 600; status++ {
			if http.StatusText(status) == "" || slices.Contains(knownStatuses, status) {
				continue
			}
			to := time.Now()
			from := to.Add(-10 * time.Hour)

			resp, err := outlier.Execute(from, to, func(method string, path string, payload []byte) (response.Response, error) {
				return response.CreateNormalResponse(nil, []byte(successResponse), status), nil
			})
			require.ErrorContains(t, err, fmt.Sprintf("unexpected status %d returned by ML API", status))
			require.Nil(t, resp)
		}
	})

	t.Run("should propagate error from request function", func(t *testing.T) {
		to := time.Now()
		from := to.Add(-10 * time.Hour)

		resp, err := outlier.Execute(from, to, func(method string, path string, payload []byte) (response.Response, error) {
			return nil, errors.New("test-error")
		})
		require.ErrorContains(t, err, "test-error")
		require.Nil(t, resp)
	})
}
