package notification

import (
	"encoding/json"
	"net/http"
	"strings"
	"testing"
	"time"

	"github.com/prometheus/alertmanager/template"
)

func TestNewWebhookPayload(t *testing.T) {
	startsAt := time.Date(2026, 7, 24, 1, 2, 3, 0, time.UTC)
	alert := template.Alert{
		Status: "firing",
		Labels: map[string]string{
			AlertLabelSeverity: "major",
			"internal_label":   "sensitive-label-value",
		},
		Annotations: map[string]string{
			AlertAnnotationSubject: "high latency",
			AlertAnnotationContent: "cluster TiDBClusterName is slow",
			"internal_annotation":  "sensitive-annotation-value",
		},
		StartsAt: startsAt,
	}
	payload := newWebhookPayload(alert, AlertMeta{
		ProjectId:   42,
		ProjectName: "project-a",
		ClusterId:   9007199254740993,
		ClusterName: "cluster-a",
		GroupKey:    realGroupKey,
	}, false)

	if payload.Receiver != webhookReceiverName {
		t.Fatalf("unexpected receiver: %s", payload.Receiver)
	}
	if payload.Status != "firing" {
		t.Fatalf("unexpected status: %s", payload.Status)
	}
	if payload.GroupKey != hashGroupKey(realGroupKey) {
		t.Fatalf("group key must be hashed, got %s", payload.GroupKey)
	}
	if len(payload.Alerts) != 1 {
		t.Fatalf("expected exactly one alert, got %d", len(payload.Alerts))
	}
	item := payload.Alerts[0]
	if item.Labels["severity"] != "warning" {
		t.Fatalf("expected major to map to warning, got %s", item.Labels["severity"])
	}
	if item.Annotations["message"] != "cluster cluster-a is slow" {
		t.Fatalf("unexpected message: %s", item.Annotations["message"])
	}
	if item.Annotations["alertname"] != "high latency" {
		t.Fatalf("unexpected alertname: %s", item.Annotations["alertname"])
	}
	// IDs are deliberately not exposed, matching every other subscriber type.
	for _, key := range []string{AlertLabelTiDBClusterID, AlertLabelClusterID, AlertLabelProjectID} {
		if _, ok := item.Labels[key]; ok {
			t.Fatalf("%s must not be exposed to customers: %#v", key, item.Labels)
		}
	}
	if item.Labels["cluster"] != "cluster-a" || item.Labels["project"] != "project-a" {
		t.Fatalf("unexpected cluster/project labels: %#v", item.Labels)
	}
	if item.EndsAt != nil {
		t.Fatal("zero endsAt must be omitted")
	}
	if payload.GroupLabels["cluster"] != "cluster-a" ||
		payload.GroupLabels["severity"] != "warning" {
		t.Fatalf("unexpected group labels: %#v", payload.GroupLabels)
	}
	if payload.CommonAnnotations["alertname"] != "high latency" {
		t.Fatalf("unexpected common annotations: %#v", payload.CommonAnnotations)
	}
	body, err := json.Marshal(payload)
	if err != nil {
		t.Fatalf("failed to marshal webhook payload: %v", err)
	}
	for _, sensitiveValue := range []string{
		"internal_label",
		"sensitive-label-value",
		"internal_annotation",
		"sensitive-annotation-value",
	} {
		if strings.Contains(string(body), sensitiveValue) {
			t.Fatalf("webhook payload leaks internal alert data %q: %s", sensitiveValue, body)
		}
	}
}

// realGroupKey is a group key captured from staging. Alertmanager builds it
// from the route matchers and the group labels, so it carries internal data
// that must never reach a customer.
const realGroupKey = `{}/{o11y_biz_type=~"staging-(.*)",severity=~"critical|emergency|blocker|major|warning"}/` +
	`{visibility="external"}:{alertname="TotalTiKVMemoryUsageTooHigh", severity="major", ` +
	`tidb_cluster_id="10038208118091975001"}`

