package alerting

import (
	"context"
	"testing"
	"time"

	"github.com/grafana/grafana/pkg/components/simplejson"
	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/models"
	"github.com/grafana/grafana/pkg/services/dashboards"
	dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
	"github.com/grafana/grafana/pkg/services/org"
	"github.com/grafana/grafana/pkg/services/sqlstore"
	"github.com/grafana/grafana/pkg/services/sqlstore/db"
	"github.com/grafana/grafana/pkg/services/user"
	"github.com/grafana/grafana/pkg/setting"
	"github.com/grafana/grafana/pkg/util"

	"github.com/stretchr/testify/require"
)

func mockTimeNow() {
	var timeSeed int64
	timeNow = func() time.Time {
		loc := time.FixedZone("MockZoneUTC-5", -5*60*60)
		fakeNow := time.Unix(timeSeed, 0).In(loc)
		timeSeed++
		return fakeNow
	}
}

func resetTimeNow() {
	timeNow = time.Now
}

func TestIntegrationAlertingDataAccess(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test")
	}
	mockTimeNow()
	defer resetTimeNow()

	var store *sqlStore
	var testDash *models.Dashboard
	var items []*models.Alert

	setup := func(t *testing.T) {
		cfg := setting.NewCfg()
		cfg.RBACEnabled = false
		store = &sqlStore{
			db:  sqlstore.InitTestDB(t),
			log: log.New(),
			cfg: cfg,
		}

		testDash = insertTestDashboard(t, store.db, "dashboard with alerts", 1, 0, false, "alert")
		evalData, err := simplejson.NewJson([]byte(`{"test": "test"}`))
		require.Nil(t, err)
		items = []*models.Alert{
			{
				PanelId:     1,
				DashboardId: testDash.Id,
				OrgId:       testDash.OrgId,
				Name:        "Alerting title",
				Message:     "Alerting message",
				Settings:    simplejson.New(),
				Frequency:   1,
				EvalData:    evalData,
			},
		}

		err = store.SaveAlerts(context.Background(), testDash.Id, items)
		require.Nil(t, err)
	}

	t.Run("Can set new states", func(t *testing.T) {
		setup(t)

		// Get alert so we can use its ID in tests
		alertQuery := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: &user.SignedInUser{OrgRole: org.RoleAdmin}}
		err2 := store.HandleAlertsQuery(context.Background(), &alertQuery)
		require.Nil(t, err2)

		insertedAlert := alertQuery.Result[0]

		t.Run("new state ok", func(t *testing.T) {
			cmd := &models.SetAlertStateCommand{
				AlertId: insertedAlert.Id,
				State:   models.AlertStateOK,
			}

			err := store.SetAlertState(context.Background(), cmd)
			require.Nil(t, err)
		})

		alert, _ := getAlertById(t, insertedAlert.Id, store)
		stateDateBeforePause := alert.NewStateDate

		t.Run("can pause all alerts", func(t *testing.T) {
			err := store.pauseAllAlerts(t, true)
			require.Nil(t, err)

			t.Run("cannot updated paused alert", func(t *testing.T) {
				cmd := &models.SetAlertStateCommand{
					AlertId: insertedAlert.Id,
					State:   models.AlertStateOK,
				}

				err = store.SetAlertState(context.Background(), cmd)
				require.Error(t, err)
			})

			t.Run("alert is paused", func(t *testing.T) {
				alert, _ = getAlertById(t, insertedAlert.Id, store)
				currentState := alert.State
				require.Equal(t, models.AlertStatePaused, currentState)
			})

			t.Run("pausing alerts should update their NewStateDate", func(t *testing.T) {
				alert, _ = getAlertById(t, insertedAlert.Id, store)
				stateDateAfterPause := alert.NewStateDate
				require.True(t, stateDateBeforePause.Before(stateDateAfterPause))
			})

			t.Run("unpausing alerts should update their NewStateDate again", func(t *testing.T) {
				err := store.pauseAllAlerts(t, false)
				require.Nil(t, err)
				alert, _ = getAlertById(t, insertedAlert.Id, store)
				stateDateAfterUnpause := alert.NewStateDate
				require.True(t, stateDateBeforePause.Before(stateDateAfterUnpause))
			})
		})
	})

	t.Run("Can read properties", func(t *testing.T) {
		setup(t)
		alertQuery := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: &user.SignedInUser{OrgRole: org.RoleAdmin}}
		err2 := store.HandleAlertsQuery(context.Background(), &alertQuery)

		alert := alertQuery.Result[0]
		require.Nil(t, err2)
		require.Greater(t, alert.Id, int64(0))
		require.Equal(t, testDash.Id, alert.DashboardId)
		require.Equal(t, int64(1), alert.PanelId)
		require.Equal(t, "Alerting title", alert.Name)
		require.Equal(t, models.AlertStateUnknown, alert.State)
		require.NotNil(t, alert.NewStateDate)
		require.NotNil(t, alert.EvalData)
		require.Equal(t, "test", alert.EvalData.Get("test").MustString())
		require.NotNil(t, alert.EvalDate)
		require.Equal(t, "", alert.ExecutionError)
		require.NotNil(t, alert.DashboardUid)
		require.Equal(t, "dashboard-with-alerts", alert.DashboardSlug)
	})

	t.Run("Viewer can read alerts", func(t *testing.T) {
		setup(t)
		viewerUser := &user.SignedInUser{OrgRole: org.RoleViewer, OrgID: 1}
		alertQuery := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: viewerUser}
		err2 := store.HandleAlertsQuery(context.Background(), &alertQuery)

		require.Nil(t, err2)
		require.Equal(t, 1, len(alertQuery.Result))
	})

	t.Run("Alerts with same dashboard id and panel id should update", func(t *testing.T) {
		setup(t)
		modifiedItems := items
		modifiedItems[0].Name = "Name"

		err := store.SaveAlerts(context.Background(), testDash.Id, items)

		t.Run("Can save alerts with same dashboard and panel id", func(t *testing.T) {
			require.Nil(t, err)
		})

		t.Run("Alerts should be updated", func(t *testing.T) {
			query := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &user.SignedInUser{OrgRole: org.RoleAdmin}}
			err2 := store.HandleAlertsQuery(context.Background(), &query)

			require.Nil(t, err2)
			require.Equal(t, 1, len(query.Result))
			require.Equal(t, "Name", query.Result[0].Name)

			t.Run("Alert state should not be updated", func(t *testing.T) {
				require.Equal(t, models.AlertStateUnknown, query.Result[0].State)
			})
		})

		t.Run("Updates without changes should be ignored", func(t *testing.T) {
			err3 := store.SaveAlerts(context.Background(), testDash.Id, items)
			require.Nil(t, err3)
		})
	})

	t.Run("Multiple alerts per dashboard", func(t *testing.T) {
		setup(t)
		multipleItems := []*models.Alert{
			{
				DashboardId: testDash.Id,
				PanelId:     1,
				Name:        "1",
				OrgId:       1,
				Settings:    simplejson.New(),
			},
			{
				DashboardId: testDash.Id,
				PanelId:     2,
				Name:        "2",
				OrgId:       1,
				Settings:    simplejson.New(),
			},
			{
				DashboardId: testDash.Id,
				PanelId:     3,
				Name:        "3",
				OrgId:       1,
				Settings:    simplejson.New(),
			},
		}

		err := store.SaveAlerts(context.Background(), testDash.Id, multipleItems)

		t.Run("Should save 3 dashboards", func(t *testing.T) {
			require.Nil(t, err)

			queryForDashboard := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &user.SignedInUser{OrgRole: org.RoleAdmin}}
			err2 := store.HandleAlertsQuery(context.Background(), &queryForDashboard)

			require.Nil(t, err2)
			require.Equal(t, 3, len(queryForDashboard.Result))
		})

		t.Run("should updated two dashboards and delete one", func(t *testing.T) {
			missingOneAlert := multipleItems[:2]

			err = store.SaveAlerts(context.Background(), testDash.Id, missingOneAlert)

			t.Run("should delete the missing alert", func(t *testing.T) {
				query := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &user.SignedInUser{OrgRole: org.RoleAdmin}}
				err2 := store.HandleAlertsQuery(context.Background(), &query)
				require.Nil(t, err2)
				require.Equal(t, 2, len(query.Result))
			})
		})
	})

	t.Run("When dashboard is removed", func(t *testing.T) {
		setup(t)
		items := []*models.Alert{
			{
				PanelId:     1,
				DashboardId: testDash.Id,
				Name:        "Alerting title",
				Message:     "Alerting message",
			},
		}

		err := store.SaveAlerts(context.Background(), testDash.Id, items)
		require.Nil(t, err)

		err = store.db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
			dash := models.Dashboard{Id: testDash.Id, OrgId: 1}
			_, err := sess.Delete(dash)
			return err
		})
		require.Nil(t, err)

		t.Run("Alerts should be removed", func(t *testing.T) {
			query := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &user.SignedInUser{OrgRole: org.RoleAdmin}}
			err2 := store.HandleAlertsQuery(context.Background(), &query)

			require.Nil(t, err2)
			require.Equal(t, 0, len(query.Result))
		})
	})
}

