package rule_test

import (
	"encoding/json"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/influxdata/influxdb/v2"
	"github.com/influxdata/influxdb/v2/mock"
	"github.com/influxdata/influxdb/v2/notification"
	"github.com/influxdata/influxdb/v2/notification/rule"
	"github.com/influxdata/influxdb/v2/pkg/testing/assert"
	influxTesting "github.com/influxdata/influxdb/v2/testing"
)

func lvlPtr(l notification.CheckLevel) *notification.CheckLevel {
	return &l
}

const (
	id1 = "020f755c3c082000"
	id2 = "020f755c3c082001"
	id3 = "020f755c3c082002"
)

var goodBase = rule.Base{
	ID:         influxTesting.MustIDBase16(id1),
	Name:       "name1",
	OwnerID:    influxTesting.MustIDBase16(id2),
	OrgID:      influxTesting.MustIDBase16(id3),
	EndpointID: 1,
}

func TestValidRule(t *testing.T) {
	cases := []struct {
		name string
		src  influxdb.NotificationRule
		err  error
	}{
		{
			name: "invalid rule id",
			src:  &rule.Slack{},
			err: &influxdb.Error{
				Code: influxdb.EInvalid,
				Msg:  "Notification Rule ID is invalid",
			},
		},
		{
			name: "empty name",
			src: &rule.PagerDuty{
				Base: rule.Base{
					ID: influxTesting.MustIDBase16(id1),
				},
			},
			err: &influxdb.Error{
				Code: influxdb.EInvalid,
				Msg:  "Notification Rule Name can't be empty",
			},
		},
		{
			name: "invalid auth id",
			src: &rule.PagerDuty{
				Base: rule.Base{
					ID:   influxTesting.MustIDBase16(id1),
					Name: "name1",
				},
			},
			err: &influxdb.Error{
				Code: influxdb.EInvalid,
				Msg:  "Notification Rule OwnerID is invalid",
			},
		},
		{
			name: "invalid org id",
			src: &rule.PagerDuty{
				Base: rule.Base{
					ID:      influxTesting.MustIDBase16(id1),
					Name:    "name1",
					OwnerID: influxTesting.MustIDBase16(id2),
				},
			},
			err: &influxdb.Error{
				Code: influxdb.EInvalid,
				Msg:  "Notification Rule OrgID is invalid",
			},
		},
		{
			name: "invalid org id",
			src: &rule.Slack{
				Base: rule.Base{
					ID:         influxTesting.MustIDBase16(id1),
					Name:       "name1",
					OwnerID:    influxTesting.MustIDBase16(id2),
					OrgID:      influxTesting.MustIDBase16(id3),
					EndpointID: 0,
				},
			},
			err: &influxdb.Error{
				Code: influxdb.EInvalid,
				Msg:  "Notification Rule EndpointID is invalid",
			},
		},
		{
			name: "offset greater then interval",
			src: &rule.Slack{
				Base: rule.Base{
					ID:         influxTesting.MustIDBase16(id1),
					Name:       "name1",
					OwnerID:    influxTesting.MustIDBase16(id2),
					OrgID:      influxTesting.MustIDBase16(id3),
					EndpointID: 1,
					Every:      mustDuration("1m"),
					Offset:     mustDuration("2m"),
				},
			},
			err: &influxdb.Error{
				Code: influxdb.EInvalid,
				Msg:  "Offset should not be equal or greater than the interval",
			},
		},
		{
			name: "empty slack message",
			src: &rule.Slack{
				Base:    goodBase,
				Channel: "channel1",
			},
			err: &influxdb.Error{
				Code: influxdb.EInvalid,
				Msg:  "slack msg template is empty",
			},
		},
		{
			name: "empty pagerDuty message",
			src: &rule.PagerDuty{
				Base: goodBase,
			},
			err: &influxdb.Error{
				Code: influxdb.EInvalid,
				Msg:  "pagerduty invalid message template",
			},
		},
		{
			name: "bad tag rule",
			src: &rule.PagerDuty{
				Base: rule.Base{
					ID:         influxTesting.MustIDBase16(id1),
					OwnerID:    influxTesting.MustIDBase16(id2),
					Name:       "name1",
					OrgID:      influxTesting.MustIDBase16(id3),
					EndpointID: 1,
					TagRules: []notification.TagRule{
						{
							Tag: influxdb.Tag{
								Key:   "k1",
								Value: "v1",
							},
							Operator: -5,
						},
					},
				},
				MessageTemplate: "body {var2}",
			},
			err: &influxdb.Error{
				Code: influxdb.EInvalid,
				Msg:  `Operator is invalid`,
			},
		},
		{
			name: "bad limit",
			src: &rule.PagerDuty{
				Base: rule.Base{
					ID:         influxTesting.MustIDBase16(id1),
					OwnerID:    influxTesting.MustIDBase16(id2),
					OrgID:      influxTesting.MustIDBase16(id3),
					EndpointID: 1,
					Name:       "name1",
					TagRules: []notification.TagRule{
						{
							Tag: influxdb.Tag{
								Key:   "k1",
								Value: "v1",
							},
							Operator: influxdb.RegexEqual,
						},
					},
					Limit: &influxdb.Limit{
						Rate: 3,
					},
				},
				MessageTemplate: "body {var2}",
			},
			err: &influxdb.Error{
				Code: influxdb.EInvalid,
				Msg:  `if limit is set, limit and limitEvery must be larger than 0`,
			},
		},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			got := c.src.Valid()
			influxTesting.ErrorsEqual(t, got, c.err)
		})
	}
}

