package api

import (
	"net/http"
	"testing"
	"time"

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

	models2 "github.com/grafana/grafana/pkg/models"
	"github.com/grafana/grafana/pkg/services/accesscontrol"
	acMock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
	"github.com/grafana/grafana/pkg/services/datasources"
	fakes "github.com/grafana/grafana/pkg/services/datasources/fakes"
	"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
	"github.com/grafana/grafana/pkg/services/ngalert/eval"
	"github.com/grafana/grafana/pkg/services/ngalert/models"
	"github.com/grafana/grafana/pkg/web"
)

func TestRouteTestGrafanaRuleConfig(t *testing.T) {
	t.Run("when fine-grained access is enabled", func(t *testing.T) {
		rc := &models2.ReqContext{
			Context: &web.Context{
				Req: &http.Request{},
			},
			SignedInUser: &models2.SignedInUser{
				OrgId: 1,
			},
		}

		t.Run("should return 401 if user cannot query a data source", func(t *testing.T) {
			data1 := models.GenerateAlertQuery()
			data2 := models.GenerateAlertQuery()

			ac := acMock.New().WithPermissions([]*accesscontrol.Permission{
				{Action: datasources.ActionQuery, Scope: datasources.ScopeProvider.GetResourceScopeUID(data1.DatasourceUID)},
			})

			srv := createTestingApiSrv(nil, ac, nil)

			response := srv.RouteTestGrafanaRuleConfig(rc, definitions.TestRulePayload{
				Expr: "",
				GrafanaManagedCondition: &models.EvalAlertConditionCommand{
					Condition: data1.RefID,
					Data:      []models.AlertQuery{data1, data2},
					Now:       time.Time{},
				},
			})

			require.Equal(t, http.StatusUnauthorized, response.Status())
		})

		t.Run("should return 200 if user can query all data sources", func(t *testing.T) {
			data1 := models.GenerateAlertQuery()
			data2 := models.GenerateAlertQuery()

			ac := acMock.New().WithPermissions([]*accesscontrol.Permission{
				{Action: datasources.ActionQuery, Scope: datasources.ScopeProvider.GetResourceScopeUID(data1.DatasourceUID)},
				{Action: datasources.ActionQuery, Scope: datasources.ScopeProvider.GetResourceScopeUID(data2.DatasourceUID)},
			})

			ds := &fakes.FakeCacheService{DataSources: []*models2.DataSource{
				{Uid: data1.DatasourceUID},
				{Uid: data2.DatasourceUID},
			}}

			evaluator := &eval.FakeEvaluator{}
			var result []eval.Result
			evaluator.EXPECT().ConditionEval(mock.Anything, mock.Anything, mock.Anything).Return(result, nil)

			srv := createTestingApiSrv(ds, ac, evaluator)

			response := srv.RouteTestGrafanaRuleConfig(rc, definitions.TestRulePayload{
				Expr: "",
				GrafanaManagedCondition: &models.EvalAlertConditionCommand{
					Condition: data1.RefID,
					Data:      []models.AlertQuery{data1, data2},
					Now:       time.Time{},
				},
			})

			require.Equal(t, http.StatusOK, response.Status())

			evaluator.AssertCalled(t, "ConditionEval", mock.Anything, mock.Anything, mock.Anything)
		})
	})

	t.Run("when fine-grained access is disabled", func(t *testing.T) {
		rc := &models2.ReqContext{
			Context: &web.Context{
				Req: &http.Request{},
			},
			IsSignedIn: false,
			SignedInUser: &models2.SignedInUser{
				OrgId: 1,
			},
		}
		ac := acMock.New().WithDisabled()

		t.Run("should require user to be signed in", func(t *testing.T) {
			data1 := models.GenerateAlertQuery()

			ds := &fakes.FakeCacheService{DataSources: []*models2.DataSource{
				{Uid: data1.DatasourceUID},
			}}

			evaluator := &eval.FakeEvaluator{}
			var result []eval.Result
			evaluator.EXPECT().ConditionEval(mock.Anything, mock.Anything, mock.Anything).Return(result, nil)

			srv := createTestingApiSrv(ds, ac, evaluator)

			response := srv.RouteTestGrafanaRuleConfig(rc, definitions.TestRulePayload{
				Expr: "",
				GrafanaManagedCondition: &models.EvalAlertConditionCommand{
					Condition: data1.RefID,
					Data:      []models.AlertQuery{data1},
					Now:       time.Time{},
				},
			})

			require.Equal(t, http.StatusUnauthorized, response.Status())
			evaluator.AssertNotCalled(t, "ConditionEval", mock.Anything, mock.Anything, mock.Anything)

			rc.IsSignedIn = true

			response = srv.RouteTestGrafanaRuleConfig(rc, definitions.TestRulePayload{
				Expr: "",
				GrafanaManagedCondition: &models.EvalAlertConditionCommand{
					Condition: data1.RefID,
					Data:      []models.AlertQuery{data1},
					Now:       time.Time{},
				},
			})

			require.Equal(t, http.StatusOK, response.Status())

			evaluator.AssertCalled(t, "ConditionEval", mock.Anything, mock.Anything, mock.Anything)
		})
	})
}

