package delivery

import (
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/alertstore/pkg/event"
	"github.com/tidbcloud/alertstore/pkg/subscription"
)

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

	// description is the subject and message is the body for a customer-facing
	// rule — the reverse of the internal convention.
	subject, body := customerText(&event.EventAttrs{
		Annotations: map[string]string{
			"description": "Your cluster is running low on storage",
			"message":     "Storage on TiDBClusterName has passed 80%.",
			"summary":     "should not be preferred over description",
		},
		ClusterName: "prod-analytics",
	})

	require.Equal(t, "Your cluster is running low on storage", subject)
	require.Equal(t, "Storage on prod-analytics has passed 80%.", body)
}

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

	subject, body := customerText(&event.EventAttrs{
		Annotations: map[string]string{
			"summary": "Disk almost full",
			"message": "Disk almost full",
		},
	})

	require.Equal(t, "Disk almost full", subject)
	require.Empty(t, body, "a body identical to the subject is printed once")
}

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

	attrs := &event.EventAttrs{
		ClusterID: "10832315937079846455",
		Labels:    map[string]string{"o11y_biz_type": "staging-aws"},
		Annotations: map[string]string{
			"message": "Visit TiDBCloudDomainName to inspect TiDBClusterName," +
				" project TiDBProjectName.",
		},
	}
	_, body := customerText(attrs)

	// No clinic enrichment, so the cluster ID stands in for its name; the
	// project has no counterpart in the labels and is left alone rather than
	// guessed at.
	require.Contains(t, body, "staging.tidbcloud.com")
	require.Contains(t, body, "inspect 10832315937079846455")
	require.Contains(t, body, "project TiDBProjectName")
}

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

	require.Equal(t, "tidbcloud.com", tidbCloudDomain(nil))
	require.Equal(t, "dev.tidbcloud.com", tidbCloudDomain(map[string]string{"o11y_biz_type": "dev"}))
	require.Equal(t, "staging.tidbcloud.com", tidbCloudDomain(map[string]string{"o11y_biz_type": "staging2"}))
	require.Equal(t, "tidbcloud.com", tidbCloudDomain(map[string]string{"o11y_biz_type": "prod"}))
}

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

	card := buildNoticeCard(&noticePayload{
		ChannelID:   "oc_group",
		Mode:        subscription.ModeCustomer,
		EventID:     4242,
		Status:      "firing",
		Alertname:   "TiDBStorageAlmostFull",
		Severity:    "warn",
		ClusterID:   "123",
		ClusterName: "prod-analytics",
		TenantID:    "tenant-9",
		Region:      "us-west-2",
		Subject:     "Your cluster is running low on storage",
		NotifiedAt:  time.Date(2026, 8, 3, 10, 30, 0, 0, time.UTC),
	}, "https://alertstore.internal")

	require.Equal(t, "[customer firing] TiDBStorageAlmostFull - 123", card.Title)
	require.Equal(t, "orange", card.Color, "a notice is never red: nothing here is a page")
	require.Empty(t, card.ActionRows, "there is nothing to ack on a notice")
	require.Contains(t, card.Body, "prod-analytics (123)")
	require.Contains(t, card.Body, "2026-08-03 10:30:00 UTC")
	require.Contains(t, card.Body, "Your cluster is running low on storage")
	require.Contains(t, card.Body, "no on-caller was assigned or paged")
	require.Contains(t, card.Body, "https://alertstore.internal#/events/4242")
}

// An event-mode card is the same shape but a different claim: nobody outside
// has seen this one, so it must not read as though a customer had.
func TestEventNoticeCardDoesNotClaimTheCustomerWasTold(t *testing.T) {
	t.Parallel()

	card := buildNoticeCard(&noticePayload{
		ChannelID: "oc_group",
		Mode:      subscription.ModeEvent,
		EventID:   99,
		Status:    "firing",
		Alertname: "TiKVInstanceLowSpace",
		ClusterID: "123",
		Subject:   "Store 4 is low on space",
	}, "https://alertstore.internal")

	require.Equal(t, "[event firing] TiKVInstanceLowSpace - 123", card.Title)
	require.Equal(t, "blue", card.Color)
	require.Empty(t, card.ActionRows)
	require.Contains(t, card.Body, "Observed at")
	require.Contains(t, card.Body, "opened no alert")
	require.NotContains(t, card.Body, "customer")
}

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

	require.Equal(t, "orange", noticeColor(subscription.ModeCustomer, "firing"))
	require.Equal(t, "blue", noticeColor(subscription.ModeEvent, "firing"))
	require.Equal(t, "green", noticeColor(subscription.ModeCustomer, "resolved"))
	require.Equal(t, "green", noticeColor(subscription.ModeEvent, "resolved"))
}

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

	// What the card reports is when the audience was told, which is when the
	// notification arrived — not when the underlying condition began.
	startedAt := time.Date(2026, 8, 3, 9, 0, 0, 0, time.UTC)
	receivedAt := time.Date(2026, 8, 3, 11, 0, 0, 0, time.UTC)

	p := buildNoticePayload(
		&event.Event{ID: 7, Status: "firing", StartsAt: startedAt, ReceivedAt: receivedAt},
		&event.EventAttrs{Alertname: "TiDBStorageAlmostFull", Labels: map[string]string{"severity": "warn"}},
		subscription.ModeCustomer,
		event.Notice{},
	)

	require.Equal(t, receivedAt, p.NotifiedAt)
	require.Equal(t, int64(7), p.EventID)
	require.Equal(t, "warn", p.Severity)
	require.Equal(t, subscription.ModeCustomer, p.Mode)
}