func TestIntegrationPausingAlerts(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test")
	}
	mockTimeNow()
	defer resetTimeNow()

	t.Run("Given an alert", func(t *testing.T) {
		sqlStore := sqlStore{db: sqlstore.InitTestDB(t), log: log.New()}

		testDash := insertTestDashboard(t, sqlStore.db, "dashboard with alerts", 1, 0, false, "alert")
		alert, err := insertTestAlert("Alerting title", "Alerting message", testDash.OrgId, testDash.Id, simplejson.New(), sqlStore)
		require.Nil(t, err)

		stateDateBeforePause := alert.NewStateDate
		stateDateAfterPause := stateDateBeforePause

		// Get alert so we can use its ID in tests
		alertQuery := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: &user.SignedInUser{OrgRole: org.RoleAdmin}}
		err2 := sqlStore.HandleAlertsQuery(context.Background(), &alertQuery)
		require.Nil(t, err2)

		insertedAlert := alertQuery.Result[0]

		t.Run("when paused", func(t *testing.T) {
			_, err := sqlStore.pauseAlert(t, testDash.OrgId, insertedAlert.Id, true)
			require.Nil(t, err)

			t.Run("the NewStateDate should be updated", func(t *testing.T) {
				alert, err := getAlertById(t, insertedAlert.Id, &sqlStore)
				require.Nil(t, err)

				stateDateAfterPause = alert.NewStateDate
				require.True(t, stateDateBeforePause.Before(stateDateAfterPause))
			})
		})

		t.Run("when unpaused", func(t *testing.T) {
			_, err := sqlStore.pauseAlert(t, testDash.OrgId, insertedAlert.Id, false)
			require.Nil(t, err)

			t.Run("the NewStateDate should be updated again", func(t *testing.T) {
				alert, err := getAlertById(t, insertedAlert.Id, &sqlStore)
				require.Nil(t, err)

				stateDateAfterUnpause := alert.NewStateDate
				require.True(t, stateDateAfterPause.Before(stateDateAfterUnpause))
			})
		})
	})
}

