package checks

import (
	"bytes"
	"context"
	"sort"
	"strings"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/influxdata/flux/ast"
	"github.com/influxdata/influxdb/v2"
	"github.com/influxdata/influxdb/v2/kit/platform"
	"github.com/influxdata/influxdb/v2/kit/platform/errors"
	"github.com/influxdata/influxdb/v2/mock"
	"github.com/influxdata/influxdb/v2/notification"
	"github.com/influxdata/influxdb/v2/notification/check"
	"github.com/influxdata/influxdb/v2/task/taskmodel"
	itesting "github.com/influxdata/influxdb/v2/testing"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

const (
	orgOneID = "020f755c3c083000"
	orgTwoID = "020f755c3c083001"

	oneID   = "020f755c3c082000"
	twoID   = "020f755c3c082001"
	threeID = "020f755c3c082002"
	fourID  = "020f755c3c082003"
	fiveID  = "020f755c3c082004"
	sixID   = "020f755c3c082005"

	checkOneID = "020f755c3c082000"
	checkTwoID = "020f755c3c082001"
)

var script = `data = from(bucket: "telegraf") |> range(start: -1m) |> filter(fn: (r) => r._field == "usage_user")`

var deadman1 = &check.Deadman{
	Base: check.Base{
		Name:        "name1",
		ID:          MustIDBase16(checkOneID),
		OrgID:       MustIDBase16(orgOneID),
		OwnerID:     MustIDBase16(sixID),
		Description: "desc1",
		TaskID:      1,
		Query: influxdb.DashboardQuery{
			Text: script,
			BuilderConfig: influxdb.BuilderConfig{
				Buckets: []string{},
				Tags: []struct {
					Key                   string   `json:"key"`
					Values                []string `json:"values"`
					AggregateFunctionType string   `json:"aggregateFunctionType"`
				}{
					{
						Key:                   "_field",
						Values:                []string{"usage_user"},
						AggregateFunctionType: "filter",
					},
				},
				Functions: []struct {
					Name string `json:"name"`
				}{},
			},
		},
		Every:                 mustDuration("1m"),
		StatusMessageTemplate: "msg1",
		Tags: []influxdb.Tag{
			{Key: "k1", Value: "v1"},
			{Key: "k2", Value: "v2"},
		},
		CRUDLog: influxdb.CRUDLog{
			CreatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC),
			UpdatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC),
		},
	},
	TimeSince:  mustDuration("21s"),
	StaleTime:  mustDuration("1h"),
	ReportZero: true,
	Level:      notification.Critical,
}

var threshold1 = &check.Threshold{
	Base: check.Base{
		Name:                  "name2",
		ID:                    MustIDBase16(checkTwoID),
		OrgID:                 MustIDBase16(orgTwoID),
		OwnerID:               MustIDBase16(sixID),
		TaskID:                1,
		Description:           "desc2",
		StatusMessageTemplate: "msg2",
		Every:                 mustDuration("1m"),
		Query: influxdb.DashboardQuery{
			Text: script,
			BuilderConfig: influxdb.BuilderConfig{
				Buckets: []string{},
				Tags: []struct {
					Key                   string   `json:"key"`
					Values                []string `json:"values"`
					AggregateFunctionType string   `json:"aggregateFunctionType"`
				}{},
				Functions: []struct {
					Name string `json:"name"`
				}{},
			},
		},
		Tags: []influxdb.Tag{
			{Key: "k11", Value: "v11"},
		},
		CRUDLog: influxdb.CRUDLog{
			CreatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC),
			UpdatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC),
		},
	},
	Thresholds: []check.ThresholdConfig{
		&check.Lesser{
			ThresholdConfigBase: check.ThresholdConfigBase{
				Level: notification.Ok,
			},
			Value: 1000,
		},
		&check.Greater{
			ThresholdConfigBase: check.ThresholdConfigBase{
				Level: notification.Warn,
			},
			Value: 2000,
		},
		&check.Range{
			ThresholdConfigBase: check.ThresholdConfigBase{
				Level: notification.Info,
			},
			Min:    1500,
			Max:    1900,
			Within: true,
		},
	},
}

var checkCmpOptions = cmp.Options{
	cmp.Comparer(func(x, y []byte) bool {
		return bytes.Equal(x, y)
	}),
	cmpopts.IgnoreFields(check.Base{}, "TaskID"),
	cmp.Transformer("Sort", func(in []influxdb.Check) []influxdb.Check {
		out := append([]influxdb.Check(nil), in...) // Copy input to avoid mutating it
		sort.Slice(out, func(i, j int) bool {
			return out[i].GetID() > out[j].GetID()
		})
		return out
	}),
}

var taskCmpOptions = cmp.Options{
	cmp.Comparer(func(x, y []byte) bool {
		return bytes.Equal(x, y)
	}),
	// skip comparing permissions
	cmpopts.IgnoreFields(
		taskmodel.Task{},
		"LatestCompleted",
		"LatestScheduled",
		"CreatedAt",
		"UpdatedAt",
	),
	cmp.Transformer("Sort", func(in []*taskmodel.Task) []*taskmodel.Task {
		out := append([]*taskmodel.Task{}, in...) // Copy input to avoid mutating it
		sort.Slice(out, func(i, j int) bool {
			return out[i].ID > out[j].ID
		})
		return out
	}),
	cmp.Transformer("FormatFlux", func(in taskmodel.Task) taskmodel.Task {
		newTask := in
		newTask.Flux = itesting.FormatFluxString(&testing.T{}, newTask.Flux)
		return newTask
	}),
}

// CheckFields will include the IDGenerator, and checks
type CheckFields struct {
	IDGenerator   platform.IDGenerator
	TimeGenerator influxdb.TimeGenerator
	TaskService   taskmodel.TaskService
	Checks        []influxdb.Check
	Organizations []*influxdb.Organization
	Tasks         []taskmodel.TaskCreate
}