func TestRouteEvalQueries(t *testing.T) {
	t.Run("when fine-grained access is enabled", func(t *testing.T) {
		rc := &models2.ReqContext{
			Context: &web.Context{
				Req: &http.Request{},
			},
			SignedInUser: &models2.SignedInUser{
				OrgId: 1,
			},
		}

		t.Run("should return 401 if user cannot query a data source", func(t *testing.T) {
			data1 := models.GenerateAlertQuery()
			data2 := models.GenerateAlertQuery()

			ac := acMock.New().WithPermissions([]*accesscontrol.Permission{
				{Action: datasources.ActionQuery, Scope: datasources.ScopeProvider.GetResourceScopeUID(data1.DatasourceUID)},
			})

			srv := &TestingApiSrv{
				accessControl: ac,
			}

			response := srv.RouteEvalQueries(rc, definitions.EvalQueriesPayload{
				Data: []models.AlertQuery{data1, data2},
				Now:  time.Time{},
			})

			require.Equal(t, http.StatusUnauthorized, response.Status())
		})

		t.Run("should return 200 if user can query all data sources", func(t *testing.T) {
			data1 := models.GenerateAlertQuery()
			data2 := models.GenerateAlertQuery()

			ac := acMock.New().WithPermissions([]*accesscontrol.Permission{
				{Action: datasources.ActionQuery, Scope: datasources.ScopeProvider.GetResourceScopeUID(data1.DatasourceUID)},
				{Action: datasources.ActionQuery, Scope: datasources.ScopeProvider.GetResourceScopeUID(data2.DatasourceUID)},
			})

			ds := &fakes.FakeCacheService{DataSources: []*models2.DataSource{
				{Uid: data1.DatasourceUID},
				{Uid: data2.DatasourceUID},
			}}

			evaluator := &eval.FakeEvaluator{}
			result := &backend.QueryDataResponse{
				Responses: map[string]backend.DataResponse{
					"test": {
						Frames: nil,
						Error:  nil,
					},
				},
			}
			evaluator.EXPECT().QueriesAndExpressionsEval(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(result, nil)

			srv := createTestingApiSrv(ds, ac, evaluator)

			response := srv.RouteEvalQueries(rc, definitions.EvalQueriesPayload{
				Data: []models.AlertQuery{data1, data2},
				Now:  time.Time{},
			})

			require.Equal(t, http.StatusOK, response.Status())

			evaluator.AssertCalled(t, "QueriesAndExpressionsEval", mock.Anything, mock.Anything, mock.Anything, mock.Anything)
		})
	})

	t.Run("when fine-grained access is disabled", func(t *testing.T) {
		rc := &models2.ReqContext{
			Context: &web.Context{
				Req: &http.Request{},
			},
			IsSignedIn: false,
			SignedInUser: &models2.SignedInUser{
				OrgId: 1,
			},
		}
		ac := acMock.New().WithDisabled()

		t.Run("should require user to be signed in", func(t *testing.T) {
			data1 := models.GenerateAlertQuery()

			ds := &fakes.FakeCacheService{DataSources: []*models2.DataSource{
				{Uid: data1.DatasourceUID},
			}}

			evaluator := &eval.FakeEvaluator{}
			result := &backend.QueryDataResponse{
				Responses: map[string]backend.DataResponse{
					"test": {
						Frames: nil,
						Error:  nil,
					},
				},
			}
			evaluator.EXPECT().QueriesAndExpressionsEval(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(result, nil)

			srv := createTestingApiSrv(ds, ac, evaluator)

			response := srv.RouteEvalQueries(rc, definitions.EvalQueriesPayload{
				Data: []models.AlertQuery{data1},
				Now:  time.Time{},
			})

			require.Equal(t, http.StatusUnauthorized, response.Status())
			evaluator.AssertNotCalled(t, "QueriesAndExpressionsEval", mock.Anything, mock.Anything, mock.Anything, mock.Anything)

			rc.IsSignedIn = true

			response = srv.RouteEvalQueries(rc, definitions.EvalQueriesPayload{
				Data: []models.AlertQuery{data1},
				Now:  time.Time{},
			})

			require.Equal(t, http.StatusOK, response.Status())

			evaluator.AssertCalled(t, "QueriesAndExpressionsEval", mock.Anything, mock.Anything, mock.Anything, mock.Anything)
		})
	})
}

func createTestingApiSrv(ds *fakes.FakeCacheService, ac *acMock.Mock, evaluator *eval.FakeEvaluator) *TestingApiSrv {
	if ac == nil {
		ac = acMock.New().WithDisabled()
	}

	return &TestingApiSrv{
		DatasourceCache: ds,
		accessControl:   ac,
		evaluator:       evaluator,
	}
}