var timeGen1 = mock.TimeGenerator{FakeValue: time.Date(2006, time.July, 13, 4, 19, 10, 0, time.UTC)}
var timeGen2 = mock.TimeGenerator{FakeValue: time.Date(2006, time.July, 14, 5, 23, 53, 10, time.UTC)}
var time3 = time.Date(2006, time.July, 15, 5, 23, 53, 10, time.UTC)

func TestJSON(t *testing.T) {
	cases := []struct {
		name string
		src  influxdb.NotificationRule
	}{
		{
			name: "simple slack",
			src: &rule.Slack{
				Base: rule.Base{
					ID:          influxTesting.MustIDBase16(id1),
					OwnerID:     influxTesting.MustIDBase16(id2),
					Name:        "name1",
					OrgID:       influxTesting.MustIDBase16(id3),
					RunbookLink: "runbooklink1",
					SleepUntil:  &time3,
					Every:       mustDuration("1h"),
					TagRules: []notification.TagRule{
						{
							Tag: influxdb.Tag{
								Key:   "k1",
								Value: "v1",
							},
							Operator: influxdb.NotEqual,
						},
						{
							Tag: influxdb.Tag{
								Key:   "k2",
								Value: "v2",
							},
							Operator: influxdb.RegexEqual,
						},
					},
					CRUDLog: influxdb.CRUDLog{
						CreatedAt: timeGen1.Now(),
						UpdatedAt: timeGen2.Now(),
					},
				},
				Channel:         "channel1",
				MessageTemplate: "msg1",
			},
		},
		{
			name: "simple smtp",
			src: &rule.PagerDuty{
				Base: rule.Base{
					ID:          influxTesting.MustIDBase16(id1),
					Name:        "name1",
					OwnerID:     influxTesting.MustIDBase16(id2),
					OrgID:       influxTesting.MustIDBase16(id3),
					RunbookLink: "runbooklink1",
					SleepUntil:  &time3,
					Every:       mustDuration("1h"),
					TagRules: []notification.TagRule{
						{
							Tag: influxdb.Tag{
								Key:   "k1",
								Value: "v1",
							},
							Operator: influxdb.NotEqual,
						},
						{
							Tag: influxdb.Tag{
								Key:   "k2",
								Value: "v2",
							},
							Operator: influxdb.RegexEqual,
						},
					},
					CRUDLog: influxdb.CRUDLog{
						CreatedAt: timeGen1.Now(),
						UpdatedAt: timeGen2.Now(),
					},
				},
				MessageTemplate: "msg1",
			},
		},
		{
			name: "simple pagerDuty",
			src: &rule.PagerDuty{
				Base: rule.Base{
					ID:          influxTesting.MustIDBase16(id1),
					Name:        "name1",
					OwnerID:     influxTesting.MustIDBase16(id2),
					OrgID:       influxTesting.MustIDBase16(id3),
					RunbookLink: "runbooklink1",
					SleepUntil:  &time3,
					Every:       mustDuration("1h"),
					TagRules: []notification.TagRule{
						{
							Tag: influxdb.Tag{
								Key:   "k1",
								Value: "v1",
							},
							Operator: influxdb.NotEqual,
						},
					},
					StatusRules: []notification.StatusRule{
						{
							CurrentLevel:  notification.Warn,
							PreviousLevel: lvlPtr(notification.Critical),
						},
						{
							CurrentLevel: notification.Critical,
						},
					},
					CRUDLog: influxdb.CRUDLog{
						CreatedAt: timeGen1.Now(),
						UpdatedAt: timeGen2.Now(),
					},
				},
				MessageTemplate: "msg1",
			},
		},
	}
	for _, c := range cases {
		b, err := json.Marshal(c.src)
		if err != nil {
			t.Fatalf("%s marshal failed, err: %s", c.name, err.Error())
		}
		got, err := rule.UnmarshalJSON(b)
		if err != nil {
			t.Fatalf("%s unmarshal failed, err: %s", c.name, err.Error())
		}
		if diff := cmp.Diff(got, c.src); diff != "" {
			t.Errorf("failed %s, notification rule are different -got/+want\ndiff %s", c.name, diff)
		}
	}
}

