package alerting

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"

	"github.com/grafana/grafana/pkg/models"
)

func TestStateIsUpdatedWhenNeeded(t *testing.T) {
	ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}})

	t.Run("ok -> alerting", func(t *testing.T) {
		ctx.PrevAlertState = models.AlertStateOK
		ctx.Rule.State = models.AlertStateAlerting

		if !ctx.shouldUpdateAlertState() {
			t.Fatalf("expected should updated to be true")
		}
	})

	t.Run("ok -> ok", func(t *testing.T) {
		ctx.PrevAlertState = models.AlertStateOK
		ctx.Rule.State = models.AlertStateOK

		if ctx.shouldUpdateAlertState() {
			t.Fatalf("expected should updated to be false")
		}
	})
}

func TestGetStateFromEvalContext(t *testing.T) {
	tcs := []struct {
		name     string
		expected models.AlertStateType
		applyFn  func(ec *EvalContext)
	}{
		{
			name:     "ok -> alerting",
			expected: models.AlertStateAlerting,
			applyFn: func(ec *EvalContext) {
				ec.Firing = true
				ec.PrevAlertState = models.AlertStateOK
			},
		},
		{
			name:     "ok -> error(alerting)",
			expected: models.AlertStateAlerting,
			applyFn: func(ec *EvalContext) {
				ec.PrevAlertState = models.AlertStateOK
				ec.Error = errors.New("test error")
				ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
			},
		},
		{
			name:     "ok -> pending. since its been firing for less than FOR",
			expected: models.AlertStatePending,
			applyFn: func(ec *EvalContext) {
				ec.PrevAlertState = models.AlertStateOK
				ec.Firing = true
				ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2)
				ec.Rule.For = time.Minute * 5
			},
		},
		{
			name:     "ok -> pending. since it has to be pending longer than FOR and prev state is ok",
			expected: models.AlertStatePending,
			applyFn: func(ec *EvalContext) {
				ec.PrevAlertState = models.AlertStateOK
				ec.Firing = true
				ec.Rule.LastStateChange = time.Now().Add(-(time.Hour * 5))
				ec.Rule.For = time.Minute * 2
			},
		},
		{
			name:     "pending -> alerting. since its been firing for more than FOR and prev state is pending",
			expected: models.AlertStateAlerting,
			applyFn: func(ec *EvalContext) {
				ec.PrevAlertState = models.AlertStatePending
				ec.Firing = true
				ec.Rule.LastStateChange = time.Now().Add(-(time.Hour * 5))
				ec.Rule.For = time.Minute * 2
			},
		},
		{
			name:     "alerting -> alerting. should not update regardless of FOR",
			expected: models.AlertStateAlerting,
			applyFn: func(ec *EvalContext) {
				ec.PrevAlertState = models.AlertStateAlerting
				ec.Firing = true
				ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
				ec.Rule.For = time.Minute * 2
			},
		},
		{
			name:     "ok -> ok. should not update regardless of FOR",
			expected: models.AlertStateOK,
			applyFn: func(ec *EvalContext) {
				ec.PrevAlertState = models.AlertStateOK
				ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
				ec.Rule.For = time.Minute * 2
			},
		},
		{
			name:     "ok -> error(keep_last)",
			expected: models.AlertStateOK,
			applyFn: func(ec *EvalContext) {
				ec.PrevAlertState = models.AlertStateOK
				ec.Error = errors.New("test error")
				ec.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
			},
		},
		{
			name:     "pending -> error(keep_last)",
			expected: models.AlertStatePending,
			applyFn: func(ec *EvalContext) {
				ec.PrevAlertState = models.AlertStatePending
				ec.Error = errors.New("test error")
				ec.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
			},
		},
		{
			name:     "ok -> no_data(alerting)",
			expected: models.AlertStateAlerting,
			applyFn: func(ec *EvalContext) {
				ec.PrevAlertState = models.AlertStateOK
				ec.Rule.NoDataState = models.NoDataSetAlerting
				ec.NoDataFound = true
			},
		},
		{
			name:     "ok -> no_data(keep_last)",
			expected: models.AlertStateOK,
			applyFn: func(ec *EvalContext) {
				ec.PrevAlertState = models.AlertStateOK
				ec.Rule.NoDataState = models.NoDataKeepState
				ec.NoDataFound = true
			},
		},
		{
			name:     "pending -> no_data(keep_last)",
			expected: models.AlertStatePending,
			applyFn: func(ec *EvalContext) {
				ec.PrevAlertState = models.AlertStatePending
				ec.Rule.NoDataState = models.NoDataKeepState
				ec.NoDataFound = true
			},
		},
		{
			name:     "pending -> no_data(alerting) with for duration have not passed",
			expected: models.AlertStatePending,
			applyFn: func(ec *EvalContext) {
				ec.PrevAlertState = models.AlertStatePending
				ec.Rule.NoDataState = models.NoDataSetAlerting
				ec.NoDataFound = true
				ec.Rule.For = time.Minute * 5
				ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2)
			},
		},
		{
			name:     "pending -> no_data(alerting) should set alerting since time passed FOR",
			expected: models.AlertStateAlerting,
			applyFn: func(ec *EvalContext) {
				ec.PrevAlertState = models.AlertStatePending
				ec.Rule.NoDataState = models.NoDataSetAlerting
				ec.NoDataFound = true
				ec.Rule.For = time.Minute * 2
				ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
			},
		},
		{
			name:     "pending -> error(alerting) with for duration have not passed ",
			expected: models.AlertStatePending,
			applyFn: func(ec *EvalContext) {
				ec.PrevAlertState = models.AlertStatePending
				ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
				ec.Error = errors.New("test error")
				ec.Rule.For = time.Minute * 5
				ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2)
			},
		},
		{
			name:     "pending -> error(alerting) should set alerting since time passed FOR",
			expected: models.AlertStateAlerting,
			applyFn: func(ec *EvalContext) {
				ec.PrevAlertState = models.AlertStatePending
				ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
				ec.Error = errors.New("test error")
				ec.Rule.For = time.Minute * 2
				ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
			},
		},
	}

	for _, tc := range tcs {
		evalContext := NewEvalContext(context.Background(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}})

		tc.applyFn(evalContext)
		newState := evalContext.GetNewState()
		assert.Equal(t, tc.expected, newState, "failed: %s \n expected '%s' have '%s'\n", tc.name, tc.expected, string(newState))
	}
}