type checkServiceFactory func(CheckFields, *testing.T) (influxdb.CheckService, taskmodel.TaskService, string, func())

type checkServiceF func(
	init checkServiceFactory,
	t *testing.T,
)

// CheckService tests all the service functions.
func CheckService(
	init checkServiceFactory,
	t *testing.T,
) {
	tests := []struct {
		name string
		fn   checkServiceF
	}{
		{
			name: "CreateCheck",
			fn:   CreateCheck,
		},
		{
			name: "FindCheckByID",
			fn:   FindCheckByID,
		},
		{
			name: "FindChecks",
			fn:   FindChecks,
		},
		{
			name: "FindCheck",
			fn:   FindCheck,
		},
		{
			name: "PatchCheck",
			fn:   PatchCheck,
		},
		{
			name: "UpdateCheck",
			fn:   UpdateCheck,
		},
		{
			name: "DeleteCheck",
			fn:   DeleteCheck,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.fn(init, t)
		})
	}
}

// CreateCheck testing
func CreateCheck(
	init checkServiceFactory,
	t *testing.T,
) {
	type args struct {
		userID platform.ID
		check  influxdb.Check
	}
	type wants struct {
		err    *errors.Error
		checks []influxdb.Check
		tasks  []*taskmodel.Task
	}

	tests := []struct {
		name   string
		fields CheckFields
		args   args
		wants  wants
	}{
		{
			name: "create checks with empty set",
			fields: CheckFields{
				IDGenerator:   mock.NewIDGenerator(checkOneID, t),
				TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)},
				Checks:        []influxdb.Check{},
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
			},
			args: args{
				userID: MustIDBase16(twoID),
				check: &check.Deadman{
					Base: check.Base{
						Name:        "name1",
						OrgID:       MustIDBase16(orgOneID),
						Description: "desc1",
						Query: influxdb.DashboardQuery{
							Text: script,
							BuilderConfig: influxdb.BuilderConfig{
								Tags: []struct {
									Key                   string   `json:"key"`
									Values                []string `json:"values"`
									AggregateFunctionType string   `json:"aggregateFunctionType"`
								}{
									{
										Key:                   "_field",
										Values:                []string{"usage_user"},
										AggregateFunctionType: "filter",
									},
								},
							},
						},
						Every:                 mustDuration("1m"),
						StatusMessageTemplate: "msg1",
						Tags: []influxdb.Tag{
							{Key: "k1", Value: "v1"},
							{Key: "k2", Value: "v2"},
						},
					},
					TimeSince:  mustDuration("21s"),
					StaleTime:  mustDuration("1h"),
					ReportZero: true,
					Level:      notification.Critical,
				},
			},
			wants: wants{
				tasks: []*taskmodel.Task{
					{
						ID:             MustIDBase16("020f755c3c082000"),
						Name:           "name1",
						Type:           "deadman",
						OrganizationID: MustIDBase16("020f755c3c083000"),
						Organization:   "theorg",
						OwnerID:        MustIDBase16("020f755c3c082001"),
						Status:         "active",
						Flux:           "import \"influxdata/influxdb/monitor\"\nimport \"experimental\"\nimport \"influxdata/influxdb/v1\"\n\ndata = from(bucket: \"telegraf\") |> range(start: -1h) |> filter(fn: (r) => r._field == \"usage_user\")\n\noption task = {name: \"name1\", every: 1m}\n\ncheck = {_check_id: \"020f755c3c082000\", _check_name: \"name1\", _type: \"deadman\", tags: {k1: \"v1\", k2: \"v2\"}}\ncrit = (r) => r[\"dead\"]\nmessageFn = (r) => \"msg1\"\n\ndata\n    |> v1[\"fieldsAsCols\"]()\n    |> monitor[\"deadman\"](t: experimental[\"subDuration\"](from: now(), d: 21s))\n    |> monitor[\"check\"](data: check, messageFn: messageFn, crit: crit)\n",
						Every:          "1m",
					},
				},
				checks: []influxdb.Check{
					&check.Deadman{
						Base: check.Base{
							Name:    "name1",
							ID:      MustIDBase16(checkOneID),
							OrgID:   MustIDBase16(orgOneID),
							OwnerID: MustIDBase16(twoID),
							Query: influxdb.DashboardQuery{
								Text: script,
								BuilderConfig: influxdb.BuilderConfig{
									Buckets: []string{},
									Tags: []struct {
										Key                   string   `json:"key"`
										Values                []string `json:"values"`
										AggregateFunctionType string   `json:"aggregateFunctionType"`
									}{
										{
											Key:                   "_field",
											Values:                []string{"usage_user"},
											AggregateFunctionType: "filter",
										},
									},
									Functions: []struct {
										Name string `json:"name"`
									}{},
								},
							},
							Every:                 mustDuration("1m"),
							Description:           "desc1",
							StatusMessageTemplate: "msg1",
							Tags: []influxdb.Tag{
								{Key: "k1", Value: "v1"},
								{Key: "k2", Value: "v2"},
							},
							CRUDLog: influxdb.CRUDLog{
								CreatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC),
								UpdatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC),
							},
						},
						TimeSince:  mustDuration("21s"),
						StaleTime:  mustDuration("1h"),
						ReportZero: true,
						Level:      notification.Critical,
					},
				},
			},
		},
		{
			name: "basic create check",
			fields: CheckFields{
				IDGenerator: &mock.IDGenerator{
					IDFn: func() platform.ID {
						return MustIDBase16(checkTwoID)
					},
				},
				TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)},
				Checks: []influxdb.Check{
					deadman1,
				},
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
					{
						Name: "otherorg",
						ID:   MustIDBase16(orgTwoID),
					},
				},
			},
			args: args{
				userID: MustIDBase16(sixID),
				check: &check.Threshold{
					Base: check.Base{
						Name:                  "name2",
						OrgID:                 MustIDBase16(orgTwoID),
						OwnerID:               MustIDBase16(twoID),
						Description:           "desc2",
						StatusMessageTemplate: "msg2",
						Every:                 mustDuration("1m"),
						Query: influxdb.DashboardQuery{
							Text: script,
						},
						Tags: []influxdb.Tag{
							{Key: "k11", Value: "v11"},
						},
					},
					Thresholds: []check.ThresholdConfig{
						&check.Lesser{
							ThresholdConfigBase: check.ThresholdConfigBase{
								Level: notification.Ok,
							},
							Value: 1000,
						},
						&check.Greater{
							ThresholdConfigBase: check.ThresholdConfigBase{
								Level: notification.Warn,
							},
							Value: 2000,
						},
						&check.Range{
							ThresholdConfigBase: check.ThresholdConfigBase{
								Level: notification.Info,
							},
							Min:    1500,
							Max:    1900,
							Within: true,
						},
					},
				},
			},
			wants: wants{
				checks: []influxdb.Check{
					deadman1,
					threshold1,
				},
				tasks: []*taskmodel.Task{
					{
						ID:             MustIDBase16("020f755c3c082001"),
						Name:           "name2",
						Type:           "threshold",
						OrganizationID: MustIDBase16("020f755c3c083001"),
						Organization:   "otherorg",
						OwnerID:        MustIDBase16("020f755c3c082005"),
						Status:         "active",
						Every:          "1m",
						Flux: `import "influxdata/influxdb/monitor"
import "influxdata/influxdb/v1"

data = from(bucket: "telegraf") |> range(start: -1m) |> filter(fn: (r) => r._field == "usage_user")

option task = {name: "name2", every: 1m}

check = {_check_id: "020f755c3c082001", _check_name: "name2", _type: "threshold", tags: {k11: "v11"}}
ok = (r) => r["usage_user"] < 1000.0
warn = (r) => r["usage_user"] > 2000.0
info = (r) => r["usage_user"] < 1900.0 and r["usage_user"] > 1500.0
messageFn = (r) => "msg2"

data
    |> v1["fieldsAsCols"]()
    |> monitor["check"](
        data: check,
        messageFn: messageFn,
        ok: ok,
        warn: warn,
        info: info,
    )
`,
					},
				},
			},
		},
		{
			name: "names should be unique within an organization",
			fields: CheckFields{
				IDGenerator: &mock.IDGenerator{
					IDFn: func() platform.ID {
						return MustIDBase16(checkTwoID)
					},
				},
				TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)},
				Checks: []influxdb.Check{
					deadman1,
				},
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
					{
						Name: "otherorg",
						ID:   MustIDBase16(orgTwoID),
					},
				},
			},
			args: args{
				userID: MustIDBase16(twoID),
				check: &check.Threshold{
					Base: check.Base{
						Name:        "name1",
						OrgID:       MustIDBase16(orgOneID),
						Description: "desc1",
						Every:       mustDuration("1m"),
						Query: influxdb.DashboardQuery{
							Text: script,
							BuilderConfig: influxdb.BuilderConfig{
								Tags: []struct {
									Key                   string   `json:"key"`
									Values                []string `json:"values"`
									AggregateFunctionType string   `json:"aggregateFunctionType"`
								}{
									{
										Key:                   "_field",
										Values:                []string{"usage_user"},
										AggregateFunctionType: "filter",
									},
								},
							},
						},
						StatusMessageTemplate: "msg1",
						Tags: []influxdb.Tag{
							{Key: "k1", Value: "v1"},
							{Key: "k2", Value: "v2"},
						},
					},
				},
			},
			wants: wants{
				checks: []influxdb.Check{
					deadman1,
				},
				err: &errors.Error{
					Code: errors.EConflict,
					Op:   influxdb.OpCreateCheck,
					Msg:  "check is not unique",
				},
			},
		},
		{
			name: "names should not be unique across organizations",
			fields: CheckFields{
				IDGenerator: &mock.IDGenerator{
					IDFn: func() platform.ID {
						return MustIDBase16(checkTwoID)
					},
				},
				TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)},
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
					{
						Name: "otherorg",
						ID:   MustIDBase16(orgTwoID),
					},
				},
				Checks: []influxdb.Check{
					deadman1,
				},
			},
			args: args{
				userID: MustIDBase16(twoID),
				check: &check.Threshold{
					Base: check.Base{
						Name:        "name1",
						OrgID:       MustIDBase16(orgTwoID),
						Description: "desc2",
						Every:       mustDuration("1m"),
						Query: influxdb.DashboardQuery{
							Text: script,
							BuilderConfig: influxdb.BuilderConfig{
								Tags: []struct {
									Key                   string   `json:"key"`
									Values                []string `json:"values"`
									AggregateFunctionType string   `json:"aggregateFunctionType"`
								}{
									{
										Key:                   "_field",
										Values:                []string{"usage_user"},
										AggregateFunctionType: "filter",
									},
								},
							},
						},
						StatusMessageTemplate: "msg2",
						Tags: []influxdb.Tag{
							{Key: "k11", Value: "v11"},
							{Key: "k22", Value: "v22"},
						},
					},
				},
			},
			wants: wants{
				tasks: []*taskmodel.Task{
					{
						ID:             MustIDBase16("020f755c3c082001"),
						Name:           "name1",
						Type:           "threshold",
						OrganizationID: MustIDBase16("020f755c3c083001"),
						Organization:   "otherorg",
						OwnerID:        MustIDBase16("020f755c3c082001"),
						Status:         "active",
						Every:          "1m",
						Flux:           "import \"influxdata/influxdb/monitor\"\nimport \"influxdata/influxdb/v1\"\n\ndata = from(bucket: \"telegraf\") |> range(start: -1m) |> filter(fn: (r) => r._field == \"usage_user\")\n\noption task = {name: \"name1\", every: 1m}\n\ncheck = {_check_id: \"020f755c3c082001\", _check_name: \"name1\", _type: \"threshold\", tags: {k11: \"v11\", k22: \"v22\"}}\nmessageFn = (r) => \"msg2\"\n\ndata |> v1[\"fieldsAsCols\"]() |> monitor[\"check\"](data: check, messageFn: messageFn)\n",
					},
				},
				checks: []influxdb.Check{
					deadman1,
					&check.Threshold{
						Base: check.Base{
							ID:    MustIDBase16(checkTwoID),
							Name:  "name1",
							OrgID: MustIDBase16(orgTwoID),
							Every: mustDuration("1m"),
							Query: influxdb.DashboardQuery{
								Text: script,
								BuilderConfig: influxdb.BuilderConfig{
									Buckets: []string{},
									Tags: []struct {
										Key                   string   `json:"key"`
										Values                []string `json:"values"`
										AggregateFunctionType string   `json:"aggregateFunctionType"`
									}{
										{
											Key:                   "_field",
											Values:                []string{"usage_user"},
											AggregateFunctionType: "filter",
										},
									},
									Functions: []struct {
										Name string `json:"name"`
									}{},
								},
							},
							OwnerID:               MustIDBase16(twoID),
							Description:           "desc2",
							StatusMessageTemplate: "msg2",
							Tags: []influxdb.Tag{
								{Key: "k11", Value: "v11"},
								{Key: "k22", Value: "v22"},
							},
							CRUDLog: influxdb.CRUDLog{
								CreatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC),
								UpdatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC),
							},
						},
					},
				},
			},
		},
		{
			name: "create check with orgID not exist",
			fields: CheckFields{
				IDGenerator:   mock.NewIDGenerator(checkOneID, t),
				TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)},
				Checks:        []influxdb.Check{},
				Organizations: []*influxdb.Organization{},
			},
			args: args{
				userID: MustIDBase16(twoID),
				check: &check.Threshold{
					Base: check.Base{
						Name:  "name1",
						OrgID: MustIDBase16(orgOneID),
						Every: mustDuration("1m"),
						Query: influxdb.DashboardQuery{
							Text: script,
							BuilderConfig: influxdb.BuilderConfig{
								Tags: []struct {
									Key                   string   `json:"key"`
									Values                []string `json:"values"`
									AggregateFunctionType string   `json:"aggregateFunctionType"`
								}{
									{
										Key:                   "_field",
										Values:                []string{"usage_user"},
										AggregateFunctionType: "filter",
									},
								},
							},
						},
						Description:           "desc2",
						StatusMessageTemplate: "msg2",
						Tags: []influxdb.Tag{
							{Key: "k11", Value: "v11"},
							{Key: "k22", Value: "v22"},
						},
					},
				},
			},
			wants: wants{
				checks: []influxdb.Check{},
				err: &errors.Error{
					Code: errors.ENotFound,
					Msg:  "organization not found",
					Op:   influxdb.OpCreateCheck,
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s, tasks, _, done := init(tt.fields, t)
			defer done()
			ctx := context.Background()
			createCheck := influxdb.CheckCreate{Check: tt.args.check, Status: influxdb.Active}
			err := s.CreateCheck(ctx, createCheck, tt.args.userID)
			influxErrsEqual(t, tt.wants.err, err)

			defer s.DeleteCheck(ctx, tt.args.check.GetID())

			checks, _, err := s.FindChecks(ctx, influxdb.CheckFilter{})
			if err != nil {
				t.Fatalf("failed to retrieve checks: %v", err)
			}
			if diff := cmp.Diff(checks, tt.wants.checks, checkCmpOptions...); diff != "" {
				t.Errorf("checks are different -got/+want\ndiff %s", diff)
			}

			foundTasks, _, err := tasks.FindTasks(ctx, taskmodel.TaskFilter{})
			if err != nil {
				t.Fatal(err)
			}

			if diff := cmp.Diff(foundTasks, tt.wants.tasks, taskCmpOptions); diff != "" {
				t.Errorf("tasks are different -got/+want\ndiff %s", diff)
			}
		})
	}
}

