package subscription

import (
	"encoding/json"
	"testing"
)

func mustMatch(t *testing.T, raw, target string, want bool) {
	t.Helper()
	var f SubscriptionFilter
	if err := json.Unmarshal([]byte(raw), &f); err != nil {
		t.Fatalf("unmarshal %s: %v", raw, err)
	}
	if got := f.Matches(Target{Alertname: target}); got != want {
		t.Errorf("%s against alertname %q = %v, want %v", raw, target, got, want)
	}
}

// A regex operator matches the whole target, not a substring of it: `TiKV_.*`
// means an alertname that begins with `TiKV_`, the way alertmanager's `=~` reads.
func TestRegexOpIsWholeStringAnchored(t *testing.T) {
	const re = `{"alertname":{"op":"=~","values":["TiKV_.*"]}}`
	mustMatch(t, re, "TiKV_disk_almost_full", true)
	mustMatch(t, re, "PD_TiKV_down", false)
}

// Values are alternatives under =~ as they are under =, so any pattern matching
// is enough.
func TestRegexOpIsOrAcrossValues(t *testing.T) {
	const re = `{"alertname":{"op":"=~","values":["TiKV_.*","PD_.*"]}}`
	mustMatch(t, re, "PD_down", true)
	mustMatch(t, re, "TiFlash_down", false)
}

// Negation flips the whole set: != means the target is none of the values, and
// !~ means it matches none of the patterns.
func TestNegationIsNoneOfTheSet(t *testing.T) {
	mustMatch(t, `{"alertname":{"op":"!=","values":["PD_down","TiKV_oom"]}}`, "PD_down", false)
	mustMatch(t, `{"alertname":{"op":"!=","values":["PD_down","TiKV_oom"]}}`, "TiFlash_down", true)
	mustMatch(t, `{"alertname":{"op":"!~","values":["TiKV_.*"]}}`, "TiKV_oom", false)
	mustMatch(t, `{"alertname":{"op":"!~","values":["TiKV_.*"]}}`, "PD_down", true)
}

// A regex carries its own casing; the enum fields' case-folding does not reach
// it. (Enum fields refuse regex at validation, but the matcher must not fold one
// if a stored filter ever holds it.)
func TestRegexIsNotCaseFolded(t *testing.T) {
	mustMatch(t, `{"alertname":{"op":"=~","values":["tikv.*"]}}`, "TiKV_down", false)
}

// The wire is backward compatible: the equal case is the bare array it always
// was and round-trips to the same bytes; any other operator is the object form.
func TestFieldMatchJSONRoundTrip(t *testing.T) {
	for _, raw := range []string{
		`{"alertname":["PD_down"]}`,
		`{"alertname":{"op":"=~","values":["TiKV_.*"]}}`,
		`{"cluster_id":{"op":"!=","values":["c9"]}}`,
	} {
		var f SubscriptionFilter
		if err := json.Unmarshal([]byte(raw), &f); err != nil {
			t.Fatalf("unmarshal %s: %v", raw, err)
		}
		out, err := json.Marshal(f)
		if err != nil {
			t.Fatalf("marshal %s: %v", raw, err)
		}
		if string(out) != raw {
			t.Errorf("round-trip changed the bytes:\n got %s\nwant %s", out, raw)
		}
	}
}

// An unparseable pattern is refused where the filter is decoded, so a bad regex
// is a create-time error rather than a rule that silently skips itself later.
func TestBadRegexRejectedOnDecode(t *testing.T) {
	if err := ValidateMuteFilter(json.RawMessage(`{"alertname":{"op":"=~","values":["TiKV_(["]}}`)); err == nil {
		t.Error("an unparseable regex should be refused")
	}
}

// classification, tenant_kind, customer_type and biz_region equal one of a
// fixed set, so they take only `=` — even `!=` is refused.
func TestFixedSetFieldsAreEqualOnly(t *testing.T) {
	for _, raw := range []string{
		`{"classification":{"op":"=~","values":["pa.*"]}}`,
		`{"classification":{"op":"!=","values":["page"]}}`,
		`{"tenant_kind":{"op":"!=","values":["internal"]}}`,
		`{"customer_type":{"op":"!=","values":["KA"]}}`,
		`{"biz_region":{"op":"!=","values":["NA"]}}`,
	} {
		if err := ValidateMuteFilter(json.RawMessage(raw)); err == nil {
			t.Errorf("%s should be refused: fixed-set fields take only =", raw)
		}
	}
}

// The id fields take `=` and `!=` but not a regex: an id is an exact value and a
// regex over it would only obscure it.
func TestIdFieldsAllowNegationButNotRegex(t *testing.T) {
	for _, raw := range []string{
		`{"cluster_id":{"op":"=~","values":["c.*"]}}`,
		`{"tenant_id":{"op":"!~","values":["t.*"]}}`,
	} {
		if err := ValidateMuteFilter(json.RawMessage(raw)); err == nil {
			t.Errorf("%s should be refused: these fields do not support regex", raw)
		}
	}
	for _, raw := range []string{
		`{"cluster_id":{"op":"!=","values":["c9"]}}`,
		`{"tenant_id":{"op":"!=","values":["t9"]}}`,
	} {
		if err := ValidateMuteFilter(json.RawMessage(raw)); err != nil {
			t.Errorf("%s should be allowed: != is meaningful here: %v", raw, err)
		}
	}
}

// An unknown operator is refused rather than silently treated as a mismatch.
func TestUnknownOperatorRejected(t *testing.T) {
	if err := ValidateMuteFilter(json.RawMessage(`{"alertname":{"op":"~=","values":["x"]}}`)); err == nil {
		t.Error("an unknown operator should be refused")
	}
}
