package api

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"

	"github.com/grafana/grafana-plugin-sdk-go/backend"
	"github.com/grafana/grafana-plugin-sdk-go/data"
	"github.com/grafana/grafana/pkg/api/dtos"
	"github.com/grafana/grafana/pkg/components/simplejson"
	"github.com/grafana/grafana/pkg/models"
	"github.com/grafana/grafana/pkg/services/dashboards"
	"github.com/grafana/grafana/pkg/services/featuremgmt"
	"github.com/grafana/grafana/pkg/services/query"
	"github.com/grafana/grafana/pkg/web/webtest"

	fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes"
)

func TestAPIGetPublicDashboard(t *testing.T) {
	t.Run("It should 404 if featureflag is not enabled", func(t *testing.T) {
		sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures())
		dashSvc := dashboards.NewFakeDashboardService(t)
		dashSvc.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).
			Return(&models.Dashboard{}, nil).Maybe()
		sc.hs.dashboardService = dashSvc

		setInitCtxSignedInViewer(sc.initCtx)
		response := callAPI(
			sc.server,
			http.MethodGet,
			"/api/public/dashboards",
			nil,
			t,
		)
		assert.Equal(t, http.StatusNotFound, response.Code)
		response = callAPI(
			sc.server,
			http.MethodGet,
			"/api/public/dashboards/asdf",
			nil,
			t,
		)
		assert.Equal(t, http.StatusNotFound, response.Code)
	})

	dashboardUid := "dashboard-abcd1234"
	pubdashUid := "pubdash-abcd1234"

	testCases := []struct {
		name                  string
		uid                   string
		expectedHttpResponse  int
		publicDashboardResult *models.Dashboard
		publicDashboardErr    error
	}{
		{
			name:                 "It gets a public dashboard",
			uid:                  pubdashUid,
			expectedHttpResponse: http.StatusOK,
			publicDashboardResult: &models.Dashboard{
				Data: simplejson.NewFromAny(map[string]interface{}{
					"Uid": dashboardUid,
				}),
				IsPublic: true,
			},
			publicDashboardErr: nil,
		},
		{
			name:                  "It should return 404 if isPublicDashboard is false",
			uid:                   pubdashUid,
			expectedHttpResponse:  http.StatusNotFound,
			publicDashboardResult: nil,
			publicDashboardErr:    models.ErrPublicDashboardNotFound,
		},
	}

	for _, test := range testCases {
		t.Run(test.name, func(t *testing.T) {
			sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards))
			dashSvc := dashboards.NewFakeDashboardService(t)
			dashSvc.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).
				Return(test.publicDashboardResult, test.publicDashboardErr)
			sc.hs.dashboardService = dashSvc

			setInitCtxSignedInViewer(sc.initCtx)
			response := callAPI(
				sc.server,
				http.MethodGet,
				fmt.Sprintf("/api/public/dashboards/%v", test.uid),
				nil,
				t,
			)

			assert.Equal(t, test.expectedHttpResponse, response.Code)

			if test.publicDashboardErr == nil {
				var dashResp dtos.DashboardFullWithMeta
				err := json.Unmarshal(response.Body.Bytes(), &dashResp)
				require.NoError(t, err)

				assert.Equal(t, dashboardUid, dashResp.Dashboard.Get("Uid").MustString())
				assert.Equal(t, true, dashResp.Meta.IsPublic)
				assert.Equal(t, false, dashResp.Meta.CanEdit)
				assert.Equal(t, false, dashResp.Meta.CanDelete)
				assert.Equal(t, false, dashResp.Meta.CanSave)
			} else {
				var errResp struct {
					Error string `json:"error"`
				}
				err := json.Unmarshal(response.Body.Bytes(), &errResp)
				require.NoError(t, err)
				assert.Equal(t, test.publicDashboardErr.Error(), errResp.Error)
			}
		})
	}
}

func TestAPIGetPublicDashboardConfig(t *testing.T) {
	pdc := &models.PublicDashboardConfig{IsPublic: true}

	testCases := []struct {
		name                        string
		dashboardUid                string
		expectedHttpResponse        int
		publicDashboardConfigResult *models.PublicDashboardConfig
		publicDashboardConfigError  error
	}{
		{
			name:                        "retrieves public dashboard config when dashboard is found",
			dashboardUid:                "1",
			expectedHttpResponse:        http.StatusOK,
			publicDashboardConfigResult: pdc,
			publicDashboardConfigError:  nil,
		},
		{
			name:                        "returns 404 when dashboard not found",
			dashboardUid:                "77777",
			expectedHttpResponse:        http.StatusNotFound,
			publicDashboardConfigResult: nil,
			publicDashboardConfigError:  models.ErrDashboardNotFound,
		},
		{
			name:                        "returns 500 when internal server error",
			dashboardUid:                "1",
			expectedHttpResponse:        http.StatusInternalServerError,
			publicDashboardConfigResult: nil,
			publicDashboardConfigError:  errors.New("database broken"),
		},
	}

	for _, test := range testCases {
		t.Run(test.name, func(t *testing.T) {
			sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards))
			dashSvc := dashboards.NewFakeDashboardService(t)
			dashSvc.On("GetPublicDashboardConfig", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).
				Return(test.publicDashboardConfigResult, test.publicDashboardConfigError)
			sc.hs.dashboardService = dashSvc

			setInitCtxSignedInViewer(sc.initCtx)
			response := callAPI(
				sc.server,
				http.MethodGet,
				"/api/dashboards/uid/1/public-config",
				nil,
				t,
			)

			assert.Equal(t, test.expectedHttpResponse, response.Code)

			if response.Code == http.StatusOK {
				var pdcResp models.PublicDashboardConfig
				err := json.Unmarshal(response.Body.Bytes(), &pdcResp)
				require.NoError(t, err)
				assert.Equal(t, test.publicDashboardConfigResult, &pdcResp)
			}
		})
	}
}