// FindCheckByID testing
func FindCheckByID(
	init checkServiceFactory,
	t *testing.T,
) {
	type args struct {
		id platform.ID
	}
	type wants struct {
		err   *errors.Error
		check influxdb.Check
	}

	tests := []struct {
		name   string
		fields CheckFields
		args   args
		wants  wants
	}{
		{
			name: "basic find check by id",
			fields: CheckFields{
				Checks: []influxdb.Check{
					deadman1,
					threshold1,
				},
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
			},
			args: args{
				id: MustIDBase16(checkTwoID),
			},
			wants: wants{
				check: threshold1,
			},
		},
		{
			name: "find check by id not exist",
			fields: CheckFields{
				Checks: []influxdb.Check{
					deadman1,
					threshold1,
				},
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
			},
			args: args{
				id: MustIDBase16(threeID),
			},
			wants: wants{
				err: &errors.Error{
					Code: errors.ENotFound,
					Op:   influxdb.OpFindCheckByID,
					Msg:  "check not found",
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s, _, _, done := init(tt.fields, t)
			defer done()
			ctx := context.Background()

			check, err := s.FindCheckByID(ctx, tt.args.id)
			influxErrsEqual(t, tt.wants.err, err)

			if diff := cmp.Diff(check, tt.wants.check, checkCmpOptions...); diff != "" {
				t.Errorf("check is different -got/+want\ndiff %s", diff)
			}
		})
	}
}

// FindChecks testing
func FindChecks(
	init checkServiceFactory,
	t *testing.T,
) {
	type args struct {
		ID           platform.ID
		name         string
		organization string
		OrgID        platform.ID
		userID       platform.ID
		findOptions  influxdb.FindOptions
	}

	type wants struct {
		checks []influxdb.Check
		err    error
	}
	tests := []struct {
		name   string
		fields CheckFields
		args   args
		wants  wants
	}{
		{
			name: "find all checks",
			fields: CheckFields{
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
					{
						Name: "otherorg",
						ID:   MustIDBase16(orgTwoID),
					},
				},
				Checks: []influxdb.Check{
					deadman1,
					threshold1,
				},
			},
			args: args{
				userID: MustIDBase16(sixID),
			},
			wants: wants{
				checks: []influxdb.Check{
					deadman1,
					threshold1,
				},
			},
		},
		{
			name: "find all checks by offset and limit",
			fields: CheckFields{
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Checks: []influxdb.Check{
					deadman1,
					threshold1,
				},
			},
			args: args{
				findOptions: influxdb.FindOptions{
					Offset: 1,
					Limit:  1,
				},
			},
			wants: wants{
				checks: []influxdb.Check{
					threshold1,
				},
			},
		},
		{
			name: "find all checks by descending",
			fields: CheckFields{
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Checks: []influxdb.Check{
					deadman1,
					threshold1,
				},
			},
			args: args{
				userID: MustIDBase16(sixID),
				findOptions: influxdb.FindOptions{
					Limit:      1,
					Descending: true,
				},
			},
			wants: wants{
				checks: []influxdb.Check{
					threshold1,
				},
			},
		},
		{
			name: "find checks by organization name",
			fields: CheckFields{
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
					{
						Name: "otherorg",
						ID:   MustIDBase16(orgTwoID),
					},
				},
				Checks: []influxdb.Check{
					deadman1,
					threshold1,
				},
			},
			args: args{
				userID:       MustIDBase16(sixID),
				organization: "theorg",
			},
			wants: wants{
				checks: []influxdb.Check{
					deadman1,
				},
			},
		},
		{
			name: "find checks by organization id",
			fields: CheckFields{
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
					{
						Name: "otherorg",
						ID:   MustIDBase16(orgTwoID),
					},
				},
				Checks: []influxdb.Check{
					deadman1,
					threshold1,
				},
			},
			args: args{
				userID: MustIDBase16(sixID),
				OrgID:  MustIDBase16(orgTwoID),
			},
			wants: wants{
				checks: []influxdb.Check{
					threshold1,
				},
			},
		},
		{
			name: "find check by name",
			fields: CheckFields{
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Checks: []influxdb.Check{
					deadman1,
					threshold1,
				},
			},
			args: args{
				userID: MustIDBase16(sixID),
				name:   "name2",
			},
			wants: wants{
				checks: []influxdb.Check{
					threshold1,
				},
			},
		},
		{
			name: "missing check returns no checks",
			fields: CheckFields{
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Checks: []influxdb.Check{},
			},
			args: args{
				userID: MustIDBase16(sixID),
				name:   "xyz",
			},
			wants: wants{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s, _, opPrefix, done := init(tt.fields, t)
			defer done()
			ctx := context.Background()

			filter := influxdb.CheckFilter{}
			if tt.args.ID.Valid() {
				filter.ID = &tt.args.ID
			}
			if tt.args.OrgID.Valid() {
				filter.OrgID = &tt.args.OrgID
			}
			if tt.args.organization != "" {
				filter.Org = &tt.args.organization
			}
			if tt.args.name != "" {
				filter.Name = &tt.args.name
			}

			checks, _, err := s.FindChecks(ctx, filter, tt.args.findOptions)
			diffPlatformErrors(tt.name, err, tt.wants.err, opPrefix, t)

			if diff := cmp.Diff(checks, tt.wants.checks, checkCmpOptions...); diff != "" {
				t.Errorf("checks are different -got/+want\ndiff %s", diff)
			}
		})
	}
}

