package alert

import (
	"testing"

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

func TestAppendTeamFilterLeavesQueryUntouchedWhenTeamEmpty(t *testing.T) {
	t.Parallel()

	q, args := appendTeamFilter("SELECT 1 FROM alert WHERE created_at > ?", []any{"x"}, "")
	require.Equal(t, "SELECT 1 FROM alert WHERE created_at > ?", q)
	require.Equal(t, []any{"x"}, args)
}

func TestAppendTeamFilterAddsClauseAndArgWhenTeamSet(t *testing.T) {
	t.Parallel()

	q, args := appendTeamFilter("SELECT 1 FROM alert WHERE created_at > ?", []any{"x"}, "tikv-oncall")
	require.Equal(t, "SELECT 1 FROM alert WHERE created_at > ? AND "+oncallTeamExpr+" = ?", q)
	require.Equal(t, []any{"x", "tikv-oncall"}, args)
}

func TestPivotDailyRows(t *testing.T) {
	t.Parallel()

	got := pivotDailyRows([]dayResolutionRow{
		{Day: "2026-08-14", Resolution: ResolutionHandled, Count: 3},
		{Day: "2026-08-14", Resolution: ResolutionNoise, Count: 2},
		{Day: "2026-08-15", Resolution: ResolutionSelfRecovered, Count: 5},
	})

	require.Equal(t, []DailyResolutionMix{
		{Day: "2026-08-14", ResolutionCounts: ResolutionCounts{Handled: 3, Noise: 2, Total: 5}},
		{Day: "2026-08-15", ResolutionCounts: ResolutionCounts{SelfRecovered: 5, Total: 5}},
	}, got)
}

func TestPivotDailyRowsPreservesFirstSeenOrder(t *testing.T) {
	t.Parallel()

	// The caller's query is ORDER BY day, so a day's rows are contiguous but
	// arrive in whatever order the group's own resolutions happened to sort
	// in — the pivot must not reorder days by anything other than that.
	got := pivotDailyRows([]dayResolutionRow{
		{Day: "2026-08-16", Resolution: ResolutionNoise, Count: 1},
		{Day: "2026-08-14", Resolution: ResolutionHandled, Count: 1},
	})
	require.Equal(t, []string{"2026-08-16", "2026-08-14"}, []string{got[0].Day, got[1].Day})
}

// Every rule comes back — the page picks its own top ten by whichever
// resolution the reader chooses — busiest first, ties broken by name so two
// calls over the same data agree on the order.
func TestPivotRuleRowsKeepsEveryRuleBusiestFirst(t *testing.T) {
	t.Parallel()

	var rows []ruleResolutionRow
	for i := 0; i < 12; i++ {
		name := string(rune('a' + i))
		rows = append(rows, ruleResolutionRow{Alertname: name, Resolution: ResolutionNoise, Count: 12 - i})
	}
	// "z" ties with "a" on total but is mostly handled, not noise: still first
	// by name order among equals, and still present at all.
	rows = append(rows,
		ruleResolutionRow{Alertname: "z", Resolution: ResolutionHandled, Count: 11},
		ruleResolutionRow{Alertname: "z", Resolution: ResolutionNoise, Count: 1},
	)

	got := pivotRuleRows(rows)
	require.Len(t, got, 13)
	require.Equal(t, "a", got[0].Alertname)
	require.Equal(t, "z", got[1].Alertname)
	require.Equal(t, 12, got[1].Total)
	require.Equal(t, "l", got[12].Alertname)
}

func TestPivotRuleRowsSumsAcrossResolutions(t *testing.T) {
	t.Parallel()

	got := pivotRuleRows([]ruleResolutionRow{
		{Alertname: "TiKVStoreDown", Resolution: ResolutionHandled, Count: 4},
		{Alertname: "TiKVStoreDown", Resolution: ResolutionSelfRecovered, Count: 1},
		{Alertname: "TiKVStoreDown", Resolution: ResolutionNoise, Count: 10},
	})
	require.Equal(t, []RuleResolution{
		{Alertname: "TiKVStoreDown", ResolutionCounts: ResolutionCounts{Handled: 4, SelfRecovered: 1, Noise: 10, Total: 15}},
	}, got)
}

func TestPivotTeamRowsSplitsNightDayAndTicket(t *testing.T) {
	t.Parallel()

	got := pivotTeamRows([]teamRow{
		{OncallTeam: "tikv", Classification: ClassPage, Night: 1, Count: 5},
		{OncallTeam: "tikv", Classification: ClassPage, Night: 0, Count: 2},
		{OncallTeam: "tikv", Classification: ClassTicket, Night: 0, Count: 3},
		{OncallTeam: "tidb", Classification: ClassPage, Night: 1, Count: 1},
	})

	require.Equal(t, []TeamLoad{
		{OncallTeam: "tikv", NightPage: 5, DayPage: 2, Ticket: 3, Total: 10},
		{OncallTeam: "tidb", NightPage: 1, Total: 1},
	}, got, "sorted by night pages descending")
}

// An alert whose rule set no oncall_team label comes back from SQL as the
// COALESCE'd empty string; it must land in a labelled bucket, not a blank row,
// so a reader can tell "nobody's rotation owns this" from a real team.
func TestPivotTeamRowsFoldsEmptyTeamIntoUnrouted(t *testing.T) {
	t.Parallel()

	got := pivotTeamRows([]teamRow{
		{OncallTeam: "", Classification: ClassPage, Night: 0, Count: 4},
		{OncallTeam: "", Classification: ClassTicket, Night: 0, Count: 1},
	})

	require.Equal(t, []TeamLoad{
		{OncallTeam: UnroutedTeam, DayPage: 4, Ticket: 1, Total: 5},
	}, got)
}

func TestPivotHourRowsFillsAllTwentyFourHours(t *testing.T) {
	t.Parallel()

	got := pivotHourRows([]hourRow{{Hour: 3, Count: 7}, {Hour: 23, Count: 2}})
	require.Len(t, got, 24)
	require.Equal(t, HourlyBucket{Hour: 0, Count: 0}, got[0])
	require.Equal(t, HourlyBucket{Hour: 3, Count: 7}, got[3])
	require.Equal(t, HourlyBucket{Hour: 23, Count: 2}, got[23])
}

func TestAddResolutionIgnoresUnknownResolutionInCount(t *testing.T) {
	t.Parallel()

	var c ResolutionCounts
	addResolution(&c, ResolutionHandled, 2)
	addResolution(&c, "unexpected", 1)
	require.Equal(t, ResolutionCounts{Handled: 2, Total: 3}, c, "total still counts every row, even one whose resolution is unrecognized")
}

func TestPercentileEmptyIsZero(t *testing.T) {
	t.Parallel()

	require.Equal(t, int64(0), percentile(nil, 50))
	require.Equal(t, int64(0), percentile([]int64{}, 90))
}

func TestPercentileNearestRank(t *testing.T) {
	t.Parallel()

	// 10 ascending values 10..100: P50 is the 5th (ceil(0.5*10)=5), P90 the 9th.
	sorted := []int64{10, 20, 30, 40, 50, 60, 70, 80, 90, 100}
	require.Equal(t, int64(50), percentile(sorted, 50))
	require.Equal(t, int64(90), percentile(sorted, 90))
	require.Equal(t, int64(10), percentile(sorted, 1), "even a tiny percentile picks the smallest value, never nothing")
	require.Equal(t, int64(100), percentile(sorted, 100))
}

func TestPercentileSingleSample(t *testing.T) {
	t.Parallel()

	require.Equal(t, int64(42), percentile([]int64{42}, 50))
	require.Equal(t, int64(42), percentile([]int64{42}, 90))
}

func TestPivotDurationRowsEmpty(t *testing.T) {
	t.Parallel()

	overall, daily := pivotDurationRows(nil)
	require.Nil(t, overall.P50Seconds, "no samples means no percentile, not a percentile of zero")
	require.Nil(t, overall.P90Seconds)
	require.Empty(t, daily)
}

func TestPivotDurationRowsOverallAndDaily(t *testing.T) {
	t.Parallel()

	overall, daily := pivotDurationRows([]durationRow{
		{Day: "2026-08-15", DurationSeconds: 300},
		{Day: "2026-08-14", DurationSeconds: 100},
		{Day: "2026-08-14", DurationSeconds: 200},
	})

	// Days come out chronological regardless of input order.
	require.Equal(t, []string{"2026-08-14", "2026-08-15"}, []string{daily[0].Day, daily[1].Day})
	require.Equal(t, SLADayPoint{Day: "2026-08-14", P50Seconds: 100, P90Seconds: 200}, daily[0])
	require.Equal(t, SLADayPoint{Day: "2026-08-15", P50Seconds: 300, P90Seconds: 300}, daily[1])

	require.NotNil(t, overall.P50Seconds)
	require.NotNil(t, overall.P90Seconds)
	// Overall pools every row regardless of day: sorted [100, 200, 300].
	require.Equal(t, int64(200), *overall.P50Seconds)
	require.Equal(t, int64(300), *overall.P90Seconds)
}
