package models_test

import (
	"slices"
	"testing"
	"time"

	"github.com/prometheus/prometheus/model/labels"

	"github.com/prymitive/karma/internal/models"
)

type alertListSortTest struct {
	alert    models.Alert
	position int
}

var alertListSortTests = []alertListSortTest{
	{
		alert: models.Alert{
			Receiver: "default",
			State:    models.AlertStateActive,
			StartsAt: time.Date(2017, time.January, 10, 0, 0, 0, 5, time.UTC),
		},
		position: 0,
	},
	{
		alert: models.Alert{
			Receiver: "default",
			State:    models.AlertStateActive,
			StartsAt: time.Date(2017, time.January, 10, 0, 0, 0, 1, time.UTC),
		},
		position: 1,
	},
	{
		alert: models.Alert{
			Receiver: "default",
			State:    models.AlertStateActive,
			StartsAt: time.Date(2017, time.January, 10, 0, 0, 0, 0, time.UTC),
		},
		position: 2,
	},
	{
		alert: models.Alert{
			Receiver: "default",
			State:    models.AlertStateActive,
			StartsAt: time.Date(2015, time.March, 10, 0, 0, 0, 0, time.UTC),
		},
		position: 6,
	},
	{
		alert: models.Alert{
			Receiver: "default",
			State:    models.AlertStateActive,
			StartsAt: time.Date(2016, time.December, 10, 0, 0, 0, 0, time.UTC),
		},
		position: 4,
	},
	{
		alert: models.Alert{
			Receiver: "default",
			State:    models.AlertStateActive, StartsAt: time.Date(2017, time.January, 10, 0, 0, 0, 0, time.UTC),
		},

		position: 3,
	},
	{
		alert: models.Alert{
			Receiver: "default",
			State:    models.AlertStateActive,
			StartsAt: time.Date(2015, time.March, 10, 0, 0, 0, 0, time.UTC),
		},
		position: 5,
	},
}

func TestAlertListSort(t *testing.T) {
	al := make(models.AlertList, 0, len(alertListSortTests))
	for _, testCase := range alertListSortTests {
		testCase.alert.UpdateFingerprints()
		al = append(al, testCase.alert)
	}

	// repeat sort 100 times to ensure we're always sorting same way
	iterations := 100
	failures := 0
	for i := 1; i <= iterations; i++ {
		slices.SortFunc(al, models.CompareAlerts)
		for _, testCase := range alertListSortTests {
			testCase.alert.UpdateFingerprints()
			if al[testCase.position].ContentFingerprint() != testCase.alert.ContentFingerprint() {
				failures++
			}
		}
	}
	if failures > 0 {
		t.Errorf("%d sort failures for %d checks", failures, iterations*len(al))
	}
}

type agFPTest struct {
	name     string
	ag       models.AlertGroup
	fpChange bool
}

var agFPTests = []agFPTest{
	{
		name: "empty group fingerprint",
		ag: models.AlertGroup{
			Receiver: "default",
		},
	},
	{
		name: "different Receiver shouldn't change content fingerprint",
		ag: models.AlertGroup{
			Receiver: "default",
		},
		fpChange: false,
	},
	{
		name: "different StateCount shouldn't change content fingerprint",
		ag: models.AlertGroup{
			Receiver:   "default",
			StateCount: map[string]int{},
		},
		fpChange: false,
	},
	{
		name: "different Labels shouldn't change content fingerprint",
		ag: models.AlertGroup{
			Receiver:   "default",
			Labels:     labels.FromStrings("foo", "bar"),
			StateCount: map[string]int{},
		},
		fpChange: false,
	},
	{
		name: "different set of alerts should change content fingerprint",
		ag: models.AlertGroup{
			Receiver: "default",
			Labels:   labels.FromStrings("foo", "bar"),
			Alerts: models.AlertList{
				models.Alert{
					Receiver: "default",
					State:    models.AlertStateActive,
					Labels:   labels.FromStrings("foo1", "bar"),
				},
			},
			StateCount: map[string]int{},
		},
		fpChange: true,
	},
	{
		name: "another different set of alerts should change content fingerprint",
		ag: models.AlertGroup{
			Receiver: "default",
			Labels:   labels.FromStrings("bar", "foo"),
			Alerts: models.AlertList{
				models.Alert{
					Receiver: "default",
					State:    models.AlertStateActive,
					Labels:   labels.FromStrings("bar", "foo"),
				},
			},
			StateCount: map[string]int{},
		},
		fpChange: true,
	},
	{
		name: "repeating last set of alerts shouldn't change content fingerprint",
		ag: models.AlertGroup{
			Receiver: "default",
			Labels:   labels.FromStrings("bar", "foo"),
			Alerts: models.AlertList{
				models.Alert{
					Receiver: "default",
					State:    models.AlertStateActive,
					Labels:   labels.FromStrings("bar", "foo"),
				},
			},
			StateCount: map[string]int{},
		},
		fpChange: false,
	},
}

