package alert

import (
	"encoding/json"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/alertstore/pkg/instance"
)

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

	tests := []struct {
		name   string
		labels map[string]string
		want   string
	}{
		{"tidb id", map[string]string{"tidb_cluster_id": "tidb123"}, "123"},
		{"cluster fallback", map[string]string{"cluster_id": "456"}, "456"},
		{"non numeric retained", map[string]string{"tidb_cluster_id": "cluster-prod"}, "cluster-prod"},
		// namespace stands in only as a last resort, and only in the tidb<id> form.
		{"namespace tidb id when no cluster label", map[string]string{"namespace": "tidb789"}, "789"},
		{"namespace does not override a cluster label", map[string]string{"tidb_cluster_id": "name", "namespace": "tidb789"}, "name"},
		{"namespace name is not a cluster", map[string]string{"namespace": "chenyiwen"}, ""},
	}
	for _, test := range tests {
		test := test
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()
			require.Equal(t, test.want, Fact(FactClusterID, test.labels))
		})
	}
}

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

	// Numeric preference decides between tenant_id and tenant only.
	require.Equal(t, "42", Fact(FactTenantID, map[string]string{
		"tenant_id": "name",
		"tenant":    "42",
	}))
	require.Equal(t, "tenant-name", Fact(FactTenantID, map[string]string{
		"tenant": "tenant-name",
	}))
	// o11y_tenant_id is a last resort: used when neither tenant_id nor tenant is
	// set, and never overriding one that is.
	require.Equal(t, "infra-dev", Fact(FactTenantID, map[string]string{
		"o11y_tenant_id": "infra-dev",
	}))
	require.Equal(t, "acct", Fact(FactTenantID, map[string]string{
		"tenant_id":      "acct",
		"o11y_tenant_id": "123",
	}))
}

func labelledInstances(t *testing.T, labelSets ...map[string]string) []instance.Instance {
	t.Helper()
	instances := make([]instance.Instance, len(labelSets))
	for i, labels := range labelSets {
		raw, err := json.Marshal(labels)
		require.NoError(t, err)
		instances[i] = instance.Instance{ID: int64(i + 1), Labels: raw}
	}
	return instances
}

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

	agree := labelledInstances(t,
		map[string]string{"component": "tikv", "region": "us-east-1"},
		map[string]string{"component": "tikv", "region": "us-east-1"},
	)
	require.Equal(t, "tikv", CommonFact(FactComponent, agree, "tikv"))

	diverge := labelledInstances(t,
		map[string]string{"component": "tikv"},
		map[string]string{"component": "tiflash"},
	)
	require.Equal(t, "tiflash, tikv", CommonFact(FactComponent, diverge, "tikv"))
}

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

	instances := labelledInstances(t,
		map[string]string{"environment": "prod"},
		map[string]string{},
	)
	require.Equal(t, "(none), prod", CommonFact(FactEnvironment, instances, "prod"))
}

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

	instances := labelledInstances(t,
		map[string]string{"component": "a"},
		map[string]string{"component": "b"},
		map[string]string{"component": "c"},
		map[string]string{"component": "d"},
		map[string]string{"component": "e"},
	)
	require.Equal(t, "a, b, c, +2 more", CommonFact(FactComponent, instances, "a"))
}

// Clinic enrichment and pre-instance alerts never reach an occurrence's labels,
// so the frozen attrs value has to survive an occurrence set that says nothing.
func TestCommonFactFallsBackToAttrs(t *testing.T) {
	t.Parallel()

	require.Equal(t, "1234", CommonFact(FactTenantID, nil, "1234"))
	require.Equal(t, "1234", CommonFact(FactTenantID,
		labelledInstances(t, map[string]string{"component": "tikv"}), "1234"))
}

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

	instances := labelledInstances(t,
		map[string]string{"alertname": "TiKVDiskAlmostFull", "cluster_id": "42", "pod": "tikv-0", "component": "tikv"},
		map[string]string{"alertname": "TiKVDiskAlmostFull", "cluster_id": "42", "pod": "tikv-1", "component": "tiflash"},
	)

	require.Equal(t, map[string]string{
		"alertname":  "TiKVDiskAlmostFull",
		"cluster_id": "42",
	}, CommonLabels(instances))

	// A key every occurrence carries at a different value is exactly what tells
	// them apart, so it stays on the occurrence.
	require.Equal(t, map[string]string{"pod": "tikv-0", "component": "tikv"},
		DistinctLabels(&instances[0], CommonLabels(instances)))
}

