package promrelabel

import (
	"reflect"
	"testing"

	"gopkg.in/yaml.v2"
)

func TestMultiLineRegexUnmarshalMarshal(t *testing.T) {
	f := func(data, resultExpected string) {
		t.Helper()
		var mlr MultiLineRegex
		if err := yaml.UnmarshalStrict([]byte(data), &mlr); err != nil {
			t.Fatalf("cannot unmarshal %q: %s", data, err)
		}
		result, err := yaml.Marshal(&mlr)
		if err != nil {
			t.Fatalf("cannot marshal %q: %s", data, err)
		}
		if string(result) != resultExpected {
			t.Fatalf("unexpected marshaled data; got\n%q\nwant\n%q", result, resultExpected)
		}
	}
	f(``, `""`+"\n")
	f(`foo`, "foo\n")
	f(`a|b||c`, "- a\n- b\n- \"\"\n- c\n")
	f(`(a|b)`, "(a|b)\n")
	f(`a|b[c|d]`, "a|b[c|d]\n")
	f("- a\n- b", "- a\n- b\n")
	f("- a\n- (b)", "a|(b)\n")
}

func TestRelabelConfigMarshalUnmarshal(t *testing.T) {
	f := func(data, resultExpected string) {
		t.Helper()
		var rcs []RelabelConfig
		if err := yaml.UnmarshalStrict([]byte(data), &rcs); err != nil {
			t.Fatalf("cannot unmarshal %q: %s", data, err)
		}
		result, err := yaml.Marshal(&rcs)
		if err != nil {
			t.Fatalf("cannot marshal %q: %s", data, err)
		}
		if string(result) != resultExpected {
			t.Fatalf("unexpected marshaled data; got\n%q\nwant\n%q", result, resultExpected)
		}
	}
	f(``, "[]\n")
	f(`
- action: keep
  regex: foobar
  `, "- action: keep\n  regex: foobar\n")
	f(`
- regex:
  - 'fo.+'
  - '.*ba[r-z]a'
`, "- regex: fo.+|.*ba[r-z]a\n")
	f(`- regex: foo|bar`, "- regex:\n  - foo\n  - bar\n")
	f(`- regex: True`, `- regex: "true"`+"\n")
	f(`- regex: true`, `- regex: "true"`+"\n")
	f(`- regex: 123`, `- regex: "123"`+"\n")
	f(`- regex: 1.23`, `- regex: "1.23"`+"\n")
	f(`- regex: [null]`, `- regex: "null"`+"\n")
	f(`
- regex:
  - -1.23
  - False
  - null
  - nan
`, "- regex:\n  - \"-1.23\"\n  - \"false\"\n  - \"null\"\n  - nan\n")
	f(`
- action: graphite
  match: 'foo.*.*.aaa'
  labels:
    instance: '$1-abc'
    job: '${2}'
`, "- action: graphite\n  match: foo.*.*.aaa\n  labels:\n    instance: $1-abc\n    job: ${2}\n")
}

func TestLoadRelabelConfigsSuccess(t *testing.T) {
	path := "testdata/relabel_configs_valid.yml"
	pcs, _, err := LoadRelabelConfigs(path)
	if err != nil {
		t.Fatalf("cannot load relabel configs from %q: %s", path, err)
	}
	nExpected := 18
	if n := pcs.Len(); n != nExpected {
		t.Fatalf("unexpected number of relabel configs loaded from %q; got %d; want %d", path, n, nExpected)
	}
}

func TestLoadRelabelConfigsFailure(t *testing.T) {
	f := func(path string) {
		t.Helper()
		rcs, _, err := LoadRelabelConfigs(path)
		if err == nil {
			t.Fatalf("expecting non-nil error")
		}
		if rcs.Len() != 0 {
			t.Fatalf("unexpected non-empty rcs: %#v", rcs)
		}
	}

	// non-existing-file
	f("testdata/non-exsiting-file")

	// invalid-file
	f("testdata/invalid_config.yml")
}