func TestAlertGroupContentFingerprint(t *testing.T) {
	fps := []string{}
	for i, testCase := range agFPTests {
		t.Run(testCase.name, func(t *testing.T) {
			alerts := models.AlertList{}
			for _, alert := range testCase.ag.Alerts {
				alert.UpdateFingerprints()
				alerts = append(alerts, alert)
			}
			slices.SortFunc(alerts, models.CompareAlerts)
			testCase.ag.Alerts = alerts
			// get alert group fingerprint
			fp := testCase.ag.ContentFingerprint()
			// add it to the list
			fps = append(fps, fp)
			// skip first test case since there's nothing to compare it with
			if i > 0 {
				// check if current test case generated fingerprint that is different
				// from the previous one
				fpChange := fp != fps[i-1]
				// check if we expected a change or not
				if fpChange != testCase.fpChange {
					t.Errorf("Fingerprint changed=%t while expected change=%t, alertgroup: %v",
						fpChange, testCase.fpChange, testCase.ag)
				}
			}
		})
	}
}

func TestFingerprint(t *testing.T) {
	ag := models.AlertGroup{
		Receiver: "default",
	}
	if ag.LabelsFingerprint() == ag.ContentFingerprint() {
		t.Errorf("Expected LabelsFingerprint and ContentFingerprint to return different values")
	}
}

func TestLabelsFingerprint(t *testing.T) {
	// verifies that LabelsFingerprint produces non-empty output when labels are present
	ag := models.AlertGroup{
		Receiver: "default",
		Labels:   labels.FromStrings("baz", "qux", "foo", "bar"),
	}
	fp := ag.LabelsFingerprint()
	if fp == "" {
		t.Error("LabelsFingerprint() returned empty string for group with labels")
	}

	// verifies that different labels produce a different fingerprint
	ag2 := models.AlertGroup{
		Receiver: "default",
		Labels:   labels.FromStrings("different", "label"),
	}
	fp2 := ag2.LabelsFingerprint()
	if fp == fp2 {
		t.Errorf("LabelsFingerprint() should differ for groups with different labels, both returned %q", fp)
	}

	// verifies that same receiver and labels produce the same fingerprint
	ag3 := models.AlertGroup{
		Receiver: "default",
		Labels:   labels.FromStrings("baz", "qux", "foo", "bar"),
	}
	if ag3.LabelsFingerprint() != fp {
		t.Errorf("LabelsFingerprint() not stable: %q != %q", ag3.LabelsFingerprint(), fp)
	}
}

type findLatestStartsAtTest struct {
	expectedStartsAt time.Time
	alerts           []models.Alert
}

var findLatestStartsAtTests = []findLatestStartsAtTest{
	{
		alerts: []models.Alert{
			{StartsAt: time.Date(2017, time.January, 10, 0, 0, 0, 5, time.UTC)},
			{StartsAt: time.Date(2017, time.January, 10, 0, 0, 0, 1, time.UTC)},
			{StartsAt: time.Date(2017, time.January, 10, 0, 0, 0, 8, time.UTC)},
		},
		expectedStartsAt: time.Date(2017, time.January, 10, 0, 0, 0, 8, time.UTC),
	},
	{
		alerts: []models.Alert{
			{StartsAt: time.Date(2017, time.January, 10, 0, 0, 0, 8, time.UTC)},
			{StartsAt: time.Date(2017, time.January, 10, 0, 0, 0, 2, time.UTC)},
			{StartsAt: time.Date(2017, time.January, 10, 0, 0, 0, 1, time.UTC)},
		},
		expectedStartsAt: time.Date(2017, time.January, 10, 0, 0, 0, 8, time.UTC),
	},
	{
		alerts: []models.Alert{
			{StartsAt: time.Date(2017, time.January, 10, 0, 0, 0, 1, time.UTC)},
			{StartsAt: time.Date(2017, time.January, 10, 0, 0, 0, 1, time.UTC)},
			{StartsAt: time.Date(2017, time.January, 10, 0, 0, 0, 1, time.UTC)},
		},
		expectedStartsAt: time.Date(2017, time.January, 10, 0, 0, 0, 1, time.UTC),
	},
	{
		alerts: []models.Alert{
			{StartsAt: time.Date(2017, time.January, 10, 0, 0, 0, 5, time.UTC)},
			{StartsAt: time.Date(2017, time.January, 10, 0, 0, 0, 8, time.UTC)},
			{StartsAt: time.Date(2017, time.January, 10, 0, 0, 0, 1, time.UTC)},
		},
		expectedStartsAt: time.Date(2017, time.January, 10, 0, 0, 0, 8, time.UTC),
	},
}

func TestFindLatestStartsAt(t *testing.T) {
	for _, testCase := range findLatestStartsAtTests {
		ag := models.AlertGroup{Alerts: testCase.alerts}
		got := ag.FindLatestStartsAt()
		if !got.Equal(testCase.expectedStartsAt) {
			t.Errorf("FindLatestStartsAt returned %s when %s was expected", got, testCase.expectedStartsAt)
		}
	}
}