// DeleteCheck testing
func DeleteCheck(
	init checkServiceFactory,
	t *testing.T,
) {
	type args struct {
		ID     string
		userID platform.ID
	}
	type wants struct {
		err    *errors.Error
		checks []influxdb.Check
	}

	tests := []struct {
		name   string
		fields CheckFields
		args   args
		wants  wants
	}{
		{
			name: "delete checks using exist id",
			fields: CheckFields{
				IDGenerator: mock.NewIDGenerator("0000000000000001", t),
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Tasks: []taskmodel.TaskCreate{
					{
						Flux: `option task = { every: 10s, name: "foo" }
data = from(bucket: "telegraf") |> range(start: -1m)`,
						OrganizationID: MustIDBase16(orgOneID),
						OwnerID:        MustIDBase16(sixID),
					},
				},
				Checks: []influxdb.Check{
					deadman1,
					threshold1,
				},
			},
			args: args{
				ID:     checkOneID,
				userID: MustIDBase16(sixID),
			},
			wants: wants{
				checks: []influxdb.Check{
					threshold1,
				},
			},
		},
		{
			name: "delete checks using id that does not exist",
			fields: CheckFields{
				IDGenerator: mock.NewIDGenerator("0000000000000001", t),
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Tasks: []taskmodel.TaskCreate{
					{
						Flux: `option task = { every: 10s, name: "foo" }
		data = from(bucket: "telegraf") |> range(start: -1m)`,
						OrganizationID: MustIDBase16(orgOneID),
						OwnerID:        MustIDBase16(sixID),
					},
				},
				Checks: []influxdb.Check{
					deadman1,
					threshold1,
				},
			},
			args: args{
				ID:     "1234567890654321",
				userID: MustIDBase16(sixID),
			},
			wants: wants{
				err: &errors.Error{
					Op:   influxdb.OpDeleteCheck,
					Msg:  "check not found",
					Code: errors.ENotFound,
				},
				checks: []influxdb.Check{
					deadman1,
					threshold1,
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s, _, _, done := init(tt.fields, t)
			defer done()
			ctx := context.Background()
			err := s.DeleteCheck(ctx, MustIDBase16(tt.args.ID))
			influxErrsEqual(t, tt.wants.err, err)

			filter := influxdb.CheckFilter{}
			checks, _, err := s.FindChecks(ctx, filter)
			if err != nil {
				t.Fatalf("failed to retrieve checks: %v", err)
			}
			if diff := cmp.Diff(checks, tt.wants.checks, checkCmpOptions...); diff != "" {
				t.Errorf("checks are different -got/+want\ndiff %s", diff)
			}
		})
	}
}

// FindCheck testing
func FindCheck(
	init checkServiceFactory,
	t *testing.T,
) {
	type args struct {
		name  string
		OrgID platform.ID
	}

	type wants struct {
		check influxdb.Check
		err   *errors.Error
	}

	tests := []struct {
		name   string
		fields CheckFields
		args   args
		wants  wants
	}{
		{
			name: "find check by name",
			fields: CheckFields{
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
					{
						Name: "theorg2",
						ID:   MustIDBase16(orgTwoID),
					},
				},
				Checks: []influxdb.Check{
					deadman1,
					threshold1,
				},
			},
			args: args{
				name:  "name1",
				OrgID: MustIDBase16(orgOneID),
			},
			wants: wants{
				check: deadman1,
			},
		},
		{
			name: "mixed filter",
			fields: CheckFields{
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Checks: []influxdb.Check{},
			},
			args: args{
				name:  "name2",
				OrgID: MustIDBase16(orgOneID),
			},
			wants: wants{
				err: &errors.Error{
					Code: errors.ENotFound,
					Op:   influxdb.OpFindCheck,
					Msg:  "check not found",
				},
			},
		},
		{
			name: "missing check returns error",
			fields: CheckFields{
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Checks: []influxdb.Check{},
			},
			args: args{
				name:  "xyz",
				OrgID: MustIDBase16(orgOneID),
			},
			wants: wants{
				err: &errors.Error{
					Code: errors.ENotFound,
					Op:   influxdb.OpFindCheck,
					Msg:  "check not found",
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s, _, _, done := init(tt.fields, t)
			defer done()

			var filter influxdb.CheckFilter
			if tt.args.name != "" {
				filter.Name = &tt.args.name
			}
			if tt.args.OrgID.Valid() {
				filter.OrgID = &tt.args.OrgID
			}

			check, err := s.FindCheck(context.Background(), filter)
			influxErrsEqual(t, tt.wants.err, err)

			if diff := cmp.Diff(check, tt.wants.check, checkCmpOptions...); diff != "" {
				t.Errorf("checks are different -got/+want\ndiff %s", diff)
			}
		})
	}
}

// UpdateCheck testing
func UpdateCheck(
	init checkServiceFactory,
	t *testing.T,
) {
	type args struct {
		id    platform.ID
		check influxdb.Check
	}
	type wants struct {
		err   error
		check influxdb.Check
	}

	tests := []struct {
		name   string
		fields CheckFields
		args   args
		wants  wants
	}{
		{
			name: "mixed update",
			fields: CheckFields{
				IDGenerator:   mock.NewIDGenerator("0000000000000001", t),
				TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2007, 5, 4, 1, 2, 3, 0, time.UTC)},
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Tasks: []taskmodel.TaskCreate{
					{
						Flux: `option task = { every: 10s, name: "foo" }
data = from(bucket: "telegraf") |> range(start: -1m)`,
						OrganizationID: MustIDBase16(orgOneID),
						OwnerID:        MustIDBase16(sixID),
					},
				},
				Checks: []influxdb.Check{
					deadman1,
				},
			},
			args: args{
				id: MustIDBase16(checkOneID),
				check: &check.Threshold{
					Base: check.Base{
						ID:      MustIDBase16(checkTwoID),
						OrgID:   MustIDBase16(orgOneID),
						OwnerID: MustIDBase16(twoID),
						Every:   mustDuration("1m"),
						Query: influxdb.DashboardQuery{
							Text: script,
							BuilderConfig: influxdb.BuilderConfig{
								Tags: []struct {
									Key                   string   `json:"key"`
									Values                []string `json:"values"`
									AggregateFunctionType string   `json:"aggregateFunctionType"`
								}{
									{
										Key:                   "_field",
										Values:                []string{"usage_user"},
										AggregateFunctionType: "filter",
									},
								},
							},
						},
						Name:                  "changed",
						Description:           "desc changed",
						StatusMessageTemplate: "msg2",
						TaskID:                1,
						Tags: []influxdb.Tag{
							{Key: "k11", Value: "v11"},
							{Key: "k22", Value: "v22"},
							{Key: "k33", Value: "v33"},
						},
						CRUDLog: influxdb.CRUDLog{
							CreatedAt: time.Date(2001, 5, 4, 1, 2, 3, 0, time.UTC),
							UpdatedAt: time.Date(2002, 5, 4, 1, 2, 3, 0, time.UTC),
						},
					},
					Thresholds: []check.ThresholdConfig{
						&check.Lesser{
							ThresholdConfigBase: check.ThresholdConfigBase{
								Level: notification.Ok,
							},
							Value: 1000,
						},
						&check.Greater{
							ThresholdConfigBase: check.ThresholdConfigBase{
								Level: notification.Warn,
							},
							Value: 2000,
						},
						&check.Range{
							ThresholdConfigBase: check.ThresholdConfigBase{
								Level: notification.Info,
							},
							Min:    1500,
							Max:    1900,
							Within: true,
						},
					},
				},
			},
			wants: wants{
				check: &check.Threshold{
					Base: check.Base{
						ID:      MustIDBase16(checkOneID),
						OrgID:   MustIDBase16(orgOneID),
						Name:    "changed",
						Every:   mustDuration("1m"),
						OwnerID: MustIDBase16(sixID),
						Query: influxdb.DashboardQuery{
							Text: script,
							BuilderConfig: influxdb.BuilderConfig{
								Tags: []struct {
									Key                   string   `json:"key"`
									Values                []string `json:"values"`
									AggregateFunctionType string   `json:"aggregateFunctionType"`
								}{
									{
										Key:                   "_field",
										Values:                []string{"usage_user"},
										AggregateFunctionType: "filter",
									},
								},
							},
						},
						Description:           "desc changed",
						StatusMessageTemplate: "msg2",
						Tags: []influxdb.Tag{
							{Key: "k11", Value: "v11"},
							{Key: "k22", Value: "v22"},
							{Key: "k33", Value: "v33"},
						},
						TaskID: 1,
						CRUDLog: influxdb.CRUDLog{
							CreatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC),
							UpdatedAt: time.Date(2007, 5, 4, 1, 2, 3, 0, time.UTC),
						},
					},
					Thresholds: []check.ThresholdConfig{
						&check.Lesser{
							ThresholdConfigBase: check.ThresholdConfigBase{
								Level: notification.Ok,
							},
							Value: 1000,
						},
						&check.Greater{
							ThresholdConfigBase: check.ThresholdConfigBase{
								Level: notification.Warn,
							},
							Value: 2000,
						},
						&check.Range{
							ThresholdConfigBase: check.ThresholdConfigBase{
								Level: notification.Info,
							},
							Min:    1500,
							Max:    1900,
							Within: true,
						},
					},
				},
			},
		},
		{
			name: "update name unique",
			fields: CheckFields{
				TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)},
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Checks: []influxdb.Check{
					deadman1,
					&check.Deadman{
						Base: check.Base{
							ID:    MustIDBase16(checkTwoID),
							OrgID: MustIDBase16(orgOneID),
							Every: mustDuration("1m"),
							Query: influxdb.DashboardQuery{
								Text: script,
								BuilderConfig: influxdb.BuilderConfig{
									Tags: []struct {
										Key                   string   `json:"key"`
										Values                []string `json:"values"`
										AggregateFunctionType string   `json:"aggregateFunctionType"`
									}{
										{
											Key:                   "_field",
											Values:                []string{"usage_user"},
											AggregateFunctionType: "filter",
										},
									},
								},
							},
							TaskID:                1,
							Name:                  "check2",
							OwnerID:               MustIDBase16(twoID),
							StatusMessageTemplate: "msg1",
						},
					},
				},
			},
			args: args{
				id: MustIDBase16(checkOneID),
				check: &check.Deadman{
					Base: check.Base{
						OrgID:                 MustIDBase16(orgOneID),
						OwnerID:               MustIDBase16(twoID),
						Name:                  "check2",
						Description:           "desc changed",
						TaskID:                1,
						Every:                 mustDuration("1m"),
						StatusMessageTemplate: "msg2",
						Tags: []influxdb.Tag{
							{Key: "k11", Value: "v11"},
							{Key: "k22", Value: "v22"},
							{Key: "k33", Value: "v33"},
						},
						CRUDLog: influxdb.CRUDLog{
							CreatedAt: time.Date(2001, 5, 4, 1, 2, 3, 0, time.UTC),
							UpdatedAt: time.Date(2002, 5, 4, 1, 2, 3, 0, time.UTC),
						},
					},
					TimeSince:  mustDuration("12s"),
					StaleTime:  mustDuration("1h"),
					ReportZero: false,
					Level:      notification.Warn,
				},
			},
			wants: wants{
				err: &errors.Error{
					Code: errors.EConflict,
					Msg:  "check name is not unique",
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s, _, opPrefix, done := init(tt.fields, t)
			defer done()
			ctx := context.Background()

			checkCreate := influxdb.CheckCreate{Check: tt.args.check, Status: influxdb.Active}

			check, err := s.UpdateCheck(ctx, tt.args.id, checkCreate)
			diffPlatformErrors(tt.name, err, tt.wants.err, opPrefix, t)

			if diff := cmp.Diff(check, tt.wants.check, checkCmpOptions...); diff != "" {
				t.Errorf("check is different -got/+want\ndiff %s", diff)
			}
		})
	}
}