// Event mode reads the internal annotation convention (summary titles,
// description elaborates); customer mode reads the reverse. Feeding one set of
// annotations to both must not produce the same card.
func TestEventModeReadsTheInternalAnnotationConvention(t *testing.T) {
	t.Parallel()

	attrs := &event.EventAttrs{
		Alertname: "TiKVInstanceLowSpace",
		Annotations: map[string]string{
			"summary":     "Store is low on space",
			"description": "Store 4 has 8% capacity left.",
		},
	}

	internal := buildNoticePayload(&event.Event{ID: 1}, attrs, subscription.ModeEvent, event.Notice{})
	require.Equal(t, "Store is low on space", internal.Subject)
	require.Equal(t, "Store 4 has 8% capacity left.", internal.Body)

	// The customer reading puts description in the subject slot instead.
	customer := buildNoticePayload(&event.Event{ID: 1}, attrs, subscription.ModeCustomer, event.Notice{})
	require.Equal(t, "Store 4 has 8% capacity left.", customer.Subject)
}

// The reason event mode exists: an alert collapses an occurrence's repeat
// firings into silence, so this card is the only place the repetition shows.
func TestEventNoticeReportsARepeatFiringAndNamesItsAlert(t *testing.T) {
	t.Parallel()

	card := buildNoticeCard(&noticePayload{
		ChannelID:       "oc_group",
		Mode:            subscription.ModeEvent,
		EventID:         512,
		Status:          "firing",
		ObservationType: "repeat_firing",
		Classification:  "page",
		AlertID:         "ALT-2608-13",
		Alertname:       "TiKVInstanceLowSpace",
		ClusterID:       "123",
	}, "https://alertstore.internal")

	require.Contains(t, card.Body, "repeat firing")
	require.Contains(t, card.Body, "has not cleared since the last notification")
	// A page-classified observation is somebody's problem, and the card must
	// not read like the "nobody was paged" case it used to be limited to.
	require.Contains(t, card.Body, "belongs to ALT-2608-13")
	require.NotContains(t, card.Body, "opened no alert")
	require.Contains(t, card.Body, "https://alertstore.internal#/alerts/ALT-2608-13")
	require.Empty(t, card.ActionRows, "acting on it happens on the alert, not here")
}

// Mute suppresses the response, never the record: a muted page still produces
// an event card, and that card is the only place the active rule is visible
// while it is suppressing something.
func TestMutedObservationIsStillAnnouncedAndSaysWhy(t *testing.T) {
	t.Parallel()

	card := buildNoticeCard(&noticePayload{
		ChannelID:       "oc_group",
		Mode:            subscription.ModeEvent,
		EventID:         777,
		Status:          "firing",
		ObservationType: "repeat_firing",
		Classification:  "page",
		MuteRuleID:      "3f2a-rule",
		MuteReason:      "noisy during the 8.5 upgrade",
		Alertname:       "TiKVInstanceLowSpace",
		ClusterID:       "123",
	}, "https://alertstore.internal")

	require.Contains(t, card.Body, "Muted by an active rule (3f2a-rule)")
	require.Contains(t, card.Body, "noisy during the 8.5 upgrade")
	require.NotContains(t, card.Body, "Recorded only")
	require.NotContains(t, card.Body, "belongs to")
}

// An observation that opened nothing keeps the older, simpler claim.
func TestEventNoticeWithoutAnAlertSaysNobodyWasPaged(t *testing.T) {
	t.Parallel()

	card := buildNoticeCard(&noticePayload{
		ChannelID:       "oc_group",
		Mode:            subscription.ModeEvent,
		EventID:         513,
		Status:          "firing",
		ObservationType: "first_firing",
		Classification:  "logging",
		Alertname:       "TiDBSchemaChangeSlow",
		ClusterID:       "123",
	}, "https://alertstore.internal")

	require.Contains(t, card.Body, "first firing")
	require.Contains(t, card.Body, "opened no alert")
	require.NotContains(t, card.Body, "belongs to")
}