func TestHashGroupKeyHidesInternalData(t *testing.T) {
	hashed := hashGroupKey(realGroupKey)

	for _, secret := range []string{
		"tidb_cluster_id", "10038208118091975001", "o11y_biz_type", "staging",
		"visibility", "external", "TotalTiKVMemoryUsageTooHigh", "major", "emergency",
	} {
		if strings.Contains(hashed, secret) {
			t.Fatalf("hashed group key leaks %q: %s", secret, hashed)
		}
	}
	if len(hashed) != 16 {
		t.Fatalf("expected a 16 character digest, got %q", hashed)
	}
}

func TestHashGroupKeyIsStableAndDistinct(t *testing.T) {
	first := hashGroupKey(realGroupKey)
	second := hashGroupKey("" + realGroupKey)
	if first != second {
		t.Fatal("hashing must be deterministic, otherwise firing and resolved cannot be paired")
	}
	other := strings.Replace(realGroupKey, "TotalTiKVMemoryUsageTooHigh", "TiKVStorageUsageTooHigh", 1)
	if first == hashGroupKey(other) {
		t.Fatal("different groups must not collide")
	}
	// Test connection has no group, and an empty key must stay empty so that
	// omitempty drops it instead of shipping a constant digest.
	if hashGroupKey("") != "" {
		t.Fatalf("empty group key must hash to empty, got %q", hashGroupKey(""))
	}
}

// The same group fires and resolves; customers pair them by groupKey.
func TestWebhookPayloadPairsFiringAndResolved(t *testing.T) {
	meta := AlertMeta{ClusterId: 7, ClusterName: "cluster-b", GroupKey: realGroupKey}
	firing := newWebhookPayload(template.Alert{
		Status:   "firing",
		Labels:   map[string]string{AlertLabelSeverity: "major"},
		StartsAt: time.Date(2026, 8, 4, 9, 29, 0, 0, time.UTC),
	}, meta, false)
	resolved := newWebhookPayload(template.Alert{
		Status:   "resolved",
		Labels:   map[string]string{AlertLabelSeverity: "major"},
		StartsAt: time.Date(2026, 8, 4, 9, 29, 0, 0, time.UTC),
		EndsAt:   time.Date(2026, 8, 4, 9, 45, 0, 0, time.UTC),
	}, meta, false)

	if firing.GroupKey == "" || firing.GroupKey != resolved.GroupKey {
		t.Fatalf("firing and resolved must share a group key: %q vs %q",
			firing.GroupKey, resolved.GroupKey)
	}
	if resolved.Alerts[0].EndsAt == nil {
		t.Fatal("resolved alert must carry endsAt")
	}
}

func TestNewWebhookPayloadWithoutProject(t *testing.T) {
	alert := template.Alert{
		Status:   "resolved",
		Labels:   map[string]string{AlertLabelSeverity: "Major"},
		StartsAt: time.Date(2026, 7, 24, 1, 2, 3, 0, time.UTC),
		EndsAt:   time.Date(2026, 7, 24, 1, 30, 0, 0, time.UTC),
	}
	payload := newWebhookPayload(alert, AlertMeta{
		ClusterId:   7,
		ClusterName: "cluster-b",
	}, true)

	item := payload.Alerts[0]
	if _, ok := item.Labels["project"]; ok {
		t.Fatalf("project label must be omitted when there is no project: %#v", item.Labels)
	}
	// severity mapping must be case insensitive, matching replaceWebhookPayload
	// and the lark card renderer.
	if item.Labels["severity"] != "warning" {
		t.Fatalf("expected Major to map to warning, got %s", item.Labels["severity"])
	}
	if item.EndsAt == nil || !item.EndsAt.Equal(alert.EndsAt) {
		t.Fatalf("unexpected endsAt: %#v", item.EndsAt)
	}
	if !payload.DryRun {
		t.Fatal("dryRun must be propagated")
	}
}

func TestShouldRetryWebhook(t *testing.T) {
	tests := map[int]bool{
		http.StatusOK:                  false,
		http.StatusBadRequest:          false,
		http.StatusTooManyRequests:     true,
		http.StatusInternalServerError: true,
	}
	for statusCode, expected := range tests {
		if actual := shouldRetryWebhook(statusCode); actual != expected {
			t.Errorf("status %d: got retry=%v, want %v", statusCode, actual, expected)
		}
	}
}