func TestApiSavePublicDashboardConfig(t *testing.T) {
	testCases := []struct {
		name                  string
		dashboardUid          string
		publicDashboardConfig *models.PublicDashboardConfig
		expectedHttpResponse  int
		saveDashboardError    error
	}{
		{
			name:                  "returns 200 when update persists",
			dashboardUid:          "1",
			publicDashboardConfig: &models.PublicDashboardConfig{IsPublic: true},
			expectedHttpResponse:  http.StatusOK,
			saveDashboardError:    nil,
		},
		{
			name:                  "returns 500 when not persisted",
			expectedHttpResponse:  http.StatusInternalServerError,
			publicDashboardConfig: &models.PublicDashboardConfig{},
			saveDashboardError:    errors.New("backend failed to save"),
		},
		{
			name:                  "returns 404 when dashboard not found",
			expectedHttpResponse:  http.StatusNotFound,
			publicDashboardConfig: &models.PublicDashboardConfig{},
			saveDashboardError:    models.ErrDashboardNotFound,
		},
	}

	for _, test := range testCases {
		t.Run(test.name, func(t *testing.T) {
			sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards))

			dashSvc := dashboards.NewFakeDashboardService(t)
			dashSvc.On("SavePublicDashboardConfig", mock.Anything, mock.AnythingOfType("*dashboards.SavePublicDashboardConfigDTO")).
				Return(&models.PublicDashboardConfig{IsPublic: true}, test.saveDashboardError)
			sc.hs.dashboardService = dashSvc

			setInitCtxSignedInViewer(sc.initCtx)
			response := callAPI(
				sc.server,
				http.MethodPost,
				"/api/dashboards/uid/1/public-config",
				strings.NewReader(`{ "isPublic": true }`),
				t,
			)

			assert.Equal(t, test.expectedHttpResponse, response.Code)

			// check the result if it's a 200
			if response.Code == http.StatusOK {
				val, err := json.Marshal(test.publicDashboardConfig)
				require.NoError(t, err)
				assert.Equal(t, string(val), response.Body.String())
			}
		})
	}
}