// One occurrence agrees with itself on everything, so the intersection would
// claim the whole set and leave the row below empty. The fixed alert-level list
// keeps what says which pod on the occurrence.
func TestCommonLabelsOfASingleOccurrence(t *testing.T) {
	t.Parallel()

	instances := labelledInstances(t, map[string]string{
		"alertname":       "TiDBDown",
		"severity":        "critical",
		"tidb_cluster_id": "42",
		"pod":             "tidb-0",
		"instance":        "10.0.0.1:10080",
	})

	require.Equal(t, map[string]string{
		"alertname":       "TiDBDown",
		"severity":        "critical",
		"tidb_cluster_id": "42",
	}, CommonLabels(instances))
	require.Equal(t, map[string]string{"pod": "tidb-0", "instance": "10.0.0.1:10080"},
		DistinctLabels(&instances[0], CommonLabels(instances)))
	require.Empty(t, CommonLabels(nil))
}

// Every fact source is alert-level, so a single occurrence does not keep
// fleet-wide context — cluster_id, region, environment — in the block that is
// supposed to say what distinguishes it. A hand-list once missed these.
func TestCommonLabelsOfASingleOccurrenceLiftsFactLabels(t *testing.T) {
	t.Parallel()

	instances := labelledInstances(t, map[string]string{
		"alertname":   "TiKVDiskAlmostFull",
		"cluster_id":  "42",
		"region":      "us-east-1",
		"environment": "prod",
		"pod":         "tikv-0",
		"instance":    "10.0.0.1:20180",
	})

	require.Equal(t, map[string]string{
		"alertname":   "TiKVDiskAlmostFull",
		"cluster_id":  "42",
		"region":      "us-east-1",
		"environment": "prod",
	}, CommonLabels(instances))
	require.Equal(t, map[string]string{"pod": "tikv-0", "instance": "10.0.0.1:20180"},
		DistinctLabels(&instances[0], CommonLabels(instances)))
}

// An occurrence carrying none of the alert-level keys leaves the alert-level
// block empty rather than swallowing its labels.
func TestCommonLabelsOfASingleOccurrenceWithoutAlertLevelKeys(t *testing.T) {
	t.Parallel()

	instances := labelledInstances(t, map[string]string{"pod": "tidb-0"})

	require.Empty(t, CommonLabels(instances))
	require.Equal(t, map[string]string{"pod": "tidb-0"},
		DistinctLabels(&instances[0], CommonLabels(instances)))
}

// A key one occurrence simply does not carry is not common, however many others
// agree on it.
func TestCommonLabelsRequireEveryOccurrence(t *testing.T) {
	t.Parallel()

	instances := labelledInstances(t,
		map[string]string{"alertname": "TiDBDown", "environment": "prod"},
		map[string]string{"alertname": "TiDBDown"},
	)

	require.Equal(t, map[string]string{"alertname": "TiDBDown"}, CommonLabels(instances))
	require.Equal(t, map[string]string{"environment": "prod"},
		DistinctLabels(&instances[0], CommonLabels(instances)))
}

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

	instances := labelledInstances(t,
		map[string]string{"component": "tikv", "tidb_cluster_id": "tidb42"},
		map[string]string{"component": "pd", "tidb_cluster_id": "tidb42"},
	)
	facts := CommonFacts(instances, &AlertAttrs{ClusterID: "42", Component: "tikv"})

	require.Equal(t, "42", facts[FactClusterID])
	require.Equal(t, "pd, tikv", facts[FactComponent])
	require.NotContains(t, facts, FactRegion)
}

// The audience is canonicalized once, so a rule that varied the spelling is
// not read as customer-facing by one caller and internal by another.
func TestNormalizeVisibility(t *testing.T) {
	cases := map[string]string{
		"external":   VisibilityExternal,
		"External":   VisibilityExternal,
		"  EXTERNAL": VisibilityExternal,
		"internal":   "internal",
		"":           "",
	}
	for in, want := range cases {
		got := NormalizeVisibility(map[string]string{FactVisibility: in})
		if got != want {
			t.Errorf("NormalizeVisibility(%q) = %q, want %q", in, got, want)
		}
	}
	if got := NormalizeVisibility(nil); got != "" {
		t.Errorf("NormalizeVisibility(nil) = %q, want empty (internal)", got)
	}
}

// Who an alert belongs to decides whether anyone is put on the hook for it: an
// internal tenant is one of our own accounts, so delivery hands it to nobody.
// Anything the ops portal could not answer about counts as a customer's, which
// is the assumption that fails safe — an unenriched alert still reaches an
// on-caller.
func TestInternalTenant(t *testing.T) {
	for name, tc := range map[string]struct {
		kind string
		want bool
	}{
		"internal":           {kind: "internal", want: true},
		"internal, any case": {kind: "Internal", want: true},
		"internal, padded":   {kind: " internal ", want: true},
		"external":           {kind: "external", want: false},
		"unenriched":         {kind: "", want: false},
		"something else":     {kind: "partner", want: false},
	} {
		t.Run(name, func(t *testing.T) {
			attrs := &AlertAttrs{TenantKind: tc.kind}
			if got := attrs.InternalTenant(); got != tc.want {
				t.Errorf("InternalTenant() = %v, want %v for %q", got, tc.want, tc.kind)
			}
		})
	}
}