func TestParsedConfigsString(t *testing.T) {
	f := func(rcs []RelabelConfig, sExpected string) {
		t.Helper()
		pcs, err := ParseRelabelConfigs(rcs)
		if err != nil {
			t.Fatalf("unexpected error: %s", err)
		}
		s := pcs.String()
		if s != sExpected {
			t.Fatalf("unexpected string representation for ParsedConfigs;\ngot\n%s\nwant\n%s", s, sExpected)
		}
	}
	f([]RelabelConfig{
		{
			TargetLabel:  "foo",
			SourceLabels: []string{"aaa"},
		},
	}, "- source_labels: [aaa]\n  target_label: foo\n")
	var ie IfExpression
	if err := ie.Parse("{foo=~'bar'}"); err != nil {
		t.Fatalf("unexpected error when parsing if expression: %s", err)
	}
	f([]RelabelConfig{
		{
			Action: "graphite",
			Match:  "foo.*.bar",
			Labels: map[string]string{
				"job": "$1-zz",
			},
			If: &ie,
		},
	}, "- if: '{foo=~''bar''}'\n  action: graphite\n  match: foo.*.bar\n  labels:\n    job: $1-zz\n")
	replacement := "foo"
	f([]RelabelConfig{
		{
			Action:       "replace",
			SourceLabels: []string{"foo", "bar"},
			TargetLabel:  "x",
			If:           &ie,
		},
		{
			TargetLabel: "x",
			Replacement: &replacement,
		},
	}, "- if: '{foo=~''bar''}'\n  action: replace\n  source_labels: [foo, bar]\n  target_label: x\n- target_label: x\n  replacement: foo\n")
}

func TestParseRelabelConfigsSuccess(t *testing.T) {
	f := func(rcs []RelabelConfig, pcsExpected *ParsedConfigs) {
		t.Helper()
		pcs, err := ParseRelabelConfigs(rcs)
		if err != nil {
			t.Fatalf("unexpected error: %s", err)
		}
		if pcs != nil {
			for _, prc := range pcs.prcs {
				prc.ruleOriginal = ""
				prc.stringReplacer = nil
				prc.submatchReplacer = nil
			}
		}
		if !reflect.DeepEqual(pcs, pcsExpected) {
			t.Fatalf("unexpected pcs; got\n%#v\nwant\n%#v", pcs, pcsExpected)
		}
	}
	f(nil, nil)
	f([]RelabelConfig{
		{
			SourceLabels: []string{"foo", "bar"},
			TargetLabel:  "xxx",
		},
	}, &ParsedConfigs{
		prcs: []*parsedRelabelConfig{
			{
				SourceLabels:  []string{"foo", "bar"},
				Separator:     ";",
				TargetLabel:   "xxx",
				RegexAnchored: defaultRegexForRelabelConfig,
				Replacement:   "$1",
				Action:        "replace",

				regex:                        defaultPromRegex,
				regexOriginal:                defaultOriginalRegexForRelabelConfig,
				hasCaptureGroupInReplacement: true,
			},
		},
	})
}

