package state

import (
	"context"
	"errors"
	"fmt"
	"math/rand"
	"testing"
	"time"

	"github.com/grafana/grafana-plugin-sdk-go/data"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"k8s.io/component-base/tracing"

	"github.com/grafana/grafana/pkg/infra/log/logtest"
	"github.com/grafana/grafana/pkg/services/ngalert/eval"
	ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
	"github.com/grafana/grafana/pkg/util"
)

func TestSyncPersister_saveAlertStates(t *testing.T) {
	type stateWithReason struct {
		State  eval.State
		Reason string
	}
	create := func(s eval.State, r string) stateWithReason {
		return stateWithReason{
			State:  s,
			Reason: r,
		}
	}
	allStates := [...]stateWithReason{
		create(eval.Normal, ""),
		create(eval.Normal, eval.NoData.String()),
		create(eval.Normal, eval.Error.String()),
		create(eval.Normal, util.GenerateShortUID()),
		create(eval.Alerting, ""),
		create(eval.Pending, ""),
		create(eval.NoData, ""),
		create(eval.Error, ""),
	}
	ruleKey := ngmodels.AlertRuleKeyWithGroup{}

	transitionToKey := map[ngmodels.AlertInstanceKey]StateTransition{}
	transitions := make([]StateTransition, 0)
	for _, fromState := range allStates {
		for i, toState := range allStates {
			tr := StateTransition{
				State: &State{
					State:       toState.State,
					StateReason: toState.Reason,
					Labels:      ngmodels.GenerateAlertLabels(5, fmt.Sprintf("%d--", i)),
				},
				PreviousState:       fromState.State,
				PreviousStateReason: fromState.Reason,
			}
			key, err := tr.GetAlertInstanceKey()
			require.NoError(t, err)
			transitionToKey[key] = tr
			transitions = append(transitions, tr)
		}
	}

	t.Run("should save all transitions if doNotSaveNormalState is false", func(t *testing.T) {
		trace := tracing.NewNoopTracerProvider().Tracer("test")
		_, span := trace.Start(context.Background(), "")
		st := &FakeInstanceStore{}
		syncStatePersister := NewSyncStatePersisiter(&logtest.Fake{}, ManagerCfg{
			InstanceStore:           st,
			MaxStateSaveConcurrency: 1,
		})
		syncStatePersister.Sync(context.Background(), span, ruleKey, transitions)
		savedKeys := map[ngmodels.AlertInstanceKey]ngmodels.AlertInstance{}
		for _, op := range st.RecordedOps() {
			saved := op.(ngmodels.AlertInstance)
			savedKeys[saved.AlertInstanceKey] = saved
		}
		assert.Len(t, transitionToKey, len(savedKeys))

		for key, tr := range transitionToKey {
			assert.Containsf(t, savedKeys, key, "state %s (%s) was not saved but should be", tr.State.State, tr.StateReason)
		}
	})

	t.Run("should not save Normal->Normal if doNotSaveNormalState is true", func(t *testing.T) {
		trace := tracing.NewNoopTracerProvider().Tracer("test")
		_, span := trace.Start(context.Background(), "")
		st := &FakeInstanceStore{}
		syncStatePersister := NewSyncStatePersisiter(&logtest.Fake{}, ManagerCfg{
			InstanceStore:           st,
			MaxStateSaveConcurrency: 1,
		})
		syncStatePersister.Sync(context.Background(), span, ruleKey, transitions)

		savedKeys := map[ngmodels.AlertInstanceKey]ngmodels.AlertInstance{}
		for _, op := range st.RecordedOps() {
			saved := op.(ngmodels.AlertInstance)
			savedKeys[saved.AlertInstanceKey] = saved
		}
		for key, tr := range transitionToKey {
			if tr.State.State == eval.Normal && tr.StateReason == "" && tr.PreviousState == eval.Normal && tr.PreviousStateReason == "" {
				continue
			}
			assert.Containsf(t, savedKeys, key, "state %s (%s) was not saved but should be", tr.State.State, tr.StateReason)
		}
	})

	t.Run("should save expected fields", func(t *testing.T) {
		trace := tracing.NewNoopTracerProvider().Tracer("test")
		_, span := trace.Start(context.Background(), "")
		st := &FakeInstanceStore{}
		syncStatePersister := NewSyncStatePersisiter(&logtest.Fake{}, ManagerCfg{
			InstanceStore:           st,
			MaxStateSaveConcurrency: 1,
		})

		state := &State{
			OrgID:             rand.Int63(),
			AlertRuleUID:      util.GenerateShortUID(),
			CacheID:           data.Fingerprint(rand.Int63()),
			State:             eval.Alerting,
			StateReason:       "TEST",
			ResultFingerprint: data.Fingerprint(rand.Int63()),
			LatestResult: &Evaluation{
				EvaluationTime:  time.Now().Add(1 * time.Minute),
				EvaluationState: eval.Alerting,
				Values: map[string]float64{
					"A": 1.0,
					"B": 2.0,
				},
				Condition: "A",
			},
			Error: errors.New("test"),
			Image: &ngmodels.Image{
				ID:        rand.Int63(),
				Token:     util.GenerateShortUID(),
				Path:      util.GenerateShortUID(),
				URL:       util.GenerateShortUID(),
				CreatedAt: time.Now().Add(2 * time.Minute),
				ExpiresAt: time.Now().Add(3 * time.Minute),
			},
			Annotations: ngmodels.GenerateAlertLabels(4, "annotations_"),
			Labels:      ngmodels.GenerateAlertLabels(4, "labels_"),
			Values: map[string]float64{
				"A1": 11.0,
				"B1": 12.0,
			},
			StartsAt:             time.Now().Add(4 * time.Minute),
			EndsAt:               time.Now().Add(5 * time.Minute),
			ResolvedAt:           util.Pointer(time.Now().Add(6 * time.Minute)),
			LastSentAt:           util.Pointer(time.Now().Add(7 * time.Minute)),
			LastEvaluationString: util.GenerateShortUID(),
			LastEvaluationTime:   time.Now().Add(8 * time.Minute),
			EvaluationDuration:   time.Duration(rand.Intn(100)+1) * time.Second,
		}

		transition := StateTransition{
			State:               state,
			PreviousState:       eval.Normal,
			PreviousStateReason: util.GenerateShortUID(),
		}

		syncStatePersister.Sync(context.Background(), span, ruleKey, []StateTransition{transition})

		require.Len(t, st.RecordedOps(), 1)
		saved := st.RecordedOps()[0].(ngmodels.AlertInstance)

		expectedAlertInstanceKey, err := state.GetAlertInstanceKey()
		require.NoError(t, err)
		assert.Equal(t, expectedAlertInstanceKey, saved.AlertInstanceKey)
		assert.Equal(t, ngmodels.InstanceLabels(state.Labels), saved.Labels)
		assert.EqualValues(t, ngmodels.InstanceStateType(state.State.String()), saved.CurrentState)
		assert.Equal(t, state.StateReason, saved.CurrentReason)
		assert.Equal(t, state.StartsAt, saved.CurrentStateSince)
		assert.Equal(t, state.EndsAt, saved.CurrentStateEnd)
		assert.Equal(t, state.LastEvaluationTime, saved.LastEvalTime)
		assert.Equal(t, state.LastSentAt, saved.LastSentAt)
		assert.EqualValues(t, state.ResolvedAt, saved.ResolvedAt)
		assert.Equal(t, state.ResultFingerprint.String(), saved.ResultFingerprint)
	})
}