func (ss *sqlStore) pauseAlert(t *testing.T, orgId int64, alertId int64, pauseState bool) (int64, error) {
	cmd := &models.PauseAlertCommand{
		OrgId:    orgId,
		AlertIds: []int64{alertId},
		Paused:   pauseState,
	}
	err := ss.PauseAlert(context.Background(), cmd)
	require.Nil(t, err)
	return cmd.ResultCount, err
}

func insertTestAlert(title string, message string, orgId int64, dashId int64, settings *simplejson.Json, ss sqlStore) (*models.Alert, error) {
	items := []*models.Alert{
		{
			PanelId:     1,
			DashboardId: dashId,
			OrgId:       orgId,
			Name:        title,
			Message:     message,
			Settings:    settings,
			Frequency:   1,
		},
	}

	err := ss.SaveAlerts(context.Background(), dashId, items)
	return items[0], err
}

func getAlertById(t *testing.T, id int64, ss *sqlStore) (*models.Alert, error) {
	q := &models.GetAlertByIdQuery{
		Id: id,
	}
	err := ss.GetAlertById(context.Background(), q)
	require.Nil(t, err)
	return q.Result, err
}

func (ss *sqlStore) pauseAllAlerts(t *testing.T, pauseState bool) error {
	cmd := &models.PauseAllAlertCommand{
		Paused: pauseState,
	}
	err := ss.PauseAllAlerts(context.Background(), cmd)
	require.Nil(t, err)
	return err
}

func insertTestDashboard(t *testing.T, db db.DB, title string, orgId int64,
	folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard {
	t.Helper()
	cmd := models.SaveDashboardCommand{
		OrgId:    orgId,
		FolderId: folderId,
		IsFolder: isFolder,
		Dashboard: simplejson.NewFromAny(map[string]interface{}{
			"id":    nil,
			"title": title,
			"tags":  tags,
		}),
	}

	var dash *models.Dashboard
	err := db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
		dash = cmd.GetDashboardModel()
		dash.SetVersion(1)
		dash.Created = time.Now()
		dash.Updated = time.Now()
		dash.Uid = util.GenerateShortUID()
		_, err := sess.Insert(dash)
		return err
	})

	require.NoError(t, err)
	require.NotNil(t, dash)
	dash.Data.Set("id", dash.Id)
	dash.Data.Set("uid", dash.Uid)

	err = db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
		dashVersion := &dashver.DashboardVersion{
			DashboardID:   dash.Id,
			ParentVersion: dash.Version,
			RestoredFrom:  cmd.RestoredFrom,
			Version:       dash.Version,
			Created:       time.Now(),
			CreatedBy:     dash.UpdatedBy,
			Message:       cmd.Message,
			Data:          dash.Data,
		}
		require.NoError(t, err)

		if affectedRows, err := sess.Insert(dashVersion); err != nil {
			return err
		} else if affectedRows == 0 {
			return dashboards.ErrDashboardNotFound
		}

		return nil
	})
	require.NoError(t, err)

	return dash
}