// `/public/dashboards/:uid/query`` endpoint test
func TestAPIQueryPublicDashboard(t *testing.T) {
	queryReturnsError := false

	qds := query.ProvideService(
		nil,
		&fakeDatasources.FakeCacheService{
			DataSources: []*models.DataSource{
				{Uid: "mysqlds"},
				{Uid: "promds"},
				{Uid: "promds2"},
			},
		},
		nil,
		&fakePluginRequestValidator{},
		&fakeDatasources.FakeDataSourceService{},
		&fakePluginClient{
			QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
				if queryReturnsError {
					return nil, errors.New("error")
				}

				resp := backend.Responses{}

				for _, query := range req.Queries {
					resp[query.RefID] = backend.DataResponse{
						Frames: []*data.Frame{
							{
								RefID: query.RefID,
								Name:  "query-" + query.RefID,
							},
						},
					}
				}
				return &backend.QueryDataResponse{Responses: resp}, nil
			},
		},
		&fakeOAuthTokenService{},
	)

	setup := func(enabled bool) (*webtest.Server, *dashboards.FakeDashboardService) {
		fakeDashboardService := &dashboards.FakeDashboardService{}

		return SetupAPITestServer(t, func(hs *HTTPServer) {
			hs.queryDataService = qds
			hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards, enabled)
			hs.dashboardService = fakeDashboardService
		}), fakeDashboardService
	}

	t.Run("Status code is 404 when feature toggle is disabled", func(t *testing.T) {
		server, _ := setup(false)

		req := server.NewPostRequest(
			"/api/public/dashboards/abc123/panels/2/query",
			strings.NewReader("{}"),
		)
		resp, err := server.SendJSON(req)
		require.NoError(t, err)
		require.NoError(t, resp.Body.Close())
		require.Equal(t, http.StatusNotFound, resp.StatusCode)
	})

	t.Run("Status code is 400 when the panel ID is invalid", func(t *testing.T) {
		server, _ := setup(true)

		req := server.NewPostRequest(
			"/api/public/dashboards/abc123/panels/notanumber/query",
			strings.NewReader("{}"),
		)
		resp, err := server.SendJSON(req)
		require.NoError(t, err)
		require.NoError(t, resp.Body.Close())
		require.Equal(t, http.StatusBadRequest, resp.StatusCode)
	})

	t.Run("Returns query data when feature toggle is enabled", func(t *testing.T) {
		server, fakeDashboardService := setup(true)

		fakeDashboardService.On(
			"BuildPublicDashboardMetricRequest",
			mock.Anything,
			"abc123",
			int64(2),
		).Return(dtos.MetricRequest{
			Queries: []*simplejson.Json{
				simplejson.MustJson([]byte(`
					{
					  "datasource": {
						"type": "prometheus",
						"uid": "promds"
					  },
					  "exemplar": true,
					  "expr": "query_2_A",
					  "interval": "",
					  "legendFormat": "",
					  "refId": "A"
					}
				`)),
			},
		}, nil)
		req := server.NewPostRequest(
			"/api/public/dashboards/abc123/panels/2/query",
			strings.NewReader("{}"),
		)
		resp, err := server.SendJSON(req)
		require.NoError(t, err)
		bodyBytes, err := ioutil.ReadAll(resp.Body)
		require.NoError(t, err)
		require.JSONEq(
			t,
			`{
				"results": {
					"A": {
						"frames": [
							{
								"data": {
									"values": []
								},
								"schema": {
									"fields": [],
									"refId": "A",
									"name": "query-A"
								}
							}
						]
					}
				}
			}`,
			string(bodyBytes),
		)
		require.NoError(t, resp.Body.Close())
		require.Equal(t, http.StatusOK, resp.StatusCode)
	})

	t.Run("Status code is 500 when the query fails", func(t *testing.T) {
		server, fakeDashboardService := setup(true)

		fakeDashboardService.On(
			"BuildPublicDashboardMetricRequest",
			mock.Anything,
			"abc123",
			int64(2),
		).Return(dtos.MetricRequest{
			Queries: []*simplejson.Json{
				simplejson.MustJson([]byte(`
					{
					  "datasource": {
						"type": "prometheus",
						"uid": "promds"
					  },
					  "exemplar": true,
					  "expr": "query_2_A",
					  "interval": "",
					  "legendFormat": "",
					  "refId": "A"
					}
				`)),
			},
		}, nil)
		req := server.NewPostRequest(
			"/api/public/dashboards/abc123/panels/2/query",
			strings.NewReader("{}"),
		)
		queryReturnsError = true
		resp, err := server.SendJSON(req)
		require.NoError(t, err)
		require.NoError(t, resp.Body.Close())
		require.Equal(t, http.StatusInternalServerError, resp.StatusCode)
		queryReturnsError = false
	})

	t.Run("Status code is 200 when a panel has queries from multiple datasources", func(t *testing.T) {
		server, fakeDashboardService := setup(true)

		fakeDashboardService.On(
			"BuildPublicDashboardMetricRequest",
			mock.Anything,
			"abc123",
			int64(2),
		).Return(dtos.MetricRequest{
			Queries: []*simplejson.Json{
				simplejson.MustJson([]byte(`
					{
					  "datasource": {
						"type": "prometheus",
						"uid": "promds"
					  },
					  "exemplar": true,
					  "expr": "query_2_A",
					  "interval": "",
					  "legendFormat": "",
					  "refId": "A"
					}
				`)),
				simplejson.MustJson([]byte(`
					{
					  "datasource": {
						"type": "prometheus",
						"uid": "promds2"
					  },
					  "exemplar": true,
					  "expr": "query_2_B",
					  "interval": "",
					  "legendFormat": "",
					  "refId": "B"
					}
				`)),
			},
		}, nil)
		req := server.NewPostRequest(
			"/api/public/dashboards/abc123/panels/2/query",
			strings.NewReader("{}"),
		)
		resp, err := server.SendJSON(req)
		require.NoError(t, err)
		bodyBytes, err := ioutil.ReadAll(resp.Body)
		require.NoError(t, err)
		require.JSONEq(
			t,
			`{
				"results": {
					"A": {
						"frames": [
							{
								"data": {
									"values": []
								},
								"schema": {
									"fields": [],
									"refId": "A",
									"name": "query-A"
								}
							}
						]
					},
					"B": {
						"frames": [
							{
								"data": {
									"values": []
								},
								"schema": {
									"fields": [],
									"refId": "B",
									"name": "query-B"
								}
							}
						]
					}
				}
			}`,
			string(bodyBytes),
		)
		require.NoError(t, resp.Body.Close())
		require.Equal(t, http.StatusOK, resp.StatusCode)
	})
}