func TestParseRelabelConfigsFailure(t *testing.T) {
	f := func(rcs []RelabelConfig) {
		t.Helper()
		pcs, err := ParseRelabelConfigs(rcs)
		if err == nil {
			t.Fatalf("expecting non-nil error")
		}
		if pcs.Len() > 0 {
			t.Fatalf("unexpected non-empty pcs: %#v", pcs)
		}
	}

	// invalid regex
	f([]RelabelConfig{
		{
			SourceLabels: []string{"aaa"},
			TargetLabel:  "xxx",
			Regex: &MultiLineRegex{
				S: "foo[bar",
			},
		},
	})

	// replace-missing-target-label
	f([]RelabelConfig{
		{
			Action:       "replace",
			SourceLabels: []string{"foo"},
		},
	})

	// replace_all-missing-source-labels
	f([]RelabelConfig{
		{
			Action:      "replace_all",
			TargetLabel: "xxx",
		},
	})

	// replace_all-missing-target-label
	f([]RelabelConfig{
		{
			Action:       "replace_all",
			SourceLabels: []string{"foo"},
		},
	})

	// keep-missing-source-labels
	f([]RelabelConfig{
		{
			Action: "keep",
		},
	})

	// keep_if_contains-missing-target-label
	f([]RelabelConfig{
		{
			Action:       "keep_if_contains",
			SourceLabels: []string{"foo"},
		},
	})

	// keep_if_contains-missing-source-labels
	f([]RelabelConfig{
		{
			Action:      "keep_if_contains",
			TargetLabel: "foo",
		},
	})

	// keep_if_contains-unused-regex
	f([]RelabelConfig{
		{
			Action:       "keep_if_contains",
			TargetLabel:  "foo",
			SourceLabels: []string{"bar"},
			Regex: &MultiLineRegex{
				S: "bar",
			},
		},
	})

	// drop_if_contains-missing-target-label
	f([]RelabelConfig{
		{
			Action:       "drop_if_contains",
			SourceLabels: []string{"foo"},
		},
	})

	// drop_if_contains-missing-source-labels
	f([]RelabelConfig{
		{
			Action:      "drop_if_contains",
			TargetLabel: "foo",
		},
	})

	// drop_if_contains-unused-regex
	f([]RelabelConfig{
		{
			Action:       "drop_if_contains",
			TargetLabel:  "foo",
			SourceLabels: []string{"bar"},
			Regex: &MultiLineRegex{
				S: "bar",
			},
		},
	})

	// keep_if_equal-missing-source-labels
	f([]RelabelConfig{
		{
			Action: "keep_if_equal",
		},
	})

	// keep_if_equal-single-source-label
	f([]RelabelConfig{
		{
			Action:       "keep_if_equal",
			SourceLabels: []string{"foo"},
		},
	})

	// keep_if_equal-unused-target-label
	f([]RelabelConfig{
		{
			Action:       "keep_if_equal",
			SourceLabels: []string{"foo", "bar"},
			TargetLabel:  "foo",
		},
	})

	// keep_if_equal-unused-regex
	f([]RelabelConfig{
		{
			Action:       "keep_if_equal",
			SourceLabels: []string{"foo", "bar"},
			Regex: &MultiLineRegex{
				S: "bar",
			},
		},
	})

	// drop_if_equal-missing-source-labels
	f([]RelabelConfig{
		{
			Action: "drop_if_equal",
		},
	})

	// drop_if_equal-single-source-label
	f([]RelabelConfig{
		{
			Action:       "drop_if_equal",
			SourceLabels: []string{"foo"},
		},
	})

	// drop_if_equal-unused-target-label
	f([]RelabelConfig{
		{
			Action:       "drop_if_equal",
			SourceLabels: []string{"foo", "bar"},
			TargetLabel:  "foo",
		},
	})

	// drop_if_equal-unused-regex
	f([]RelabelConfig{
		{
			Action:       "drop_if_equal",
			SourceLabels: []string{"foo", "bar"},
			Regex: &MultiLineRegex{
				S: "bar",
			},
		},
	})

	// keepequal-missing-source-labels
	f([]RelabelConfig{
		{
			Action: "keepequal",
		},
	})

	// keepequal-missing-target-label
	f([]RelabelConfig{
		{
			Action:       "keepequal",
			SourceLabels: []string{"foo"},
		},
	})

	// keepequal-unused-regex
	f([]RelabelConfig{
		{
			Action:       "keepequal",
			SourceLabels: []string{"foo"},
			TargetLabel:  "foo",
			Regex: &MultiLineRegex{
				S: "bar",
			},
		},
	})

	// dropequal-missing-source-labels
	f([]RelabelConfig{
		{
			Action: "dropequal",
		},
	})

	// dropequal-missing-target-label
	f([]RelabelConfig{
		{
			Action:       "dropequal",
			SourceLabels: []string{"foo"},
		},
	})

	// dropequal-unused-regex
	f([]RelabelConfig{
		{
			Action:       "dropequal",
			SourceLabels: []string{"foo"},
			TargetLabel:  "foo",
			Regex: &MultiLineRegex{
				S: "bar",
			},
		},
	})

	// drop-missing-source-labels
	f([]RelabelConfig{
		{
			Action: "drop",
		},
	})

	// hashmod-missing-source-labels
	f([]RelabelConfig{
		{
			Action:      "hashmod",
			TargetLabel: "aaa",
			Modulus:     123,
		},
	})

	// hashmod-missing-target-label
	f([]RelabelConfig{
		{
			Action:       "hashmod",
			SourceLabels: []string{"aaa"},
			Modulus:      123,
		},
	})

	// hashmod-missing-modulus
	f([]RelabelConfig{
		{
			Action:       "hashmod",
			SourceLabels: []string{"aaa"},
			TargetLabel:  "xxx",
		},
	})

	// invalid-action
	f([]RelabelConfig{
		{
			Action: "invalid-action",
		},
	})

	// drop_metrics-missing-regex
	f([]RelabelConfig{
		{
			Action: "drop_metrics",
		},
	})

	// drop_metrics-non-empty-source-labels
	f([]RelabelConfig{
		{
			Action:       "drop_metrics",
			SourceLabels: []string{"foo"},
			Regex: &MultiLineRegex{
				S: "bar",
			},
		},
	})

	// keep_metrics-missing-regex
	f([]RelabelConfig{
		{
			Action: "keep_metrics",
		},
	})

	// keep_metrics-non-empty-source-labels
	f([]RelabelConfig{
		{
			Action:       "keep_metrics",
			SourceLabels: []string{"foo"},
			Regex: &MultiLineRegex{
				S: "bar",
			},
		},
	})

	// uppercase-missing-sourceLabels
	f([]RelabelConfig{
		{
			Action:      "uppercase",
			TargetLabel: "foobar",
		},
	})

	// lowercase-missing-targetLabel
	f([]RelabelConfig{
		{
			Action:       "lowercase",
			SourceLabels: []string{"foobar"},
		},
	})

	// graphite-missing-match
	f([]RelabelConfig{
		{
			Action: "graphite",
			Labels: map[string]string{
				"foo": "bar",
			},
		},
	})

	// graphite-missing-labels
	f([]RelabelConfig{
		{
			Action: "graphite",
			Match:  "foo.*.bar",
		},
	})

	// graphite-superflouous-sourceLabels
	f([]RelabelConfig{
		{
			Action: "graphite",
			Match:  "foo.*.bar",
			Labels: map[string]string{
				"foo": "bar",
			},
			SourceLabels: []string{"foo"},
		},
	})

	// graphite-superflouous-targetLabel
	f([]RelabelConfig{
		{
			Action: "graphite",
			Match:  "foo.*.bar",
			Labels: map[string]string{
				"foo": "bar",
			},
			TargetLabel: "foo",
		},
	})

	// graphite-superflouous-replacement
	replacement := "foo"
	f([]RelabelConfig{
		{
			Action: "graphite",
			Match:  "foo.*.bar",
			Labels: map[string]string{
				"foo": "bar",
			},
			Replacement: &replacement,
		},
	})

	// graphite-superflouous-regex
	var re MultiLineRegex
	f([]RelabelConfig{
		{
			Action: "graphite",
			Match:  "foo.*.bar",
			Labels: map[string]string{
				"foo": "bar",
			},
			Regex: &re,
		},
	})

	// non-graphite-superflouos-match
	f([]RelabelConfig{
		{
			Action:       "uppercase",
			SourceLabels: []string{"foo"},
			TargetLabel:  "foo",
			Match:        "aaa",
		},
	})

	// non-graphite-superflouos-labels
	f([]RelabelConfig{
		{
			Action:       "uppercase",
			SourceLabels: []string{"foo"},
			TargetLabel:  "foo",
			Labels: map[string]string{
				"foo": "Bar",
			},
		},
	})
}

func TestIsDefaultRegex(t *testing.T) {
	f := func(s string, resultExpected bool) {
		t.Helper()
		result := isDefaultRegex(s)
		if result != resultExpected {
			t.Fatalf("unexpected result for isDefaultRegex(%q); got %v; want %v", s, result, resultExpected)
		}
	}
	f("", false)
	f("foo", false)
	f(".+", false)
	f("a.*", false)
	f(".*", true)
	f("(.*)", true)
	f("^.*$", true)
	f("(?:.*)", true)
}