func TestMatchingRules(t *testing.T) {
	cases := []struct {
		name       string
		tagRules   []notification.TagRule
		filterTags []influxdb.Tag
		exp        bool
	}{
		{
			name: "Matches when tagrules and filterTags are the same. ",
			tagRules: []notification.TagRule{
				{
					Tag: influxdb.Tag{
						Key:   "a",
						Value: "b",
					},
					Operator: influxdb.Equal,
				},
				{
					Tag: influxdb.Tag{
						Key:   "c",
						Value: "d",
					},
					Operator: influxdb.Equal,
				},
			},
			filterTags: []influxdb.Tag{
				{Key: "a", Value: "b"},
				{Key: "c", Value: "d"},
			},
			exp: true,
		},
		{
			name: "Matches when tagrules are subset of filterTags. ",
			tagRules: []notification.TagRule{
				{
					Tag: influxdb.Tag{
						Key:   "a",
						Value: "b",
					},
					Operator: influxdb.Equal,
				},
				{
					Tag: influxdb.Tag{
						Key:   "c",
						Value: "d",
					},
					Operator: influxdb.Equal,
				},
			},
			filterTags: []influxdb.Tag{
				{Key: "a", Value: "b"},
				{Key: "c", Value: "d"},
				{Key: "e", Value: "f"},
			},
			exp: true,
		},
		{
			name: "Does not match when filterTags are missing tags that are in tag rules.",
			tagRules: []notification.TagRule{
				{
					Tag: influxdb.Tag{
						Key:   "a",
						Value: "b",
					},
					Operator: influxdb.Equal,
				},
				{
					Tag: influxdb.Tag{
						Key:   "c",
						Value: "d",
					},
					Operator: influxdb.Equal,
				},
			},
			filterTags: []influxdb.Tag{
				{Key: "a", Value: "b"},
			},
			exp: false,
		},
		{
			name: "Does not match when tagrule has key value pair that does not match value of same key in filterTags",
			tagRules: []notification.TagRule{
				{
					Tag: influxdb.Tag{
						Key:   "a",
						Value: "b",
					},
					Operator: influxdb.Equal,
				},
				{
					Tag: influxdb.Tag{
						Key:   "c",
						Value: "d",
					},
					Operator: influxdb.Equal,
				},
			},
			filterTags: []influxdb.Tag{
				{Key: "a", Value: "b"},
				{Key: "c", Value: "X"},
			},
			exp: false,
		},
		{
			name: "Match when tagrule has key value pair that does not match value of same key in filterTags, if tagrule has notEqual operator",
			tagRules: []notification.TagRule{
				{
					Tag: influxdb.Tag{
						Key:   "a",
						Value: "b",
					},
					Operator: influxdb.Equal,
				},
				{
					Tag: influxdb.Tag{
						Key:   "c",
						Value: "d",
					},
					Operator: influxdb.NotEqual,
				},
			},
			filterTags: []influxdb.Tag{
				{Key: "a", Value: "b"},
				{Key: "c", Value: "X"},
			},
			exp: true,
		},
		{
			name:     "Empty tag rule matches filterTags",
			tagRules: []notification.TagRule{},
			filterTags: []influxdb.Tag{
				{Key: "a", Value: "b"},
				{Key: "c", Value: "X"},
			},
			exp: true,
		},
		{
			name: "Non empty tag rule matches empty filter tags",
			tagRules: []notification.TagRule{
				{
					Tag: influxdb.Tag{
						Key:   "c",
						Value: "d",
					},
					Operator: influxdb.NotEqual,
				},
			},
			filterTags: []influxdb.Tag{},
			exp:        true,
		},
		{
			name:       "Empty tag rule matches empty filter tags",
			tagRules:   []notification.TagRule{},
			filterTags: []influxdb.Tag{},
			exp:        true,
		},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {

			r := rule.Base{TagRules: c.tagRules}

			assert.Equal(t, r.MatchesTags(c.filterTags), c.exp, "expected NR tags to be subset of filterTags")
		})
	}
}