// PatchCheck testing
func PatchCheck(
	init checkServiceFactory,
	t *testing.T,
) {
	type args struct {
		id  platform.ID
		upd influxdb.CheckUpdate
	}
	type wants struct {
		err   *errors.Error
		check influxdb.Check
	}

	inactive := influxdb.Inactive

	tests := []struct {
		name   string
		fields CheckFields
		args   args
		wants  wants
	}{
		{
			name: "mixed patch",
			fields: CheckFields{
				IDGenerator:   mock.NewIDGenerator("0000000000000001", t),
				TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2007, 5, 4, 1, 2, 3, 0, time.UTC)},
				Tasks: []taskmodel.TaskCreate{
					{
						Flux: `option task = { every: 10s, name: "foo" }
data = from(bucket: "telegraf") |> range(start: -1m)`,
						OrganizationID: MustIDBase16(orgOneID),
						OwnerID:        MustIDBase16(sixID),
					},
				},
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Checks: []influxdb.Check{
					deadman1,
				},
			},
			args: args{
				id: MustIDBase16(checkOneID),
				upd: influxdb.CheckUpdate{
					Name:        strPtr("changed"),
					Description: strPtr("desc changed"),
					Status:      &inactive,
				},
			},
			wants: wants{
				check: &check.Deadman{
					Base: check.Base{
						ID:          MustIDBase16(checkOneID),
						OrgID:       MustIDBase16(orgOneID),
						Name:        "changed",
						OwnerID:     MustIDBase16(sixID),
						Every:       mustDuration("1m"),
						Description: "desc changed",
						Query: influxdb.DashboardQuery{
							Text: script,
							BuilderConfig: influxdb.BuilderConfig{
								Buckets: []string{},
								Tags: []struct {
									Key                   string   `json:"key"`
									Values                []string `json:"values"`
									AggregateFunctionType string   `json:"aggregateFunctionType"`
								}{
									{
										Key:                   "_field",
										Values:                []string{"usage_user"},
										AggregateFunctionType: "filter",
									},
								},
								Functions: []struct {
									Name string `json:"name"`
								}{},
							},
						},
						StatusMessageTemplate: "msg1",
						Tags: []influxdb.Tag{
							{Key: "k1", Value: "v1"},
							{Key: "k2", Value: "v2"},
						},
						CRUDLog: influxdb.CRUDLog{
							CreatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC),
							UpdatedAt: time.Date(2007, 5, 4, 1, 2, 3, 0, time.UTC),
						},
					},
					TimeSince:  mustDuration("21s"),
					StaleTime:  mustDuration("1h"),
					ReportZero: true,
					Level:      notification.Critical,
				},
			},
		},
		{
			name: "update name unique",
			fields: CheckFields{
				TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)},
				Organizations: []*influxdb.Organization{
					{
						Name: "theorg",
						ID:   MustIDBase16(orgOneID),
					},
				},
				Checks: []influxdb.Check{
					deadman1,
					&check.Deadman{
						Base: check.Base{
							ID:      MustIDBase16(checkTwoID),
							OrgID:   MustIDBase16(orgOneID),
							Every:   mustDuration("1m"),
							Name:    "check2",
							OwnerID: MustIDBase16(sixID),
							Query: influxdb.DashboardQuery{
								Text: script,
								BuilderConfig: influxdb.BuilderConfig{
									Tags: []struct {
										Key                   string   `json:"key"`
										Values                []string `json:"values"`
										AggregateFunctionType string   `json:"aggregateFunctionType"`
									}{
										{
											Key:                   "_field",
											Values:                []string{"usage_user"},
											AggregateFunctionType: "filter",
										},
									},
								},
							},
							StatusMessageTemplate: "msg1",
						},
					},
				},
			},
			args: args{
				id: MustIDBase16(checkOneID),
				upd: influxdb.CheckUpdate{
					Name: strPtr("check2"),
				},
			},
			wants: wants{
				err: &errors.Error{
					Code: errors.EConflict,
					Msg:  "check entity update conflicts with an existing entity",
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s, _, _, done := init(tt.fields, t)
			defer done()
			ctx := context.Background()

			check, err := s.PatchCheck(ctx, tt.args.id, tt.args.upd)
			influxErrsEqual(t, tt.wants.err, err)

			if diff := cmp.Diff(check, tt.wants.check, checkCmpOptions...); diff != "" {
				t.Errorf("check is different -got/+want\ndiff %s", diff)
			}
		})
	}
}

