package regionalservice

import (
	"context"
	"testing"
	"time"

	promv1 "github.com/prometheus/client_golang/api/prometheus/v1"
	"github.com/prometheus/common/model"
	"github.com/stretchr/testify/assert"
	v1 "github.com/tidbcloud/observability/api/regionalserver/resourceworker/v1"
	"github.com/tidbcloud/observability/pkg/common/constants"
	"github.com/tidbcloud/observability/pkg/common/errors"
	"github.com/tidbcloud/observability/pkg/common/message"
	"github.com/tidbcloud/observability/pkg/common/structs"
	"go.uber.org/mock/gomock"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"github.com/tidbcloud/observability/pkg/resourcemanager"
	"github.com/tidbcloud/observability/test/mockresourcemanager"
)

const (
	clusterName      = "test"
	clusterNamespace = "test-ns"
)

func TestListClusterExternalAlerts(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	clusterRW := mockresourcemanager.NewMockClusterReaderWriter(ctrl)
	resourcemanager.MockClusterReaderWriter(clusterRW)

	promAPI := mockresourcemanager.NewMockPrometheusAPIClient(ctrl)
	resourcemanager.MockPrometheusAPIClient(promAPI)

	t.Run("query cluster error", func(t *testing.T) {
		clusterRW.EXPECT().
			QueryClusters(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
			Return(nil, errors.New(errors.UnrecognizedError, "")).
			Times(1)

		_, err := GetHandler().ListClusterExternalAlerts(context.TODO(), message.QueryExternalAlertsReq{})
		assert.Error(t, err)
		assert.Equal(t, errors.UnrecognizedError, err.(errors.Error).Code())
	})

	t.Run("cluster not found", func(t *testing.T) {
		clusterRW.EXPECT().
			QueryClusters(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
			Return(&v1.ClusterList{}, nil).
			Times(1)

		_, err := GetHandler().ListClusterExternalAlerts(context.TODO(), message.QueryExternalAlertsReq{})
		assert.Error(t, err)
		assert.Equal(t, errors.ClusterNotFound, err.(errors.Error).Code())
	})

	t.Run("query prom alert error", func(t *testing.T) {
		clusterRW.EXPECT().
			QueryClusters(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
			Return(&v1.ClusterList{
				Items: []v1.Cluster{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name:      clusterName,
							Namespace: clusterNamespace,
						},
					},
				},
			}, nil).
			Times(1)

		promAPI.EXPECT().
			Alerts(gomock.Any(), gomock.Any()).
			Return(promv1.AlertsResult{
				Alerts: []promv1.Alert{},
			}, errors.New(errors.UnrecognizedError, "")).
			Times(1)

		_, err := GetHandler().ListClusterExternalAlerts(context.TODO(), message.QueryExternalAlertsReq{})
		assert.Error(t, err)
		assert.Equal(t, errors.UnrecognizedError, err.(errors.Error).Code())
	})

	t.Run("succeed active alerts", func(t *testing.T) {
		clusterRW.EXPECT().
			QueryClusters(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
			Return(&v1.ClusterList{
				Items: []v1.Cluster{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name:      clusterName,
							Namespace: clusterNamespace,
						},
					},
				},
			}, nil).
			Times(1)

		promAPI.EXPECT().
			Alerts(gomock.Any(), gomock.Any()).
			Return(promv1.AlertsResult{
				Alerts: []promv1.Alert{
					promv1.Alert{
						ActiveAt: time.Now(),
						Annotations: model.LabelSet{
							constants.UserAlertMessageKey: "CPU Usage higher than 80%, is 90% now",
							constants.UserAlertNameKey:    "CPU Usage High",
						},
						Labels: model.LabelSet{
							constants.UserAlertSeverityKey: "warning",
							constants.UserAlertLabelKey:    constants.UserAlertLabelValue,
						},
						State: promv1.AlertStateFiring,
						Value: "1",
					},
				},
			}, nil).
			Times(1)

		_, err := GetHandler().ListClusterExternalAlerts(context.TODO(), message.QueryExternalAlertsReq{})
		assert.NoError(t, err)
	})

	t.Run("succeed active alerts with endtime", func(t *testing.T) {
		clusterRW.EXPECT().
			QueryClusters(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
			Return(&v1.ClusterList{
				Items: []v1.Cluster{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name:      clusterName,
							Namespace: clusterNamespace,
						},
					},
				},
			}, nil).
			Times(1)

		now := time.Now()
		promAPI.EXPECT().
			Alerts(gomock.Any(), gomock.Any()).
			Return(promv1.AlertsResult{
				Alerts: []promv1.Alert{
					promv1.Alert{
						ActiveAt: now.Add(-time.Hour),
						Annotations: model.LabelSet{
							constants.UserAlertMessageKey: "CPU Usage higher than 80%, is 90% now",
							constants.UserAlertNameKey:    "CPU Usage High",
						},
						Labels: model.LabelSet{
							constants.UserAlertSeverityKey: "warning",
							constants.UserAlertLabelKey:    constants.UserAlertLabelValue,
						},
						State: promv1.AlertStateFiring,
						Value: "1",
					},
					promv1.Alert{
						ActiveAt: time.Now().Add(time.Hour),
						Annotations: model.LabelSet{
							constants.UserAlertMessageKey: "CPU Usage higher than 80%, is 90% now",
							constants.UserAlertNameKey:    "CPU Usage High",
						},
						Labels: model.LabelSet{
							constants.UserAlertSeverityKey: "warning",
							constants.UserAlertLabelKey:    constants.UserAlertLabelValue,
						},
						State: promv1.AlertStateFiring,
						Value: "1",
					},
				},
			}, nil).
			Times(1)

		nowTs := time.Now().Unix()
		resp, err := GetHandler().ListClusterExternalAlerts(context.TODO(), message.QueryExternalAlertsReq{
			EndTime: &nowTs,
			Pagination: message.Pagination{
				Limit: 20,
				Page:  1,
			},
		})
		assert.NoError(t, err)
		assert.Equal(t, len(resp.Alerts), 1)
	})

	t.Run("query history alerts error", func(t *testing.T) {
		clusterRW.EXPECT().
			QueryClusters(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
			Return(&v1.ClusterList{
				Items: []v1.Cluster{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name:      clusterName,
							Namespace: clusterNamespace,
						},
					},
				},
			}, nil).
			Times(1)

		promAPI.EXPECT().
			QueryRange(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
			Return(nil, nil, errors.New(errors.UnrecognizedError, "")).
			Times(1)

		now := time.Now().Unix()
		_, err := GetHandler().ListClusterExternalAlerts(context.TODO(), message.QueryExternalAlertsReq{
			State:     structs.AlertStateClosed,
			StartTime: &now,
			EndTime:   &now,
		})
		assert.Error(t, err)
	})

	t.Run("query history alerts without start end time", func(t *testing.T) {
		clusterRW.EXPECT().
			QueryClusters(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
			Return(&v1.ClusterList{
				Items: []v1.Cluster{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name:      clusterName,
							Namespace: clusterNamespace,
						},
					},
				},
			}, nil).
			Times(1)

		resp, err := GetHandler().ListClusterExternalAlerts(context.TODO(), message.QueryExternalAlertsReq{
			State:     structs.AlertStateClosed,
			StartTime: nil,
			EndTime:   nil,
		})
		assert.NoError(t, err)
		assert.Equal(t, len(resp.Alerts), 0)
	})

	t.Run("query history alerts success", func(t *testing.T) {
		clusterRW.EXPECT().
			QueryClusters(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
			Return(&v1.ClusterList{
				Items: []v1.Cluster{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name:      clusterName,
							Namespace: clusterNamespace,
						},
					},
				},
			}, nil).
			Times(1)

		now := time.Now()
		activeAt1, activeAt2 := now.Add(-10*time.Minute), now.Add(-5*time.Minute)
		ts1, ts2, ts3 := activeAt2.Add(-time.Minute), activeAt2.Add(time.Minute), now.Add(-3*time.Minute)
		promAPI.EXPECT().
			QueryRange(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
			Return(model.Matrix([]*model.SampleStream{
				&model.SampleStream{
					Metric: model.Metric{
						constants.UserAlertSeverityKey:    "warning",
						constants.UserAlertRuleIDKey:      "test",
						constants.UserAlertDescriptionKey: "test",
					},
					Values: []model.SamplePair{
						model.SamplePair{
							Timestamp: model.TimeFromUnix(activeAt1.Unix()),
							Value:     model.SampleValue(activeAt1.Unix()),
						},
						model.SamplePair{
							Timestamp: model.TimeFromUnix(ts1.Unix()),
							Value:     model.SampleValue(activeAt1.Unix()),
						},
						model.SamplePair{
							Timestamp: model.TimeFromUnix(activeAt2.Unix()),
							Value:     model.SampleValue(activeAt2.Unix()),
						},
						model.SamplePair{
							Timestamp: model.TimeFromUnix(ts2.Unix()),
							Value:     model.SampleValue(activeAt2.Unix()),
						},
						model.SamplePair{
							Timestamp: model.TimeFromUnix(ts3.Unix()),
							Value:     model.SampleValue(activeAt2.Unix()),
						},
					},
				},
				&model.SampleStream{
					Metric: model.Metric{
						constants.UserAlertSeverityKey: "critical",
						"alertname":                    "test",
					},
					Values: []model.SamplePair{
						model.SamplePair{
							Timestamp: model.TimeFromUnix(activeAt1.Unix()),
							Value:     model.SampleValue(activeAt1.Unix()),
						},
						model.SamplePair{
							Timestamp: model.TimeFromUnix(now.Unix()),
							Value:     model.SampleValue(activeAt1.Unix()),
						},
					},
				},
			}), nil, nil).
			Times(1)

		nowTs := now.Unix()
		resp, err := GetHandler().ListClusterExternalAlerts(context.TODO(), message.QueryExternalAlertsReq{
			State:     structs.AlertStateClosed,
			StartTime: &nowTs,
			EndTime:   &nowTs,
			Pagination: message.Pagination{
				Limit: 20,
				Page:  1,
			},
		})
		assert.NoError(t, err)
		assert.Equal(t, len(resp.Alerts), 2)
	})

	t.Run("query active and history alerts error", func(t *testing.T) {
		clusterRW.EXPECT().
			QueryClusters(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
			Return(&v1.ClusterList{
				Items: []v1.Cluster{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name:      clusterName,
							Namespace: clusterNamespace,
						},
					},
				},
			}, nil).
			Times(1)

		promAPI.EXPECT().
			Alerts(gomock.Any(), gomock.Any()).
			Return(promv1.AlertsResult{
				Alerts: []promv1.Alert{
					promv1.Alert{
						ActiveAt: time.Now(),
						Annotations: model.LabelSet{
							constants.UserAlertMessageKey: "CPU Usage higher than 80%, is 90% now",
							constants.UserAlertNameKey:    "CPU Usage High",
						},
						Labels: model.LabelSet{
							constants.UserAlertSeverityKey: "warning",
						},
						State: promv1.AlertStateFiring,
						Value: "1",
					},
				},
			}, nil).
			Times(1)
		promAPI.EXPECT().
			QueryRange(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
			Return(nil, nil, errors.New(errors.UnrecognizedError, "")).
			Times(1)

		now := time.Now().Unix()
		_, err := GetHandler().ListClusterExternalAlerts(context.TODO(), message.QueryExternalAlertsReq{
			State:     structs.AlertStateAll,
			StartTime: &now,
			EndTime:   &now,
		})
		assert.Error(t, err)
	})

	t.Run("query active and history alerts error", func(t *testing.T) {
		clusterRW.EXPECT().
			QueryClusters(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
			Return(&v1.ClusterList{
				Items: []v1.Cluster{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name:      clusterName,
							Namespace: clusterNamespace,
						},
					},
				},
			}, nil).
			Times(1)

		promAPI.EXPECT().
			Alerts(gomock.Any(), gomock.Any()).
			Return(promv1.AlertsResult{
				Alerts: []promv1.Alert{
					promv1.Alert{
						ActiveAt: time.Now(),
						Annotations: model.LabelSet{
							constants.UserAlertMessageKey: "CPU Usage higher than 80%, is 90% now",
							constants.UserAlertNameKey:    "CPU Usage High",
						},
						Labels: model.LabelSet{
							constants.UserAlertSeverityKey: "warning",
						},
						State: promv1.AlertStateFiring,
						Value: "1",
					},
				},
			}, nil).
			Times(1)
		promAPI.EXPECT().
			QueryRange(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
			Return(nil, nil, errors.New(errors.UnrecognizedError, "")).
			Times(1)

		now := time.Now().Unix()
		_, err := GetHandler().ListClusterExternalAlerts(context.TODO(), message.QueryExternalAlertsReq{
			State:     structs.AlertStateAll,
			StartTime: &now,
			EndTime:   &now,
		})
		assert.Error(t, err)
	})

	t.Run("severity mapping warning -> warning & major", func(t *testing.T) {
		clusterRW.EXPECT().
			QueryClusters(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
			Return(&v1.ClusterList{
				Items: []v1.Cluster{
					{
						ObjectMeta: metav1.ObjectMeta{
							Name:      clusterName,
							Namespace: clusterNamespace,
						},
					},
				},
			}, nil).
			Times(2)

		promAPI.EXPECT().
			Alerts(gomock.Any(), gomock.Any()).
			Return(promv1.AlertsResult{
				Alerts: []promv1.Alert{
					promv1.Alert{
						ActiveAt: time.Now(),
						Annotations: model.LabelSet{
							constants.UserAlertMessageKey: "CPU Usage higher than 80%, is 90% now",
							constants.UserAlertNameKey:    "CPU Usage High",
						},
						Labels: model.LabelSet{
							constants.UserAlertSeverityKey: "warning",
							constants.UserAlertLabelKey:    constants.UserAlertLabelValue,
						},
						State: promv1.AlertStateFiring,
						Value: "1",
					},
					promv1.Alert{
						ActiveAt: time.Now(),
						Annotations: model.LabelSet{
							constants.UserAlertMessageKey: "CPU Usage higher than 80%, is 90% now",
							constants.UserAlertNameKey:    "CPU Usage High",
						},
						Labels: model.LabelSet{
							constants.UserAlertSeverityKey: "major",
							constants.UserAlertLabelKey:    constants.UserAlertLabelValue,
						},
						State: promv1.AlertStateFiring,
						Value: "1",
					},
					promv1.Alert{
						ActiveAt: time.Now(),
						Annotations: model.LabelSet{
							constants.UserAlertMessageKey: "CPU Usage higher than 80%, is 90% now",
							constants.UserAlertNameKey:    "CPU Usage High",
						},
						Labels: model.LabelSet{
							constants.UserAlertSeverityKey: "critical",
							constants.UserAlertLabelKey:    constants.UserAlertLabelValue,
						},
						State: promv1.AlertStateFiring,
						Value: "1",
					},
				},
			}, nil).
			Times(2)
		resp, err := GetHandler().ListClusterExternalAlerts(context.TODO(), message.QueryExternalAlertsReq{
			State: structs.AlertStateAll,
			Pagination: message.Pagination{
				Limit: 20,
				Page:  1,
			},
			Severity: "warning",
		})
		assert.NoError(t, err)
		assert.Equal(t, len(resp.Alerts), 2)

		resp, err = GetHandler().ListClusterExternalAlerts(context.TODO(), message.QueryExternalAlertsReq{
			State: structs.AlertStateAll,
			Pagination: message.Pagination{
				Limit: 20,
				Page:  1,
			},
			Severity: "critical",
		})
		assert.NoError(t, err)
		assert.Equal(t, len(resp.Alerts), 1)
	})
}