// MustIDBase16 is an helper to ensure a correct ID is built during testing.
func MustIDBase16(s string) platform.ID {
	id, err := platform.IDFromString(s)
	if err != nil {
		panic(err)
	}
	return *id
}

func diffPlatformErrors(name string, actual, expected error, opPrefix string, t *testing.T) {
	t.Helper()
	ErrorsEqual(t, actual, expected)
}

// ErrorsEqual checks to see if the provided errors are equivalent.
func ErrorsEqual(t *testing.T, actual, expected error) {
	t.Helper()
	if expected == nil && actual == nil {
		return
	}

	if expected == nil && actual != nil {
		t.Errorf("unexpected error %s", actual.Error())
	}

	if expected != nil && actual == nil {
		t.Errorf("expected error %s but received nil", expected.Error())
	}

	if errors.ErrorCode(expected) != errors.ErrorCode(actual) {
		t.Logf("\nexpected: %v\nactual: %v\n\n", expected, actual)
		t.Errorf("expected error code %q but received %q", errors.ErrorCode(expected), errors.ErrorCode(actual))
	}

	if errors.ErrorMessage(expected) != errors.ErrorMessage(actual) {
		t.Logf("\nexpected: %v\nactual: %v\n\n", expected, actual)
		t.Errorf("expected error message %q but received %q", errors.ErrorMessage(expected), errors.ErrorMessage(actual))
	}
}

func influxErrsEqual(t *testing.T, expected *errors.Error, actual error) {
	t.Helper()

	if expected != nil {
		require.Error(t, actual)
	}

	if actual == nil {
		return
	}

	if expected == nil {
		require.NoError(t, actual)
		return
	}
	iErr, ok := actual.(*errors.Error)
	require.True(t, ok)
	assert.Equal(t, expected.Code, iErr.Code)
	assert.Truef(t, strings.HasPrefix(iErr.Error(), expected.Error()), "expected: %s got err: %s", expected.Error(), actual.Error())
}

func mustDuration(d string) *notification.Duration {
	dur, err := time.ParseDuration(d)
	if err != nil {
		panic(err)
	}

	ndur, err := notification.FromTimeDuration(dur)
	if err != nil {
		panic(err)
	}

	// Filter out the zero values from the duration.
	durs := make([]ast.Duration, 0, len(ndur.Values))
	for _, d := range ndur.Values {
		if d.Magnitude != 0 {
			durs = append(durs, d)
		}
	}
	ndur.Values = durs
	return &ndur
}
