package pkger

import (
	"bytes"
	"errors"
	"fmt"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"runtime"
	"sort"
	"strconv"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/influxdata/influxdb/v2"
	errors2 "github.com/influxdata/influxdb/v2/kit/platform/errors"
	"github.com/influxdata/influxdb/v2/notification"
	icheck "github.com/influxdata/influxdb/v2/notification/check"
	"github.com/influxdata/influxdb/v2/notification/endpoint"
	"github.com/influxdata/influxdb/v2/task/taskmodel"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestParse(t *testing.T) {
	t.Run("template with a bucket", func(t *testing.T) {
		t.Run("with valid bucket template should be valid", func(t *testing.T) {
			testfileRunner(t, "testdata/bucket", func(t *testing.T, template *Template) {
				buckets := template.Summary().Buckets
				require.Len(t, buckets, 2)

				actual := buckets[0]
				expectedBucket := SummaryBucket{
					SummaryIdentifier: SummaryIdentifier{
						Kind:          KindBucket,
						MetaName:      "rucket-11",
						EnvReferences: []SummaryReference{},
					},
					Name:              "rucket-11",
					Description:       "bucket 1 description",
					RetentionPeriod:   time.Hour,
					LabelAssociations: []SummaryLabel{},
				}
				assert.Equal(t, expectedBucket, actual)

				actual = buckets[1]
				expectedBucket = SummaryBucket{
					SummaryIdentifier: SummaryIdentifier{
						Kind:          KindBucket,
						MetaName:      "rucket-22",
						EnvReferences: []SummaryReference{},
					},
					Name:              "display name",
					Description:       "bucket 2 description",
					LabelAssociations: []SummaryLabel{},
				}
				assert.Equal(t, expectedBucket, actual)
			})
		})

		t.Run("with valid bucket and schema should be valid", func(t *testing.T) {
			template := validParsedTemplateFromFile(t, "testdata/bucket_schema.yml", EncodingYAML)
			buckets := template.Summary().Buckets
			require.Len(t, buckets, 1)

			exp := SummaryBucket{
				SummaryIdentifier: SummaryIdentifier{
					Kind:          KindBucket,
					MetaName:      "explicit-11",
					EnvReferences: []SummaryReference{},
				},
				Name:              "my_explicit",
				SchemaType:        "explicit",
				LabelAssociations: []SummaryLabel{},
				MeasurementSchemas: []SummaryMeasurementSchema{
					{
						Name: "cpu",
						Columns: []SummaryMeasurementSchemaColumn{
							{Name: "host", Type: "tag"},
							{Name: "time", Type: "timestamp"},
							{Name: "usage_user", Type: "field", DataType: "float"},
						},
					},
				},
			}

			assert.Equal(t, exp, buckets[0])
		})

		t.Run("with env refs should be valid", func(t *testing.T) {
			testfileRunner(t, "testdata/bucket_ref.yml", func(t *testing.T, template *Template) {
				actual := template.Summary().Buckets
				require.Len(t, actual, 1)

				expectedEnvRefs := []SummaryReference{
					{
						Field:        "metadata.name",
						EnvRefKey:    "meta-name",
						DefaultValue: "meta",
					},
					{
						Field:        "spec.name",
						EnvRefKey:    "spec-name",
						DefaultValue: "spectacles",
					},
					{
						Field:     "spec.associations[0].name",
						EnvRefKey: "label-meta-name",
					},
				}
				assert.Equal(t, expectedEnvRefs, actual[0].EnvReferences)
			})
		})

		t.Run("should handle bad config", func(t *testing.T) {
			tests := []testTemplateResourceError{
				{
					name:           "missing name",
					validationErrs: 1,
					valFields:      []string{fieldMetadata, fieldName},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Bucket
metadata:
spec:
`,
				},
				{
					name:           "mixed valid and missing name",
					validationErrs: 1,
					valFields:      []string{fieldMetadata, fieldName},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Bucket
metadata:
  name:  rucket-11
---
apiVersion: influxdata.com/v2alpha1
kind: Bucket
metadata:
spec:
`,
				},
				{
					name:           "mixed valid and multiple bad names",
					resourceErrs:   2,
					validationErrs: 1,
					valFields:      []string{fieldMetadata, fieldName},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Bucket
metadata:
  name:  rucket-11
---
apiVersion: influxdata.com/v2alpha1
kind: Bucket
metadata:
spec:
---
apiVersion: influxdata.com/v2alpha1
kind: Bucket
metadata:
spec:
`,
				},
				{
					name:           "duplicate bucket names",
					resourceErrs:   1,
					validationErrs: 1,
					valFields:      []string{fieldMetadata, fieldName},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Bucket
metadata:
  name:  valid-name
---
apiVersion: influxdata.com/v2alpha1
kind: Bucket
metadata:
  name:  valid-name
`,
				},
				{
					name:           "duplicate meta name and spec name",
					resourceErrs:   1,
					validationErrs: 1,
					valFields:      []string{fieldSpec, fieldName},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Bucket
metadata:
  name:  rucket-1
---
apiVersion: influxdata.com/v2alpha1
kind: Bucket
metadata:
  name:  valid-name
spec:
  name:  rucket-1
`,
				},
				{
					name:           "spec name too short",
					resourceErrs:   1,
					validationErrs: 1,
					valFields:      []string{fieldSpec, fieldName},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Bucket
metadata:
  name:  rucket-1
---
apiVersion: influxdata.com/v2alpha1
kind: Bucket
metadata:
  name:  invalid-name
spec:
  name:  f
`,
				},
				{
					name:           "invalid measurement name",
					resourceErrs:   1,
					validationErrs: 1,
					valFields:      []string{strings.Join([]string{fieldSpec, fieldMeasurementSchemas}, ".")},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Bucket
metadata:
  name: foo-1
spec:
  name: foo
  schemaType: explicit
  measurementSchemas:
    - name: _cpu
      columns: 
        - name: time
          type: timestamp
        - name: usage_user
          type: field
          dataType: float
`,
				},
				{
					name:           "invalid semantic type",
					resourceErrs:   1,
					validationErrs: 1,
					valFields:      []string{strings.Join([]string{fieldSpec, fieldMeasurementSchemas, fieldMeasurementSchemaColumns}, ".")},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Bucket
metadata:
  name: foo-1
spec:
  name: foo
  schemaType: explicit
  measurementSchemas:
    - name: _cpu
      columns: 
        - name: time
          type: field
        - name: usage_user
          type: field
          dataType: float
`,
				},
				{
					name:           "missing time column",
					resourceErrs:   1,
					validationErrs: 1,
					valFields:      []string{strings.Join([]string{fieldSpec, fieldMeasurementSchemas}, ".")},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Bucket
metadata:
  name: foo-1
spec:
  name: foo
  schemaType: explicit
  measurementSchemas:
    - name: cpu
      columns: 
        - name: usage_user
          type: field
          dataType: float
`,
				},
			}

			for _, tt := range tests {
				testTemplateErrors(t, KindBucket, tt)
			}
		})
	})

	t.Run("template with a label", func(t *testing.T) {
		t.Run("with valid label template should be valid", func(t *testing.T) {
			testfileRunner(t, "testdata/label", func(t *testing.T, template *Template) {
				labels := template.Summary().Labels
				require.Len(t, labels, 3)

				expectedLabel := sumLabelGen("label-1", "label-1", "#FFFFFF", "label 1 description")
				assert.Equal(t, expectedLabel, labels[0])

				expectedLabel = sumLabelGen("label-2", "label-2", "#000000", "label 2 description")
				assert.Equal(t, expectedLabel, labels[1])

				expectedLabel = sumLabelGen("label-3", "display name", "", "label 3 description")
				assert.Equal(t, expectedLabel, labels[2])
			})
		})

		t.Run("with env refs should be valid", func(t *testing.T) {
			testfileRunner(t, "testdata/label_ref.yml", func(t *testing.T, template *Template) {
				actual := template.Summary().Labels
				require.Len(t, actual, 1)

				expected := sumLabelGen("env-meta-name", "env-spec-name", "", "",
					SummaryReference{
						Field:     "metadata.name",
						EnvRefKey: "meta-name",
					},
					SummaryReference{
						Field:     "spec.name",
						EnvRefKey: "spec-name",
					},
				)
				assert.Contains(t, actual, expected)
			})
		})

		t.Run("with missing label name should error", func(t *testing.T) {
			tests := []testTemplateResourceError{
				{
					name:           "missing name",
					validationErrs: 1,
					valFields:      []string{fieldMetadata, fieldName},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Label
metadata:
spec:
`,
				},
				{
					name:           "mixed valid and missing name",
					validationErrs: 1,
					valFields:      []string{fieldMetadata, fieldName},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Label
metadata:
  name: valid-name
spec:
---
apiVersion: influxdata.com/v2alpha1
kind: Label
metadata:
spec:
`,
				},
				{
					name:           "duplicate names",
					validationErrs: 1,
					valFields:      []string{fieldMetadata, fieldName},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Label
metadata:
  name: valid-name
spec:
---
apiVersion: influxdata.com/v2alpha1
kind: Label
metadata:
  name: valid-name
spec:
`,
				},
				{
					name:           "multiple labels with missing name",
					resourceErrs:   2,
					validationErrs: 1,
					valFields:      []string{fieldMetadata, fieldName},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Label
---
apiVersion: influxdata.com/v2alpha1
kind: Label
`,
				},
				{
					name:           "duplicate meta name and spec name",
					validationErrs: 1,
					valFields:      []string{fieldSpec, fieldName},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Label
metadata:
  name: valid-name
spec:
---
apiVersion: influxdata.com/v2alpha1
kind: Label
metadata:
  name: label-1
spec:
  name: valid-name
`,
				},
				{
					name:           "spec name to short",
					validationErrs: 1,
					valFields:      []string{fieldSpec, fieldName},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Label
metadata:
  name: valid-name
spec:
---
apiVersion: influxdata.com/v2alpha1
kind: Label
metadata:
  name: label-1
spec:
  name: a
`,
				},
			}

			for _, tt := range tests {
				testTemplateErrors(t, KindLabel, tt)
			}
		})
	})

	t.Run("template with buckets and labels associated", func(t *testing.T) {
		t.Run("happy path", func(t *testing.T) {
			testfileRunner(t, "testdata/bucket_associates_label", func(t *testing.T, template *Template) {
				sum := template.Summary()
				require.Len(t, sum.Labels, 2)

				bkts := sum.Buckets
				require.Len(t, bkts, 3)

				expectedLabels := []struct {
					bktName string
					labels  []string
				}{
					{
						bktName: "rucket-1",
						labels:  []string{"label-1"},
					},
					{
						bktName: "rucket-2",
						labels:  []string{"label-2"},
					},
					{
						bktName: "rucket-3",
						labels:  []string{"label-1", "label-2"},
					},
				}
				for i, expected := range expectedLabels {
					bkt := bkts[i]
					require.Len(t, bkt.LabelAssociations, len(expected.labels))

					for j, label := range expected.labels {
						assert.Equal(t, label, bkt.LabelAssociations[j].Name)
					}
				}

				expectedMappings := []SummaryLabelMapping{
					{
						ResourceMetaName: "rucket-1",
						ResourceName:     "rucket-1",
						LabelMetaName:    "label-1",
						LabelName:        "label-1",
					},
					{
						ResourceMetaName: "rucket-2",
						ResourceName:     "rucket-2",
						LabelMetaName:    "label-2",
						LabelName:        "label-2",
					},
					{
						ResourceMetaName: "rucket-3",
						ResourceName:     "rucket-3",
						LabelMetaName:    "label-1",
						LabelName:        "label-1",
					},
					{
						ResourceMetaName: "rucket-3",
						ResourceName:     "rucket-3",
						LabelMetaName:    "label-2",
						LabelName:        "label-2",
					},
				}

				for _, expectedMapping := range expectedMappings {
					expectedMapping.Status = StateStatusNew
					expectedMapping.ResourceType = influxdb.BucketsResourceType
					assert.Contains(t, sum.LabelMappings, expectedMapping)
				}
			})
		})

		t.Run("association doesn't exist then provides an error", func(t *testing.T) {
			tests := []testTemplateResourceError{
				{
					name:    "no labels provided",
					assErrs: 1,
					assIdxs: []int{0},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Bucket
metadata:
  name: rucket-1
spec:
  associations:
    - kind: Label
      name: label-1
`,
				},
				{
					name:    "mixed found and not found",
					assErrs: 1,
					assIdxs: []int{1},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Label
metadata:
  name: label-1
---
apiVersion: influxdata.com/v2alpha1
kind: Bucket
metadata:
  name: rucket-3
spec:
  associations:
    - kind: Label
      name: label-1
    - kind: Label
      name: NOT TO BE FOUND
`,
				},
				{
					name:    "multiple not found",
					assErrs: 1,
					assIdxs: []int{0, 1},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Bucket
metadata:
  name: rucket-3
spec:
  associations:
    - kind: Label
      name: label-1
    - kind: Label
      name: label-2
`,
				},
				{
					name:    "duplicate valid nested labels",
					assErrs: 1,
					assIdxs: []int{1},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Label
metadata:
  name: label-1
---
apiVersion: influxdata.com/v2alpha1
kind: Bucket
metadata:
  name: rucket-3
spec:
  associations:
    - kind: Label
      name: label-1
    - kind: Label
      name: label-1
`,
				},
			}

			for _, tt := range tests {
				testTemplateErrors(t, KindBucket, tt)
			}
		})
	})

	t.Run("template with checks", func(t *testing.T) {
		t.Run("happy path", func(t *testing.T) {
			testfileRunner(t, "testdata/checks", func(t *testing.T, template *Template) {
				sum := template.Summary()
				require.Len(t, sum.Checks, 2)

				check1 := sum.Checks[0]
				assert.Equal(t, KindCheckThreshold, check1.Kind)
				thresholdCheck, ok := check1.Check.(*icheck.Threshold)
				require.Truef(t, ok, "got: %#v", check1)

				expectedBase := icheck.Base{
					Name:                  "check-0",
					Description:           "desc_0",
					Every:                 mustDuration(t, time.Minute),
					Offset:                mustDuration(t, 15*time.Second),
					StatusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }",
					Tags: []influxdb.Tag{
						{Key: "tag_1", Value: "val_1"},
						{Key: "tag_2", Value: "val_2"},
					},
				}
				expectedBase.Query.Text = "from(bucket: \"rucket_1\")\n  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n  |> filter(fn: (r) => r._measurement == \"cpu\")\n  |> filter(fn: (r) => r._field == \"usage_idle\")\n  |> aggregateWindow(every: 1m, fn: mean)\n  |> yield(name: \"mean\")"
				assert.Equal(t, expectedBase, thresholdCheck.Base)

				expectedThresholds := []icheck.ThresholdConfig{
					icheck.Greater{
						ThresholdConfigBase: icheck.ThresholdConfigBase{
							AllValues: true,
							Level:     notification.Critical,
						},
						Value: 50.0,
					},
					icheck.Lesser{
						ThresholdConfigBase: icheck.ThresholdConfigBase{Level: notification.Warn},
						Value:               49.9,
					},
					icheck.Range{
						ThresholdConfigBase: icheck.ThresholdConfigBase{Level: notification.Info},
						Within:              true,
						Min:                 30.0,
						Max:                 45.0,
					},
					icheck.Range{
						ThresholdConfigBase: icheck.ThresholdConfigBase{Level: notification.Ok},
						Min:                 30.0,
						Max:                 35.0,
					},
				}
				assert.Equal(t, expectedThresholds, thresholdCheck.Thresholds)
				assert.Equal(t, influxdb.Inactive, check1.Status)
				assert.Len(t, check1.LabelAssociations, 1)

				check2 := sum.Checks[1]
				assert.Equal(t, KindCheckDeadman, check2.Kind)
				deadmanCheck, ok := check2.Check.(*icheck.Deadman)
				require.Truef(t, ok, "got: %#v", check2)

				expectedBase = icheck.Base{
					Name:                  "display name",
					Description:           "desc_1",
					Every:                 mustDuration(t, 5*time.Minute),
					Offset:                mustDuration(t, 10*time.Second),
					StatusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }",
					Tags: []influxdb.Tag{
						{Key: "tag_1", Value: "val_1"},
						{Key: "tag_2", Value: "val_2"},
					},
				}
				expectedBase.Query.Text = "from(bucket: \"rucket_1\")\n  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n  |> filter(fn: (r) => r._measurement == \"cpu\")\n  |> filter(fn: (r) => r._field == \"usage_idle\")\n  |> aggregateWindow(every: 1m, fn: mean)\n  |> yield(name: \"mean\")"
				assert.Equal(t, expectedBase, deadmanCheck.Base)
				assert.Equal(t, influxdb.Active, check2.Status)
				assert.Equal(t, mustDuration(t, 10*time.Minute), deadmanCheck.StaleTime)
				assert.Equal(t, mustDuration(t, 90*time.Second), deadmanCheck.TimeSince)
				assert.True(t, deadmanCheck.ReportZero)
				assert.Len(t, check2.LabelAssociations, 1)

				expectedMappings := []SummaryLabelMapping{
					{
						LabelMetaName:    "label-1",
						LabelName:        "label-1",
						ResourceMetaName: "check-0",
						ResourceName:     "check-0",
					},
					{
						LabelMetaName:    "label-1",
						LabelName:        "label-1",
						ResourceMetaName: "check-1",
						ResourceName:     "display name",
					},
				}
				for _, expected := range expectedMappings {
					expected.Status = StateStatusNew
					expected.ResourceType = influxdb.ChecksResourceType
					assert.Contains(t, sum.LabelMappings, expected)
				}
			})
		})

		t.Run("with env refs should be successful", func(t *testing.T) {
			testfileRunner(t, "testdata/checks_ref.yml", func(t *testing.T, template *Template) {
				actual := template.Summary().Checks
				require.Len(t, actual, 1)

				expectedEnvRefs := []SummaryReference{
					{
						Field:        "metadata.name",
						EnvRefKey:    "meta-name",
						DefaultValue: "meta",
					},
					{
						Field:        "spec.name",
						EnvRefKey:    "spec-name",
						DefaultValue: "spectacles",
					},
					{
						Field:     "spec.associations[0].name",
						EnvRefKey: "label-meta-name",
					},
				}
				assert.Equal(t, expectedEnvRefs, actual[0].EnvReferences)
			})
		})

		t.Run("handles bad config", func(t *testing.T) {
			tests := []struct {
				kind   Kind
				resErr testTemplateResourceError
			}{
				{
					kind: KindCheckDeadman,
					resErr: testTemplateResourceError{
						name:           "duplicate name",
						validationErrs: 1,
						valFields:      []string{fieldMetadata, fieldName},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: CheckDeadman
metadata:
  name: check-1
spec:
  every: 5m
  level: cRiT
  query:  >
    from(bucket: "rucket_1") |> yield(name: "mean")
  statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }"
---
apiVersion: influxdata.com/v2alpha1
kind: CheckDeadman
metadata:
  name: check-1
spec:
  every: 5m
  level: cRiT
  query:  >
    from(bucket: "rucket_1") |> yield(name: "mean")
  statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }"
`,
					},
				},
				{
					kind: KindCheckThreshold,
					resErr: testTemplateResourceError{
						name:           "missing every duration",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldEvery},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: CheckThreshold
metadata:
  name: check-0
spec:
  query:  >
    from(bucket: "rucket_1") |> yield(name: "mean")
  statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }"
  thresholds:
    - type: outside_range
      level: ok
      min: 30.0
      max: 35.0

`,
					},
				},
				{
					kind: KindCheckThreshold,
					resErr: testTemplateResourceError{
						name:           "invalid threshold value provided",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldLevel},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: CheckThreshold
metadata:
  name: check-0
spec:
  every: 1m
  query:  >
    from(bucket: "rucket_1") |> yield(name: "mean")
  statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }"
  thresholds:
    - type: greater
      level: RANDO
      value: 50.0
`,
					},
				},
				{
					kind: KindCheckThreshold,
					resErr: testTemplateResourceError{
						name:           "invalid threshold type provided",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldType},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: CheckThreshold
metadata:
  name: check-0
spec:
  every: 1m
  query:  >
    from(bucket: "rucket_1") |> yield(name: "mean")
  statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }"
  thresholds:
    - type: RANDO_TYPE
      level: CRIT
      value: 50.0
`,
					},
				},
				{
					kind: KindCheckThreshold,
					resErr: testTemplateResourceError{
						name:           "invalid min for inside range",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldMin},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: CheckThreshold
metadata:
  name: check-0
spec:
  every: 1m
  query:  >
    from(bucket: "rucket_1") |> yield(name: "mean")
  statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }"
  thresholds:
    - type: inside_range
      level: INfO
      min: 45.0
      max: 30.0
`,
					},
				},
				{
					kind: KindCheckThreshold,
					resErr: testTemplateResourceError{
						name:           "no threshold values provided",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldCheckThresholds},
						templateStr: `---
apiVersion: influxdata.com/v2alpha1
kind: CheckThreshold
metadata:
  name: check-0
spec:
  every: 1m
  query:  >
    from(bucket: "rucket_1") |> yield(name: "mean")
  statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }"
  thresholds:
`,
					},
				},
				{
					kind: KindCheckThreshold,
					resErr: testTemplateResourceError{
						name:           "threshold missing query",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldQuery},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: CheckThreshold
metadata:
  name: check-0
spec:
  every: 1m
  statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }"
  thresholds:
    - type: greater
      level: CRIT
      value: 50.0
`,
					},
				},
				{
					kind: KindCheckThreshold,
					resErr: testTemplateResourceError{
						name:           "invalid status provided",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldStatus},
						templateStr: `---
apiVersion: influxdata.com/v2alpha1
kind: CheckThreshold
metadata:
  name: check-0
spec:
  every: 1m
  query:  >
    from(bucket: "rucket_1") |> yield(name: "mean")
  status: RANDO STATUS
  statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }"
  thresholds:
    - type: greater
      level: CRIT
      value: 50.0
      allValues: true
`,
					},
				},
				{
					kind: KindCheckThreshold,
					resErr: testTemplateResourceError{
						name:           "missing status message template",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldCheckStatusMessageTemplate},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: CheckThreshold
metadata:
  name: check-0
spec:
  every: 1m
  query:  >
    from(bucket: "rucket_1")
  thresholds:
    - type: greater
      level: CRIT
      value: 50.0
`,
					},
				},
				{
					kind: KindCheckDeadman,
					resErr: testTemplateResourceError{
						name:           "missing every",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldEvery},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: CheckDeadman
metadata:
  name: check-1
spec:
  level: cRiT
  query:  >
    from(bucket: "rucket_1") |> yield(name: "mean")
  statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }"
  timeSince: 90s
`,
					},
				},
				{
					kind: KindCheckDeadman,
					resErr: testTemplateResourceError{
						name:           "deadman missing every",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldQuery},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: CheckDeadman
metadata:
  name: check-1
spec:
  every: 5m
  level: cRiT
  statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }"
  timeSince: 90s
`,
					},
				},
				{
					kind: KindCheckDeadman,
					resErr: testTemplateResourceError{
						name:           "missing association label",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldAssociations},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: CheckDeadman
metadata:
  name: check-1
spec:
  every: 5m
  level: cRiT
  query:  >
    from(bucket: "rucket_1") |> yield(name: "mean")
  statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }"
  timeSince: 90s
  associations:
    - kind: Label
      name: label-1
`,
					},
				},
				{
					kind: KindCheckDeadman,
					resErr: testTemplateResourceError{
						name:           "duplicate association labels",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldAssociations},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Label
metadata:
  name: label-1
---
apiVersion: influxdata.com/v2alpha1
kind: CheckDeadman
metadata:
  name: check-1
spec:
  every: 5m
  level: cRiT
  query:  >
    from(bucket: "rucket_1") |> yield(name: "mean")
  statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }"
  timeSince: 90s
  associations:
    - kind: Label
      name: label-1
    - kind: Label
      name: label-1
`,
					},
				},
				/* checks are not name unique
							{
								kind: KindCheckDeadman,
								resErr: testTemplateResourceError{
									name:           "duplicate meta name and spec name",
									validationErrs: 1,
									valFields:      []string{fieldSpec, fieldAssociations},
									templateStr: `
				apiVersion: influxdata.com/v2alpha1
				kind: CheckDeadman
				metadata:
					name: check-1
				spec:
					every: 5m
					level: cRiT
					query:  >
						from(bucket: "rucket_1") |> yield(name: "mean")
					statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }"
					timeSince: 90s
				---
				apiVersion: influxdata.com/v2alpha1
				kind: CheckDeadman
				metadata:
					name: valid-name
				spec:
					name: check-1
					every: 5m
					level: cRiT
					query:  >
						from(bucket: "rucket_1") |> yield(name: "mean")
					statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }"
					timeSince: 90s
				`,
								},
							},
				*/
			}

			for _, tt := range tests {
				testTemplateErrors(t, tt.kind, tt.resErr)
			}
		})
	})

	t.Run("template with dashboard", func(t *testing.T) {
		t.Run("single chart should be successful", func(t *testing.T) {
			t.Run("gauge chart", func(t *testing.T) {
				t.Run("happy path", func(t *testing.T) {
					testfileRunner(t, "testdata/dashboard_gauge", func(t *testing.T, template *Template) {
						sum := template.Summary()
						require.Len(t, sum.Dashboards, 1)

						actual := sum.Dashboards[0]
						assert.Equal(t, KindDashboard, actual.Kind)
						assert.Equal(t, "dash-1", actual.Name)
						assert.Equal(t, "desc1", actual.Description)

						require.Len(t, actual.Charts, 1)
						actualChart := actual.Charts[0]
						assert.Equal(t, 3, actualChart.Height)
						assert.Equal(t, 6, actualChart.Width)
						assert.Equal(t, 1, actualChart.XPosition)
						assert.Equal(t, 2, actualChart.YPosition)

						props, ok := actualChart.Properties.(influxdb.GaugeViewProperties)
						require.True(t, ok)
						assert.Equal(t, "gauge", props.GetType())
						assert.Equal(t, "gauge note", props.Note)
						assert.True(t, props.ShowNoteWhenEmpty)

						require.Len(t, props.Queries, 1)
						q := props.Queries[0]
						queryText := `from(bucket: v.bucket)  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)  |> filter(fn: (r) => r._measurement == "boltdb_writes_total")  |> filter(fn: (r) => r._field == "counter")`
						assert.Equal(t, queryText, q.Text)
						assert.Equal(t, "advanced", q.EditMode)

						require.Len(t, props.ViewColors, 3)
						c := props.ViewColors[0]
						assert.Equal(t, "laser", c.Name)
						assert.Equal(t, "min", c.Type)
						assert.Equal(t, "#8F8AF4", c.Hex)
						assert.Equal(t, 0.0, c.Value)
					})
				})

				t.Run("handles invalid config", func(t *testing.T) {
					tests := []testTemplateResourceError{
						{
							name:           "color mixing a hex value",
							validationErrs: 1,
							valFields:      []string{fieldSpec, "charts[0].colors[0].hex"},
							templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name: dash-1
spec:
  description: desc1
  charts:
    - kind:   gauge
      name:   gauge
      note: gauge note
      noteOnEmpty: true
      xPos:  1
      yPos:  2
      width:  6
      height: 3
      queries:
        - query: >
            from(bucket: v.bucket)  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)  |> filter(fn: (r) => r._measurement == "boltdb_writes_total")  |> filter(fn: (r) => r._field == "counter")
      colors:
        - name: laser
          type: min
          value: 0
        - name: laser
          type: threshold
          hex: "#8F8AF4"
          value: 700
        - name: laser
          type: max
          hex: "#8F8AF4"
          value: 5000
`,
						},
					}

					for _, tt := range tests {
						testTemplateErrors(t, KindDashboard, tt)
					}
				})
			})

			t.Run("heatmap chart", func(t *testing.T) {
				t.Run("happy path", func(t *testing.T) {
					testfileRunner(t, "testdata/dashboard_heatmap", func(t *testing.T, template *Template) {
						sum := template.Summary()
						require.Len(t, sum.Dashboards, 1)

						actual := sum.Dashboards[0]
						assert.Equal(t, KindDashboard, actual.Kind)
						assert.Equal(t, "dash-0", actual.Name)
						assert.Equal(t, "a dashboard w/ heatmap chart", actual.Description)

						require.Len(t, actual.Charts, 1)
						actualChart := actual.Charts[0]
						assert.Equal(t, 3, actualChart.Height)
						assert.Equal(t, 6, actualChart.Width)
						assert.Equal(t, 1, actualChart.XPosition)
						assert.Equal(t, 2, actualChart.YPosition)

						props, ok := actualChart.Properties.(influxdb.HeatmapViewProperties)
						require.True(t, ok)
						assert.Equal(t, "heatmap", props.GetType())
						assert.Equal(t, "heatmap note", props.Note)
						assert.Equal(t, int32(10), props.BinSize)
						assert.Equal(t, []string{"xTotalTicks", "xTickStart", "xTickStep"}, props.GenerateXAxisTicks)
						assert.Equal(t, 15, props.XTotalTicks)
						assert.Equal(t, 0.0, props.XTickStart)
						assert.Equal(t, 1000.0, props.XTickStep)
						assert.Equal(t, []string{"yTotalTicks", "yTickStart", "yTickStep"}, props.GenerateYAxisTicks)
						assert.Equal(t, 10, props.YTotalTicks)
						assert.Equal(t, 0.0, props.YTickStart)
						assert.Equal(t, 100.0, props.YTickStep)
						assert.Equal(t, true, props.LegendColorizeRows)
						assert.Equal(t, false, props.LegendHide)
						assert.Equal(t, 1.0, props.LegendOpacity)
						assert.Equal(t, 5, props.LegendOrientationThreshold)
						assert.True(t, props.ShowNoteWhenEmpty)

						assert.Equal(t, []float64{0, 10}, props.XDomain)
						assert.Equal(t, []float64{0, 100}, props.YDomain)

						require.Len(t, props.Queries, 1)
						q := props.Queries[0]
						queryText := `from(bucket: v.bucket)  |> range(start: v.timeRangeStart)  |> filter(fn: (r) => r._measurement == "mem")  |> filter(fn: (r) => r._field == "used_percent")  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)  |> yield(name: "mean")`
						assert.Equal(t, queryText, q.Text)
						assert.Equal(t, "advanced", q.EditMode)

						require.Len(t, props.ViewColors, 12)
						c := props.ViewColors[0]
						assert.Equal(t, "#000004", c)
					})
				})

				t.Run("handles invalid config", func(t *testing.T) {
					tests := []testTemplateResourceError{
						{
							name:           "a color is missing a hex value",
							validationErrs: 1,
							valFields:      []string{fieldSpec, "charts[0].colors[2].hex"},
							templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name: dash-0
spec:
  charts:
    - kind:   heatmap
      name:   heatmap
      xPos:  1
      yPos:  2
      width:  6
      height: 3
      binSize: 10
      xCol: _time
      yCol: _value
      queries:
        - query: >
            from(bucket: v.bucket)  |> range(start: v.timeRangeStart)  |> filter(fn: (r) => r._measurement == "mem")  |> filter(fn: (r) => r._field == "used_percent")  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)  |> yield(name: "mean")
      colors:
        - hex: "#fbb61a"
        - hex: "#f4df53"
        - hex: ""
      axes:
        - name: "x"
          label: "x_label"
          prefix: "x_prefix"
          suffix: "x_suffix"
          domain:
            - 0
            - 10
        - name: "y"
          label: "y_label"
          prefix: "y_prefix"
          suffix: "y_suffix"
          domain:
            - 0
            - 100
`,
						},
						{
							name:           "missing axes",
							validationErrs: 1,
							valFields:      []string{fieldSpec, "charts[0].axes"},
							templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name: dash-0
spec:
  charts:
    - kind:   heatmap
      name:   heatmap
      xPos:  1
      yPos:  2
      width:  6
      height: 3
      binSize: 10
      xCol: _time
      yCol: _value
      queries:
        - query: >
            from(bucket: v.bucket)  |> range(start: v.timeRangeStart)  |> filter(fn: (r) => r._measurement == "mem")  |> filter(fn: (r) => r._field == "used_percent")  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)  |> yield(name: "mean")
      colors:
        - hex: "#000004"
`,
						},
					}

					for _, tt := range tests {
						testTemplateErrors(t, KindDashboard, tt)
					}
				})
			})

			t.Run("histogram chart", func(t *testing.T) {
				t.Run("happy path", func(t *testing.T) {
					testfileRunner(t, "testdata/dashboard_histogram", func(t *testing.T, template *Template) {
						sum := template.Summary()
						require.Len(t, sum.Dashboards, 1)

						actual := sum.Dashboards[0]
						assert.Equal(t, KindDashboard, actual.Kind)
						assert.Equal(t, "dash-0", actual.Name)
						assert.Equal(t, "a dashboard w/ single histogram chart", actual.Description)

						require.Len(t, actual.Charts, 1)
						actualChart := actual.Charts[0]
						assert.Equal(t, 3, actualChart.Height)
						assert.Equal(t, 6, actualChart.Width)

						props, ok := actualChart.Properties.(influxdb.HistogramViewProperties)
						require.True(t, ok)
						assert.Equal(t, "histogram", props.GetType())
						assert.Equal(t, "histogram note", props.Note)
						assert.Equal(t, 30, props.BinCount)
						assert.Equal(t, true, props.LegendColorizeRows)
						assert.Equal(t, false, props.LegendHide)
						assert.Equal(t, 1.0, props.LegendOpacity)
						assert.Equal(t, 5, props.LegendOrientationThreshold)
						assert.True(t, props.ShowNoteWhenEmpty)
						assert.Equal(t, []float64{0, 10}, props.XDomain)
						assert.Equal(t, []string{"a", "b"}, props.FillColumns)
						require.Len(t, props.Queries, 1)
						q := props.Queries[0]
						queryText := `from(bucket: v.bucket) |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r._measurement == "boltdb_reads_total") |> filter(fn: (r) => r._field == "counter")`
						assert.Equal(t, queryText, q.Text)
						assert.Equal(t, "advanced", q.EditMode)

						require.Len(t, props.ViewColors, 3)
						assert.Equal(t, "#8F8AF4", props.ViewColors[0].Hex)
						assert.Equal(t, "#F4CF31", props.ViewColors[1].Hex)
						assert.Equal(t, "#FFFFFF", props.ViewColors[2].Hex)
					})
				})

				t.Run("handles invalid config", func(t *testing.T) {
					tests := []testTemplateResourceError{
						{
							name:           "missing x-axis",
							validationErrs: 1,
							valFields:      []string{fieldSpec, "charts[0].axes"},
							templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name: dash-0
spec:
  description: a dashboard w/ single histogram chart
  charts:
    - kind: Histogram
      name: histogram chart
      xCol: _value
      width:  6
      height: 3
      binCount: 30
      queries:
        - query: >
            from(bucket: v.bucket) |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r._measurement == "boltdb_reads_total") |> filter(fn: (r) => r._field == "counter")
      colors:
        - hex: "#8F8AF4"
          type: scale
          value: 0
          name: mycolor
      axes:
`,
						},
					}

					for _, tt := range tests {
						testTemplateErrors(t, KindDashboard, tt)
					}
				})
			})

			t.Run("markdown chart", func(t *testing.T) {
				t.Run("happy path", func(t *testing.T) {
					testfileRunner(t, "testdata/dashboard_markdown", func(t *testing.T, template *Template) {
						sum := template.Summary()
						require.Len(t, sum.Dashboards, 1)

						actual := sum.Dashboards[0]
						assert.Equal(t, KindDashboard, actual.Kind)
						assert.Equal(t, "dash-0", actual.Name)
						assert.Equal(t, "a dashboard w/ single markdown chart", actual.Description)

						require.Len(t, actual.Charts, 1)
						actualChart := actual.Charts[0]

						props, ok := actualChart.Properties.(influxdb.MarkdownViewProperties)
						require.True(t, ok)
						assert.Equal(t, "markdown", props.GetType())
						assert.Equal(t, "## markdown note", props.Note)
					})
				})
			})

			t.Run("mosaic chart", func(t *testing.T) {
				t.Run("happy path", func(t *testing.T) {
					testfileRunner(t, "testdata/dashboard_mosaic.yml", func(t *testing.T, template *Template) {
						sum := template.Summary()
						require.Len(t, sum.Dashboards, 1)

						actual := sum.Dashboards[0]
						assert.Equal(t, KindDashboard, actual.Kind)
						assert.Equal(t, "dash-0", actual.Name)
						assert.Equal(t, "a dashboard w/ single mosaic chart", actual.Description)

						require.Len(t, actual.Charts, 1)
						actualChart := actual.Charts[0]
						assert.Equal(t, 3, actualChart.Height)
						assert.Equal(t, 6, actualChart.Width)
						assert.Equal(t, 1, actualChart.XPosition)
						assert.Equal(t, 2, actualChart.YPosition)

						props, ok := actualChart.Properties.(influxdb.MosaicViewProperties)
						require.True(t, ok)
						assert.Equal(t, "mosaic note", props.Note)
						assert.Equal(t, "y", props.HoverDimension)
						assert.True(t, props.ShowNoteWhenEmpty)

						require.Len(t, props.Queries, 1)
						q := props.Queries[0]
						expectedQuery := `from(bucket: v.bucket)  |> range(start: v.timeRangeStart)  |> filter(fn: (r) => r._measurement == "mem")  |> filter(fn: (r) => r._field == "used_percent")  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)  |> yield(name: "mean")`
						assert.Equal(t, expectedQuery, q.Text)
						assert.Equal(t, "advanced", q.EditMode)

						assert.Equal(t, ",", props.YLabelColumnSeparator)
						assert.Equal(t, []string{"foo"}, props.YLabelColumns)
						assert.Equal(t, []string{"_value", "foo"}, props.YSeriesColumns)
						assert.Equal(t, []float64{0, 10}, props.XDomain)
						assert.Equal(t, []float64{0, 100}, props.YDomain)
						assert.Equal(t, "x_label", props.XAxisLabel)
						assert.Equal(t, "y_label", props.YAxisLabel)
						assert.Equal(t, "x_prefix", props.XPrefix)
						assert.Equal(t, "y_prefix", props.YPrefix)
						assert.Equal(t, "x_suffix", props.XSuffix)
						assert.Equal(t, "y_suffix", props.YSuffix)
						assert.Equal(t, []string{"xTotalTicks", "xTickStart", "xTickStep"}, props.GenerateXAxisTicks)
						assert.Equal(t, 15, props.XTotalTicks)
						assert.Equal(t, 0.0, props.XTickStart)
						assert.Equal(t, 1000.0, props.XTickStep)
						assert.Equal(t, true, props.LegendColorizeRows)
						assert.Equal(t, false, props.LegendHide)
						assert.Equal(t, 1.0, props.LegendOpacity)
						assert.Equal(t, 5, props.LegendOrientationThreshold)
					})
				})
			})

			t.Run("band chart", func(t *testing.T) {
				t.Run("happy path", func(t *testing.T) {
					testfileRunner(t, "testdata/dashboard_band.yml", func(t *testing.T, template *Template) {
						sum := template.Summary()
						require.Len(t, sum.Dashboards, 1)

						actual := sum.Dashboards[0]
						assert.Equal(t, KindDashboard, actual.Kind)
						assert.Equal(t, "dash-1", actual.Name)
						assert.Equal(t, "a dashboard w/ single band chart", actual.Description)

						require.Len(t, actual.Charts, 1)
						actualChart := actual.Charts[0]
						assert.Equal(t, 3, actualChart.Height)
						assert.Equal(t, 6, actualChart.Width)
						assert.Equal(t, 1, actualChart.XPosition)
						assert.Equal(t, 2, actualChart.YPosition)

						props, ok := actualChart.Properties.(influxdb.BandViewProperties)
						require.True(t, ok)
						assert.Equal(t, "band note", props.Note)
						assert.True(t, props.ShowNoteWhenEmpty)
						assert.Equal(t, "y", props.HoverDimension)
						assert.Equal(t, "foo", props.UpperColumn)
						assert.Equal(t, "baz", props.MainColumn)
						assert.Equal(t, "bar", props.LowerColumn)
						assert.Equal(t, []string{"xTotalTicks", "xTickStart", "xTickStep"}, props.GenerateXAxisTicks)
						assert.Equal(t, 15, props.XTotalTicks)
						assert.Equal(t, 0.0, props.XTickStart)
						assert.Equal(t, 1000.0, props.XTickStep)
						assert.Equal(t, []string{"yTotalTicks", "yTickStart", "yTickStep"}, props.GenerateYAxisTicks)
						assert.Equal(t, 10, props.YTotalTicks)
						assert.Equal(t, 0.0, props.YTickStart)
						assert.Equal(t, 100.0, props.YTickStep)
						assert.Equal(t, true, props.LegendColorizeRows)
						assert.Equal(t, false, props.LegendHide)
						assert.Equal(t, 1.0, props.LegendOpacity)
						assert.Equal(t, 5, props.LegendOrientationThreshold)
						assert.Equal(t, true, props.StaticLegend.ColorizeRows)
						assert.Equal(t, 0.2, props.StaticLegend.HeightRatio)
						assert.Equal(t, true, props.StaticLegend.Show)
						assert.Equal(t, 1.0, props.StaticLegend.Opacity)
						assert.Equal(t, 5, props.StaticLegend.OrientationThreshold)
						assert.Equal(t, "y", props.StaticLegend.ValueAxis)
						assert.Equal(t, 1.0, props.StaticLegend.WidthRatio)

						require.Len(t, props.ViewColors, 1)
						c := props.ViewColors[0]
						assert.Equal(t, "laser", c.Name)
						assert.Equal(t, "scale", c.Type)
						assert.Equal(t, "#8F8AF4", c.Hex)
						assert.Equal(t, 3.0, c.Value)

						require.Len(t, props.Queries, 1)
						q := props.Queries[0]
						expectedQuery := `from(bucket: v.bucket)  |> range(start: v.timeRangeStart)  |> filter(fn: (r) => r._measurement == "mem")  |> filter(fn: (r) => r._field == "used_percent")  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)  |> yield(name: "mean")`
						assert.Equal(t, expectedQuery, q.Text)
						assert.Equal(t, "advanced", q.EditMode)

						for _, key := range []string{"x", "y"} {
							xAxis, ok := props.Axes[key]
							require.True(t, ok, "key="+key)
							assert.Equal(t, key+"_label", xAxis.Label, "key="+key)
							assert.Equal(t, key+"_prefix", xAxis.Prefix, "key="+key)
							assert.Equal(t, key+"_suffix", xAxis.Suffix, "key="+key)
						}

					})
				})
			})

			t.Run("scatter chart", func(t *testing.T) {
				t.Run("happy path", func(t *testing.T) {
					testfileRunner(t, "testdata/dashboard_scatter", func(t *testing.T, template *Template) {
						sum := template.Summary()
						require.Len(t, sum.Dashboards, 1)

						actual := sum.Dashboards[0]
						assert.Equal(t, KindDashboard, actual.Kind)
						assert.Equal(t, "dash-0", actual.Name)
						assert.Equal(t, "a dashboard w/ single scatter chart", actual.Description)

						require.Len(t, actual.Charts, 1)
						actualChart := actual.Charts[0]
						assert.Equal(t, 3, actualChart.Height)
						assert.Equal(t, 6, actualChart.Width)
						assert.Equal(t, 1, actualChart.XPosition)
						assert.Equal(t, 2, actualChart.YPosition)

						props, ok := actualChart.Properties.(influxdb.ScatterViewProperties)
						require.True(t, ok)
						assert.Equal(t, "scatter note", props.Note)
						assert.True(t, props.ShowNoteWhenEmpty)

						require.Len(t, props.Queries, 1)
						q := props.Queries[0]
						expectedQuery := `from(bucket: v.bucket)  |> range(start: v.timeRangeStart)  |> filter(fn: (r) => r._measurement == "mem")  |> filter(fn: (r) => r._field == "used_percent")  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)  |> yield(name: "mean")`
						assert.Equal(t, expectedQuery, q.Text)
						assert.Equal(t, "advanced", q.EditMode)

						assert.Equal(t, []float64{0, 10}, props.XDomain)
						assert.Equal(t, []float64{0, 100}, props.YDomain)
						assert.Equal(t, "x_label", props.XAxisLabel)
						assert.Equal(t, "y_label", props.YAxisLabel)
						assert.Equal(t, "x_prefix", props.XPrefix)
						assert.Equal(t, "y_prefix", props.YPrefix)
						assert.Equal(t, "x_suffix", props.XSuffix)
						assert.Equal(t, "y_suffix", props.YSuffix)
						assert.Equal(t, []string{"xTotalTicks", "xTickStart", "xTickStep"}, props.GenerateXAxisTicks)
						assert.Equal(t, 15, props.XTotalTicks)
						assert.Equal(t, 0.0, props.XTickStart)
						assert.Equal(t, 1000.0, props.XTickStep)
						assert.Equal(t, []string{"yTotalTicks", "yTickStart", "yTickStep"}, props.GenerateYAxisTicks)
						assert.Equal(t, 10, props.YTotalTicks)
						assert.Equal(t, 0.0, props.YTickStart)
						assert.Equal(t, 100.0, props.YTickStep)
						assert.Equal(t, true, props.LegendColorizeRows)
						assert.Equal(t, false, props.LegendHide)
						assert.Equal(t, 1.0, props.LegendOpacity)
						assert.Equal(t, 5, props.LegendOrientationThreshold)
					})
				})

				t.Run("handles invalid config", func(t *testing.T) {
					tests := []testTemplateResourceError{
						{
							name:           "missing axes",
							validationErrs: 1,
							valFields:      []string{fieldSpec, "charts[0].axes"},
							templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name:  dash-0
spec:
  description: a dashboard w/ single scatter chart
  charts:
    - kind:   Scatter
      name:   scatter chart
      xPos:  1
      yPos:  2
      xCol: _time
      yCol: _value
      width:  6
      height: 3
      queries:
        - query: >
            from(bucket: v.bucket)  |> range(start: v.timeRangeStart)  |> filter(fn: (r) => r._measurement == "mem")  |> filter(fn: (r) => r._field == "used_percent")  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)  |> yield(name: "mean")
      colors:
        - hex: "#8F8AF4"
        - hex: "#F4CF31"
`,
						},
						{
							name:           "no width provided",
							validationErrs: 1,
							valFields:      []string{fieldSpec, "charts[0].width"},
							templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name:  dash-0
spec:
  description: a dashboard w/ single scatter chart
  charts:
    - kind:   Scatter
      name:   scatter chart
      xPos:  1
      yPos:  2
      xCol: _time
      yCol: _value
      height: 3
      queries:
        - query: >
            from(bucket: v.bucket)  |> range(start: v.timeRangeStart)  |> filter(fn: (r) => r._measurement == "mem")  |> filter(fn: (r) => r._field == "used_percent")  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)  |> yield(name: "mean")
      colors:
        - hex: "#8F8AF4"
        - hex: "#F4CF31"
        - hex: "#FFFFFF"
      axes:
        - name : "x"
          label: x_label
          prefix: x_prefix
          suffix: x_suffix
          domain:
            - 0
            - 10
        - name: "y"
          label: y_label
          prefix: y_prefix
          suffix: y_suffix
          domain:
            - 0
            - 100
`,
						},
						{
							name:           "no height provided",
							validationErrs: 1,
							valFields:      []string{fieldSpec, "charts[0].height"},
							templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name:  dash-0
spec:
  description: a dashboard w/ single scatter chart
  charts:
    - kind:   Scatter
      name:   scatter chart
      xPos:  1
      yPos:  2
      xCol: _time
      yCol: _value
      width:  6
      queries:
        - query: >
            from(bucket: v.bucket)  |> range(start: v.timeRangeStart)  |> filter(fn: (r) => r._measurement == "mem")  |> filter(fn: (r) => r._field == "used_percent")  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)  |> yield(name: "mean")
      colors:
        - hex: "#8F8AF4"
        - hex: "#F4CF31"
        - hex: "#FFFFFF"
      axes:
        - name : "x"
          label: x_label
          prefix: x_prefix
          suffix: x_suffix
          domain:
            - 0
            - 10
        - name: "y"
          label: y_label
          prefix: y_prefix
          suffix: y_suffix
          domain:
            - 0
            - 100
`,
						},
						{
							name:           "missing hex color",
							validationErrs: 1,
							valFields:      []string{fieldSpec, "charts[0].colors[0].hex"},
							templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name:  dash-0
spec:
  description: a dashboard w/ single scatter chart
  charts:
    - kind:   Scatter
      name:   scatter chart
      note: scatter note
      noteOnEmpty: true
      prefix: sumtin
      suffix: days
      xPos:  1
      yPos:  2
      xCol: _time
      yCol: _value
      width:  6
      height: 3
      queries:
        - query: >
            from(bucket: v.bucket)  |> range(start: v.timeRangeStart)  |> filter(fn: (r) => r._measurement == "mem")  |> filter(fn: (r) => r._field == "used_percent")  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)  |> yield(name: "mean")
      colors:
        - hex: ""
      axes:
        - name : "x"
          label: x_label
          prefix: x_prefix
          suffix: x_suffix
          domain:
            - 0
            - 10
        - name: "y"
          label: y_label
          prefix: y_prefix
          suffix: y_suffix
          domain:
            - 0
            - 100
`,
						},
						{
							name:           "missing x axis",
							validationErrs: 1,
							valFields:      []string{fieldSpec, "charts[0].axes"},
							templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name:  dash-0
spec:
  description: a dashboard w/ single scatter chart
  charts:
    - kind:   Scatter
      name:   scatter chart
      xPos:  1
      yPos:  2
      xCol: _time
      yCol: _value
      width:  6
      height: 3
      queries:
        - query: >
            from(bucket: v.bucket)  |> range(start: v.timeRangeStart)  |> filter(fn: (r) => r._measurement == "mem")  |> filter(fn: (r) => r._field == "used_percent")  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)  |> yield(name: "mean")
      colors:
        - hex: "#8F8AF4"
        - hex: "#F4CF31"
        - hex: "#FFFFFF"
      axes:
        - name: "y"
          label: y_label
          prefix: y_prefix
          suffix: y_suffix
          domain:
            - 0
            - 100
`,
						},
						{
							name:           "missing y axis",
							validationErrs: 1,
							valFields:      []string{fieldSpec, "charts[0].axes"},
							templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name:  dash-0
spec:
  description: a dashboard w/ single scatter chart
  charts:
    - kind:   Scatter
      name:   scatter chart
      xPos:  1
      yPos:  2
      xCol: _time
      yCol: _value
      width:  6
      height: 3
      queries:
        - query: >
            from(bucket: v.bucket)  |> range(start: v.timeRangeStart)  |> filter(fn: (r) => r._measurement == "mem")  |> filter(fn: (r) => r._field == "used_percent")  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)  |> yield(name: "mean")
      colors:
        - hex: "#8F8AF4"
        - hex: "#F4CF31"
        - hex: "#FFFFFF"
      axes:
        - name : "x"
          label: x_label
          prefix: x_prefix
          suffix: x_suffix
          domain:
            - 0
            - 10
`,
						},
					}

					for _, tt := range tests {
						testTemplateErrors(t, KindDashboard, tt)
					}
				})
			})

			t.Run("single stat chart", func(t *testing.T) {
				t.Run("happy path", func(t *testing.T) {
					testfileRunner(t, "testdata/dashboard", func(t *testing.T, template *Template) {
						sum := template.Summary()
						require.Len(t, sum.Dashboards, 2)

						actual := sum.Dashboards[0]
						assert.Equal(t, KindDashboard, actual.Kind)
						assert.Equal(t, "dash-1", actual.MetaName)
						assert.Equal(t, "display name", actual.Name)
						assert.Equal(t, "desc1", actual.Description)

						require.Len(t, actual.Charts, 1)
						actualChart := actual.Charts[0]
						assert.Equal(t, 3, actualChart.Height)
						assert.Equal(t, 6, actualChart.Width)
						assert.Equal(t, 1, actualChart.XPosition)
						assert.Equal(t, 2, actualChart.YPosition)

						props, ok := actualChart.Properties.(influxdb.SingleStatViewProperties)
						require.True(t, ok)
						assert.Equal(t, "single-stat", props.GetType())
						assert.Equal(t, "single stat note", props.Note)
						assert.True(t, props.ShowNoteWhenEmpty)
						assert.True(t, props.DecimalPlaces.IsEnforced)
						assert.Equal(t, int32(1), props.DecimalPlaces.Digits)
						assert.Equal(t, "days", props.Suffix)
						assert.Equal(t, "true", props.TickSuffix)
						assert.Equal(t, "sumtin", props.Prefix)
						assert.Equal(t, "true", props.TickPrefix)

						require.Len(t, props.Queries, 1)
						q := props.Queries[0]
						queryText := `from(bucket: v.bucket) |> range(start: v.timeRangeStart) |> filter(fn: (r) => r._measurement == "processes") |> filter(fn: (r) => r._field == "running" or r._field == "blocked") |> aggregateWindow(every: v.windowPeriod, fn: max) |> yield(name: "max")`
						assert.Equal(t, queryText, q.Text)
						assert.Equal(t, "advanced", q.EditMode)

						require.Len(t, props.ViewColors, 1)
						c := props.ViewColors[0]
						assert.Equal(t, "laser", c.Name)
						assert.Equal(t, "text", c.Type)
						assert.Equal(t, "#8F8AF4", c.Hex)
						assert.Equal(t, 3.0, c.Value)

						actual2 := sum.Dashboards[1]
						assert.Equal(t, "dash-2", actual2.MetaName)
						assert.Equal(t, "dash-2", actual2.Name)
						assert.Equal(t, "desc", actual2.Description)
					})
				})

				t.Run("handles invalid config", func(t *testing.T) {
					tests := []testTemplateResourceError{
						{
							name:           "color missing hex value",
							validationErrs: 1,
							valFields:      []string{fieldSpec, "charts[0].colors[0].hex"},
							templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name: dash-1
spec:
  description: desc1
  charts:
    - kind:   Single_Stat
      name:   single stat
      xPos: 1
      yPos: 2
      width:  6
      height: 3
      decimalPlaces: 1
      shade: true
      hoverDimension: y
      queries:
        - query: "from(bucket: v.bucket) |> range(start: v.timeRangeStart) |> filter(fn: (r) => r._measurement == \"processes\") |> filter(fn: (r) => r._field == \"running\" or r._field == \"blocked\") |> aggregateWindow(every: v.windowPeriod, fn: max) |> yield(name: \"max\")"
      colors:
        - name: laser
          type: text
          value: 3
`,
						},
						{
							name:           "no width provided",
							validationErrs: 1,
							valFields:      []string{fieldSpec, "charts[0].width"},
							templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name: dash-1
spec:
  description: desc1
  charts:
    - kind:   Single_Stat
      name:   single stat
      xPos: 1
      yPos: 2
      height: 3
      queries:
        - query: "from(bucket: v.bucket) |> range(start: v.timeRangeStart) |> filter(fn: (r) => r._measurement == \"processes\") |> filter(fn: (r) => r._field == \"running\" or r._field == \"blocked\") |> aggregateWindow(every: v.windowPeriod, fn: max) |> yield(name: \"max\")"
      colors:
        - name: laser
          type: text
          hex: "#8F8AF4"
`,
						},
						{
							name:           "no height provided",
							validationErrs: 1,
							valFields:      []string{fieldSpec, "charts[0].height"},
							templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name: dash-1
spec:
  description: desc1
  charts:
    - kind:   Single_Stat
      name:   single stat
      xPos: 1
      yPos: 2
      width: 3
      queries:
        - query: "from(bucket: v.bucket) |> range(start: v.timeRangeStart) |> filter(fn: (r) => r._measurement == \"processes\") |> filter(fn: (r) => r._field == \"running\" or r._field == \"blocked\") |> aggregateWindow(every: v.windowPeriod, fn: max) |> yield(name: \"max\")"
      colors:
        - name: laser
          type: text
          hex: "#8F8AF4"
`,
						},
						{
							name:           "duplicate metadata names",
							validationErrs: 1,
							valFields:      []string{fieldMetadata, fieldName},
							templateStr: `
apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name: dash-1
spec:
---
apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name: dash-1
spec:
`,
						},
						{
							name:           "spec name too short",
							validationErrs: 1,
							valFields:      []string{fieldSpec, fieldName},
							templateStr: `
apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name: dash-1
spec:
  name: d
`,
						},
					}

					for _, tt := range tests {
						testTemplateErrors(t, KindDashboard, tt)
					}
				})
			})

			t.Run("single stat plus line chart", func(t *testing.T) {
				t.Run("happy path", func(t *testing.T) {
					testfileRunner(t, "testdata/dashboard_single_stat_plus_line", func(t *testing.T, template *Template) {
						sum := template.Summary()
						require.Len(t, sum.Dashboards, 1)

						actual := sum.Dashboards[0]
						assert.Equal(t, KindDashboard, actual.Kind)
						assert.Equal(t, "dash-1", actual.Name)
						assert.Equal(t, "desc1", actual.Description)

						require.Len(t, actual.Charts, 1)
						actualChart := actual.Charts[0]
						assert.Equal(t, 3, actualChart.Height)
						assert.Equal(t, 6, actualChart.Width)
						assert.Equal(t, 1, actualChart.XPosition)
						assert.Equal(t, 2, actualChart.YPosition)

						props, ok := actualChart.Properties.(influxdb.LinePlusSingleStatProperties)
						require.True(t, ok)
						assert.Equal(t, "single stat plus line note", props.Note)
						assert.True(t, props.ShowNoteWhenEmpty)
						assert.True(t, props.DecimalPlaces.IsEnforced)
						assert.Equal(t, int32(1), props.DecimalPlaces.Digits)
						assert.Equal(t, "days", props.Suffix)
						assert.Equal(t, "sumtin", props.Prefix)
						assert.Equal(t, "overlaid", props.Position)
						assert.Equal(t, []string{"xTotalTicks", "xTickStart", "xTickStep"}, props.GenerateXAxisTicks)
						assert.Equal(t, 15, props.XTotalTicks)
						assert.Equal(t, 0.0, props.XTickStart)
						assert.Equal(t, 1000.0, props.XTickStep)
						assert.Equal(t, []string{"yTotalTicks", "yTickStart", "yTickStep"}, props.GenerateYAxisTicks)
						assert.Equal(t, 10, props.YTotalTicks)
						assert.Equal(t, 0.0, props.YTickStart)
						assert.Equal(t, 100.0, props.YTickStep)
						assert.Equal(t, true, props.LegendColorizeRows)
						assert.Equal(t, false, props.LegendHide)
						assert.Equal(t, 1.0, props.LegendOpacity)
						assert.Equal(t, 5, props.LegendOrientationThreshold)
						assert.Equal(t, true, props.StaticLegend.ColorizeRows)
						assert.Equal(t, 0.2, props.StaticLegend.HeightRatio)
						assert.Equal(t, true, props.StaticLegend.Show)
						assert.Equal(t, 1.0, props.StaticLegend.Opacity)
						assert.Equal(t, 5, props.StaticLegend.OrientationThreshold)
						assert.Equal(t, "y", props.StaticLegend.ValueAxis)
						assert.Equal(t, 1.0, props.StaticLegend.WidthRatio)

						require.Len(t, props.Queries, 1)
						q := props.Queries[0]
						expectedQuery := `from(bucket: v.bucket)  |> range(start: v.timeRangeStart)  |> filter(fn: (r) => r._measurement == "mem")  |> filter(fn: (r) => r._field == "used_percent")  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)  |> yield(name: "mean")`
						assert.Equal(t, expectedQuery, q.Text)
						assert.Equal(t, "advanced", q.EditMode)

						for _, key := range []string{"x", "y"} {
							xAxis, ok := props.Axes[key]
							require.True(t, ok, "key="+key)
							assert.Equal(t, "10", xAxis.Base, "key="+key)
							assert.Equal(t, key+"_label", xAxis.Label, "key="+key)
							assert.Equal(t, key+"_prefix", xAxis.Prefix, "key="+key)
							assert.Equal(t, "linear", xAxis.Scale, "key="+key)
							assert.Equal(t, key+"_suffix", xAxis.Suffix, "key="+key)
						}

						require.Len(t, props.ViewColors, 2)
						c := props.ViewColors[0]
						assert.Equal(t, "base", c.ID)
						assert.Equal(t, "laser", c.Name)
						assert.Equal(t, "text", c.Type)
						assert.Equal(t, "#8F8AF4", c.Hex)
						assert.Equal(t, 3.0, c.Value)

						c = props.ViewColors[1]
						assert.Equal(t, "base", c.ID)
						assert.Equal(t, "android", c.Name)
						assert.Equal(t, "scale", c.Type)
						assert.Equal(t, "#F4CF31", c.Hex)
						assert.Equal(t, 1.0, c.Value)
					})
				})

				t.Run("handles invalid config", func(t *testing.T) {
					tests := []testTemplateResourceError{
						{
							name:           "color missing hex value",
							validationErrs: 1,
							valFields:      []string{fieldSpec, "charts[0].colors[0].hex"},
							templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name: dash-1
spec:
  description: desc1
  charts:
    - kind:   Single_Stat_Plus_Line
      name:   single stat plus line
      xPos:  1
      yPos:  2
      width:  6
      height: 3
      position: overlaid
      queries:
        - query: >
            from(bucket: v.bucket)  |> range(start: v.timeRangeStart)  |> filter(fn: (r) => r._measurement == "mem")  |> filter(fn: (r) => r._field == "used_percent")  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)  |> yield(name: "mean")
      colors:
        - name: laser
          type: text
          value: 3
      axes:
        - name : "x"
          label: x_label
          prefix: x_prefix
          suffix: x_suffix
          base: 10
          scale: linear
        - name: "y"
          label: y_label
          prefix: y_prefix
          suffix: y_suffix
          base: 10
          scale: linear
`,
						},
						{
							name:           "no width provided",
							validationErrs: 1,
							valFields:      []string{fieldSpec, "charts[0].width"},
							templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name: dash-1
spec:
  description: desc1
  charts:
    - kind:   Single_Stat_Plus_Line
      name:   single stat plus line
      xPos:  1
      yPos:  2
      height: 3
      shade: true
      hoverDimension: "y"
      position: overlaid
      queries:
        - query: >
            from(bucket: v.bucket)  |> range(start: v.timeRangeStart)  |> filter(fn: (r) => r._measurement == "mem")  |> filter(fn: (r) => r._field == "used_percent")  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)  |> yield(name: "mean")
      colors:
        - name: laser
          type: text
          hex: "#8F8AF4"
          value: 3
        - name: android
          type: scale
          hex: "#F4CF31"
          value: 1
      axes:
        - name : "x"
          label: x_label
          prefix: x_prefix
          suffix: x_suffix
          base: 10
          scale: linear
        - name: "y"
          label: y_label
          prefix: y_prefix
          suffix: y_suffix
          base: 10
          scale: linear
`,
						},
						{
							name:           "no height provided",
							validationErrs: 1,
							valFields:      []string{fieldSpec, "charts[0].height"},
							templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name: dash-1
spec:
  description: desc1
  charts:
    - kind:   Single_Stat_Plus_Line
      name:   single stat plus line
      xPos:  1
      yPos:  2
      width:  6
      position: overlaid
      queries:
        - query: >
            from(bucket: v.bucket)  |> range(start: v.timeRangeStart)  |> filter(fn: (r) => r._measurement == "mem")  |> filter(fn: (r) => r._field == "used_percent")  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)  |> yield(name: "mean")
      colors:
        - name: laser
          type: text
          hex: "#8F8AF4"
          value: 3
        - name: android
          type: scale
          hex: "#F4CF31"
          value: 1
      axes:
        - name : "x"
          label: x_label
          prefix: x_prefix
          suffix: x_suffix
          base: 10
          scale: linear
        - name: "y"
          label: y_label
          prefix: y_prefix
          suffix: y_suffix
          base: 10
          scale: linear
`,
						},
						{
							name:           "missing x axis",
							validationErrs: 1,
							valFields:      []string{fieldSpec, "charts[0].axes"},
							templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name: dash-1
spec:
  description: desc1
  charts:
    - kind:   Single_Stat_Plus_Line
      name:   single stat plus line
      xPos:  1
      yPos:  2
      width:  6
      height: 3
      position: overlaid
      queries:
        - query: >
            from(bucket: v.bucket)  |> range(start: v.timeRangeStart)  |> filter(fn: (r) => r._measurement == "mem")  |> filter(fn: (r) => r._field == "used_percent")  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)  |> yield(name: "mean")
      colors:
        - name: laser
          type: text
          hex: "#8F8AF4"
          value: 3
        - name: android
          type: scale
          hex: "#F4CF31"
          value: 1
      axes:
        - name: "y"
          label: y_label
          prefix: y_prefix
          suffix: y_suffix
          base: 10
          scale: linear
`,
						},
						{
							name:           "missing y axis",
							validationErrs: 1,
							valFields:      []string{fieldSpec, "charts[0].axes"},
							templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name: dash-1
spec:
  description: desc1
  charts:
    - kind:   Single_Stat_Plus_Line
      name:   single stat plus line
      xPos:  1
      yPos:  2
      width:  6
      height: 3
      position: overlaid
      queries:
        - query: >
            from(bucket: v.bucket)  |> range(start: v.timeRangeStart)  |> filter(fn: (r) => r._measurement == "mem")  |> filter(fn: (r) => r._field == "used_percent")  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)  |> yield(name: "mean")
      colors:
        - name: laser
          type: text
          hex: "#8F8AF4"
          value: 3
        - name: android
          type: scale
          hex: "#F4CF31"
          value: 1
      axes:
        - name : "x"
          label: x_label
          prefix: x_prefix
          suffix: x_suffix
          base: 10
          scale: linear
`,
						},
					}

					for _, tt := range tests {
						testTemplateErrors(t, KindDashboard, tt)
					}
				})
			})

			t.Run("table chart", func(t *testing.T) {
				t.Run("happy path", func(t *testing.T) {
					testfileRunner(t, "testdata/dashboard_table", func(t *testing.T, template *Template) {
						sum := template.Summary()
						require.Len(t, sum.Dashboards, 1)

						actual := sum.Dashboards[0]
						assert.Equal(t, KindDashboard, actual.Kind)
						assert.Equal(t, "dash-1", actual.Name)
						assert.Equal(t, "desc1", actual.Description)

						require.Len(t, actual.Charts, 1)
						actualChart := actual.Charts[0]
						assert.Equal(t, 3, actualChart.Height)
						assert.Equal(t, 6, actualChart.Width)
						assert.Equal(t, 1, actualChart.XPosition)
						assert.Equal(t, 2, actualChart.YPosition)

						props, ok := actualChart.Properties.(influxdb.TableViewProperties)
						require.True(t, ok)
						assert.Equal(t, "table note", props.Note)
						assert.True(t, props.ShowNoteWhenEmpty)
						assert.True(t, props.DecimalPlaces.IsEnforced)
						assert.Equal(t, int32(1), props.DecimalPlaces.Digits)
						assert.Equal(t, "YYYY:MMMM:DD", props.TimeFormat)

						require.Len(t, props.Queries, 1)
						q := props.Queries[0]
						expectedQuery := `from(bucket: v.bucket)  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)  |> filter(fn: (r) => r._measurement == "boltdb_writes_total")  |> filter(fn: (r) => r._field == "counter")`
						assert.Equal(t, expectedQuery, q.Text)
						assert.Equal(t, "advanced", q.EditMode)

						require.Len(t, props.ViewColors, 1)
						c := props.ViewColors[0]
						assert.Equal(t, "laser", c.Name)
						assert.Equal(t, "min", c.Type)
						assert.Equal(t, "#8F8AF4", c.Hex)
						assert.Equal(t, 3.0, c.Value)

						tableOpts := props.TableOptions
						assert.True(t, tableOpts.VerticalTimeAxis)
						assert.Equal(t, "_time", tableOpts.SortBy.InternalName)
						assert.Equal(t, "truncate", tableOpts.Wrapping)
						assert.True(t, tableOpts.FixFirstColumn)

						assert.Contains(t, props.FieldOptions, influxdb.RenamableField{
							InternalName: "_value",
							DisplayName:  "MB",
							Visible:      true,
						})
						assert.Contains(t, props.FieldOptions, influxdb.RenamableField{
							InternalName: "_time",
							DisplayName:  "time (ms)",
							Visible:      true,
						})
					})
				})

				t.Run("handles invalid config", func(t *testing.T) {
					tests := []testTemplateResourceError{
						{
							name:           "color missing hex value",
							validationErrs: 1,
							valFields:      []string{fieldSpec, "charts[0].colors[0].hex"},
							templateStr: `
apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name: dash-1
spec:
  description: desc1
  charts:
    - kind:   Table
      name:   table
      xPos:  1
      yPos:  2
      width:  6
      height: 3
      queries:
        - query: >
            from(bucket: v.bucket)  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)  |> filter(fn: (r) => r._measurement == "boltdb_writes_total")  |> filter(fn: (r) => r._field == "counter")
      colors:
        - name: laser
          type: min
          hex:
          value: 3.0`,
						},
						{
							name:           "no width provided",
							validationErrs: 1,
							valFields:      []string{fieldSpec, "charts[0].width"},
							templateStr: `
apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name: dash-1
spec:
  description: desc1
  charts:
    - kind:   Table
      name:   table
      xPos:  1
      yPos:  2
      height: 3
      queries:
        - query: >
            from(bucket: v.bucket)  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)  |> filter(fn: (r) => r._measurement == "boltdb_writes_total")  |> filter(fn: (r) => r._field == "counter")
      colors:
        - name: laser
          type: min
          hex: peru
          value: 3.0`,
						},
						{
							name:           "no height provided",
							validationErrs: 1,
							valFields:      []string{fieldSpec, "charts[0].height"},
							templateStr: `
apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name: dash-1
spec:
  description: desc1
  charts:
    - kind:   Table
      name:   table
      xPos:  1
      yPos:  2
      width:  6
      queries:
        - query: >
            from(bucket: v.bucket)  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)  |> filter(fn: (r) => r._measurement == "boltdb_writes_total")  |> filter(fn: (r) => r._field == "counter")
      colors:
        - name: laser
          type: min
          hex: peru
          value: 3.0`,
						},
						{
							name:           "invalid wrapping table option",
							validationErrs: 1,
							valFields:      []string{fieldSpec, "charts[0].tableOptions.wrapping"},
							templateStr: `
apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name: dash-1
spec:
  description: desc1
  charts:
    - kind:   Table
      name:   table
      xPos:  1
      yPos:  2
      width:  6
      height: 3
      tableOptions:
        sortBy: _time
        wrapping: WRONGO wrapping
      queries:
        - query: >
            from(bucket: v.bucket)  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)  |> filter(fn: (r) => r._measurement == "boltdb_writes_total")  |> filter(fn: (r) => r._field == "counter")
      colors:
        - name: laser
          type: min
          hex: "#8F8AF4"
          value: 3.0
`,
						},
					}

					for _, tt := range tests {
						testTemplateErrors(t, KindDashboard, tt)
					}
				})
			})

			t.Run("xy chart", func(t *testing.T) {
				t.Run("happy path", func(t *testing.T) {
					testfileRunner(t, "testdata/dashboard_xy", func(t *testing.T, template *Template) {
						sum := template.Summary()
						require.Len(t, sum.Dashboards, 1)

						actual := sum.Dashboards[0]
						assert.Equal(t, KindDashboard, actual.Kind)
						assert.Equal(t, "dash-1", actual.Name)
						assert.Equal(t, "desc1", actual.Description)

						require.Len(t, actual.Charts, 1)
						actualChart := actual.Charts[0]
						assert.Equal(t, 3, actualChart.Height)
						assert.Equal(t, 6, actualChart.Width)
						assert.Equal(t, 1, actualChart.XPosition)
						assert.Equal(t, 2, actualChart.YPosition)

						props, ok := actualChart.Properties.(influxdb.XYViewProperties)
						require.True(t, ok)
						assert.Equal(t, "xy", props.GetType())
						assert.Equal(t, true, props.ShadeBelow)
						assert.Equal(t, "y", props.HoverDimension)
						assert.Equal(t, "xy chart note", props.Note)
						assert.True(t, props.ShowNoteWhenEmpty)
						assert.Equal(t, "stacked", props.Position)
						assert.Equal(t, []string{"xTotalTicks", "xTickStart", "xTickStep"}, props.GenerateXAxisTicks)
						assert.Equal(t, 15, props.XTotalTicks)
						assert.Equal(t, 0.0, props.XTickStart)
						assert.Equal(t, 1000.0, props.XTickStep)
						assert.Equal(t, []string{"yTotalTicks", "yTickStart", "yTickStep"}, props.GenerateYAxisTicks)
						assert.Equal(t, 10, props.YTotalTicks)
						assert.Equal(t, 0.0, props.YTickStart)
						assert.Equal(t, 100.0, props.YTickStep)
						assert.Equal(t, true, props.LegendColorizeRows)
						assert.Equal(t, false, props.LegendHide)
						assert.Equal(t, 1.0, props.LegendOpacity)
						assert.Equal(t, 5, props.LegendOrientationThreshold)
						assert.Equal(t, true, props.StaticLegend.ColorizeRows)
						assert.Equal(t, 0.2, props.StaticLegend.HeightRatio)
						assert.Equal(t, true, props.StaticLegend.Show)
						assert.Equal(t, 1.0, props.StaticLegend.Opacity)
						assert.Equal(t, 5, props.StaticLegend.OrientationThreshold)
						assert.Equal(t, "y", props.StaticLegend.ValueAxis)
						assert.Equal(t, 1.0, props.StaticLegend.WidthRatio)

						require.Len(t, props.Queries, 1)
						q := props.Queries[0]
						queryText := `from(bucket: v.bucket)  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)  |> filter(fn: (r) => r._measurement == "boltdb_writes_total")  |> filter(fn: (r) => r._field == "counter")`
						assert.Equal(t, queryText, q.Text)
						assert.Equal(t, "advanced", q.EditMode)

						require.Len(t, props.ViewColors, 1)
						c := props.ViewColors[0]
						assert.Equal(t, "laser", c.Name)
						assert.Equal(t, "scale", c.Type)
						assert.Equal(t, "#8F8AF4", c.Hex)
						assert.Equal(t, 3.0, c.Value)
					})
				})

				t.Run("handles invalid config", func(t *testing.T) {
					tests := []testTemplateResourceError{
						{
							name:           "color missing hex value",
							validationErrs: 1,
							valFields:      []string{fieldSpec, "charts[0].colors[0].hex"},
							templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name: dash-1
spec:
  description: desc1
  charts:
    - kind:   XY
      name:   xy chart
      xPos:  1
      yPos:  2
      width:  6
      height: 3
      geom: line
      position: stacked
      queries:
        - query: >
            from(bucket: v.bucket)  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)  |> filter(fn: (r) => r._measurement == "boltdb_writes_total")  |> filter(fn: (r) => r._field == "counter")
      colors:
        - name: laser
          type: scale
          value: 3
      axes:
        - name : "x"
          label: x_label
          prefix: x_prefix
          suffix: x_suffix
          base: 10
          scale: linear
        - name: "y"
          label: y_label
          prefix: y_prefix
          suffix: y_suffix
          base: 10
          scale: linear
`,
						},
						{
							name:           "invalid geom flag",
							validationErrs: 1,
							valFields:      []string{fieldSpec, fieldDashCharts, fieldChartGeom},
							templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name: dash-1
spec:
  description: desc1
  charts:
    - kind:   XY
      name:   xy chart
      xPos:  1
      yPos:  2
      width:  6
      height: 3
      position: stacked
      staticLegend:
      queries:
        - query: >
            from(bucket: v.bucket)  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)  |> filter(fn: (r) => r._measurement == "boltdb_writes_total")  |> filter(fn: (r) => r._field == "counter")
      colors:
        - name: laser
          type: scale
          hex: "#8F8AF4"
          value: 3
      axes:
        - name : "x"
          label: x_label
          prefix: x_prefix
          suffix: x_suffix
          base: 10
          scale: linear
        - name: "y"
          label: y_label
          prefix: y_prefix
          suffix: y_suffix
          base: 10
          scale: linear
`,
						},
					}

					for _, tt := range tests {
						testTemplateErrors(t, KindDashboard, tt)
					}
				})
			})
		})

		t.Run("with params option should be parameterizable", func(t *testing.T) {
			testfileRunner(t, "testdata/dashboard_params.yml", func(t *testing.T, template *Template) {
				sum := template.Summary()
				require.Len(t, sum.Dashboards, 1)

				actual := sum.Dashboards[0]
				assert.Equal(t, KindDashboard, actual.Kind)
				assert.Equal(t, "dash-1", actual.MetaName)

				require.Len(t, actual.Charts, 1)
				actualChart := actual.Charts[0]
				assert.Equal(t, 3, actualChart.Height)
				assert.Equal(t, 6, actualChart.Width)
				assert.Equal(t, 1, actualChart.XPosition)
				assert.Equal(t, 2, actualChart.YPosition)

				props, ok := actualChart.Properties.(influxdb.SingleStatViewProperties)
				require.True(t, ok)
				assert.Equal(t, "single-stat", props.GetType())

				require.Len(t, props.Queries, 1)

				// parmas
				queryText := `option params = {
    bucket: "bar",
    start: -24h0m0s,
    stop: now(),
    name: "max",
    floatVal: 37.2,
    minVal: 10,
}

from(bucket: params.bucket)
    |> range(start: params.start, stop: params.stop)
    |> filter(fn: (r) => r._measurement == "processes")
    |> filter(fn: (r) => r.floater == params.floatVal)
    |> filter(fn: (r) => r._value > params.minVal)
    |> aggregateWindow(every: v.windowPeriod, fn: max)
    |> yield(name: params.name)
`

				q := props.Queries[0]
				assert.Equal(t, queryText, q.Text)
				assert.Equal(t, "advanced", q.EditMode)

				expectedRefs := []SummaryReference{
					{
						Field:        "spec.charts[0].queries[0].params.bucket",
						EnvRefKey:    `dashboards[dash-1].spec.charts[0].queries[0].params.bucket`,
						ValType:      "string",
						DefaultValue: "bar",
					},
					{
						Field:        "spec.charts[0].queries[0].params.floatVal",
						EnvRefKey:    `dashboards[dash-1].spec.charts[0].queries[0].params.floatVal`,
						ValType:      "float",
						DefaultValue: 37.2,
					},
					{
						Field:        "spec.charts[0].queries[0].params.minVal",
						EnvRefKey:    `dashboards[dash-1].spec.charts[0].queries[0].params.minVal`,
						ValType:      "integer",
						DefaultValue: int64(10),
					},
					{
						Field:        "spec.charts[0].queries[0].params.name",
						EnvRefKey:    `dashboards[dash-1].spec.charts[0].queries[0].params.name`,
						ValType:      "string",
						DefaultValue: "max",
					},
					{
						Field:        "spec.charts[0].queries[0].params.start",
						EnvRefKey:    `dashboards[dash-1].spec.charts[0].queries[0].params.start`,
						ValType:      "duration",
						DefaultValue: "-24h0m0s",
					},
					{
						Field:        "spec.charts[0].queries[0].params.stop",
						EnvRefKey:    `dashboards[dash-1].spec.charts[0].queries[0].params.stop`,
						ValType:      "time",
						DefaultValue: "now()",
					},
				}
				assert.Equal(t, expectedRefs, actual.EnvReferences)
			})
		})

		t.Run("with env refs should be valid", func(t *testing.T) {
			testfileRunner(t, "testdata/dashboard_ref.yml", func(t *testing.T, template *Template) {
				actual := template.Summary().Dashboards
				require.Len(t, actual, 1)

				expected := []SummaryReference{
					{
						Field:     "spec.associations[0].name",
						EnvRefKey: "label-meta-name",
					},
					{
						Field:        "metadata.name",
						EnvRefKey:    "meta-name",
						DefaultValue: "meta",
					},
					{
						Field:        "spec.name",
						EnvRefKey:    "spec-name",
						DefaultValue: "spectacles",
					},
				}
				assert.Equal(t, expected, actual[0].EnvReferences)
			})
		})

		t.Run("and labels associated should be successful", func(t *testing.T) {
			t.Run("happy path", func(t *testing.T) {
				testfileRunner(t, "testdata/dashboard_associates_label", func(t *testing.T, template *Template) {
					sum := template.Summary()
					require.Len(t, sum.Dashboards, 1)

					actual := sum.Dashboards[0]
					assert.Equal(t, "dash-1", actual.Name)

					require.Len(t, actual.LabelAssociations, 2)
					assert.Equal(t, "label-1", actual.LabelAssociations[0].Name)
					assert.Equal(t, "label-2", actual.LabelAssociations[1].Name)

					expectedMappings := []SummaryLabelMapping{
						{
							Status:           StateStatusNew,
							ResourceType:     influxdb.DashboardsResourceType,
							ResourceMetaName: "dash-1",
							ResourceName:     "dash-1",
							LabelMetaName:    "label-1",
							LabelName:        "label-1",
						},
						{
							Status:           StateStatusNew,
							ResourceType:     influxdb.DashboardsResourceType,
							ResourceMetaName: "dash-1",
							ResourceName:     "dash-1",
							LabelMetaName:    "label-2",
							LabelName:        "label-2",
						},
					}

					for _, expectedMapping := range expectedMappings {
						assert.Contains(t, sum.LabelMappings, expectedMapping)
					}
				})
			})

			t.Run("association doesn't exist then provides an error", func(t *testing.T) {
				tests := []testTemplateResourceError{
					{
						name:    "no labels provided",
						assErrs: 1,
						assIdxs: []int{0},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name: dash-1
spec:
  associations:
    - kind: Label
      name: label-1
`,
					},
					{
						name:    "mixed found and not found",
						assErrs: 1,
						assIdxs: []int{1},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Label
metadata:
  name: label-1
---
apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name: dash-1
spec:
  associations:
    - kind: Label
      name: label-1
    - kind: Label
      name: unfound label
`,
					},
					{
						name:    "multiple not found",
						assErrs: 1,
						assIdxs: []int{0, 1},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Label
metadata:
  name: label-1
---
apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name: dash-1
spec:
  associations:
    - kind: Label
      name: not found 1
    - kind: Label
      name: unfound label
`,
					},
					{
						name:    "duplicate valid nested labels",
						assErrs: 1,
						assIdxs: []int{1},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Label
metadata:
  name: label-1
---
apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
  name: dash-1
spec:
  associations:
    - kind: Label
      name: label-1
    - kind: Label
      name: label-1
`,
					},
				}

				for _, tt := range tests {
					testTemplateErrors(t, KindDashboard, tt)
				}
			})
		})
	})

	t.Run("template with notification endpoints", func(t *testing.T) {
		t.Run("and labels associated should be successful", func(t *testing.T) {
			testfileRunner(t, "testdata/notification_endpoint", func(t *testing.T, template *Template) {
				expectedEndpoints := []SummaryNotificationEndpoint{
					{
						SummaryIdentifier: SummaryIdentifier{
							Kind:     KindNotificationEndpointHTTP,
							MetaName: "http-basic-auth-notification-endpoint",
						},
						NotificationEndpoint: &endpoint.HTTP{
							Base: endpoint.Base{
								Name:        "basic endpoint name",
								Description: "http basic auth desc",
								Status:      taskmodel.TaskStatusInactive,
							},
							URL:        "https://www.example.com/endpoint/basicauth",
							AuthMethod: "basic",
							Method:     "POST",
							Username:   influxdb.SecretField{Value: strPtr("secret username")},
							Password:   influxdb.SecretField{Value: strPtr("secret password")},
						},
					},
					{
						SummaryIdentifier: SummaryIdentifier{
							Kind:     KindNotificationEndpointHTTP,
							MetaName: "http-bearer-auth-notification-endpoint",
						},
						NotificationEndpoint: &endpoint.HTTP{
							Base: endpoint.Base{
								Name:        "http-bearer-auth-notification-endpoint",
								Description: "http bearer auth desc",
								Status:      taskmodel.TaskStatusActive,
							},
							URL:        "https://www.example.com/endpoint/bearerauth",
							AuthMethod: "bearer",
							Method:     "PUT",
							Token:      influxdb.SecretField{Value: strPtr("secret token")},
						},
					},
					{
						SummaryIdentifier: SummaryIdentifier{
							Kind:     KindNotificationEndpointHTTP,
							MetaName: "http-none-auth-notification-endpoint",
						},
						NotificationEndpoint: &endpoint.HTTP{
							Base: endpoint.Base{
								Name:        "http-none-auth-notification-endpoint",
								Description: "http none auth desc",
								Status:      taskmodel.TaskStatusActive,
							},
							URL:        "https://www.example.com/endpoint/noneauth",
							AuthMethod: "none",
							Method:     "GET",
						},
					},
					{
						SummaryIdentifier: SummaryIdentifier{
							Kind:     KindNotificationEndpointPagerDuty,
							MetaName: "pager-duty-notification-endpoint",
						},
						NotificationEndpoint: &endpoint.PagerDuty{
							Base: endpoint.Base{
								Name:        "pager duty name",
								Description: "pager duty desc",
								Status:      taskmodel.TaskStatusActive,
							},
							ClientURL:  "http://localhost:8080/orgs/7167eb6719fa34e5/alert-history",
							RoutingKey: influxdb.SecretField{Value: strPtr("secret routing-key")},
						},
					},
					{
						SummaryIdentifier: SummaryIdentifier{
							Kind:     KindNotificationEndpointHTTP,
							MetaName: "slack-notification-endpoint",
						},
						NotificationEndpoint: &endpoint.Slack{
							Base: endpoint.Base{
								Name:        "slack name",
								Description: "slack desc",
								Status:      taskmodel.TaskStatusActive,
							},
							URL:   "https://hooks.slack.com/services/bip/piddy/boppidy",
							Token: influxdb.SecretField{Value: strPtr("tokenval")},
						},
					},
				}

				sum := template.Summary()
				endpoints := sum.NotificationEndpoints
				require.Len(t, endpoints, len(expectedEndpoints))
				require.Len(t, sum.LabelMappings, len(expectedEndpoints))

				for i := range expectedEndpoints {
					expected, actual := expectedEndpoints[i], endpoints[i]
					assert.Equalf(t, expected.NotificationEndpoint, actual.NotificationEndpoint, "index=%d", i)
					require.Len(t, actual.LabelAssociations, 1)
					assert.Equal(t, "label-1", actual.LabelAssociations[0].Name)

					assert.Contains(t, sum.LabelMappings, SummaryLabelMapping{
						Status:           StateStatusNew,
						ResourceType:     influxdb.NotificationEndpointResourceType,
						ResourceMetaName: expected.MetaName,
						ResourceName:     expected.NotificationEndpoint.GetName(),
						LabelMetaName:    "label-1",
						LabelName:        "label-1",
					})
				}
			})
		})

		t.Run("with env refs should be valid", func(t *testing.T) {
			testfileRunner(t, "testdata/notification_endpoint_ref.yml", func(t *testing.T, template *Template) {
				actual := template.Summary().NotificationEndpoints
				require.Len(t, actual, 1)

				expectedEnvRefs := []SummaryReference{
					{
						Field:        "metadata.name",
						EnvRefKey:    "meta-name",
						DefaultValue: "meta",
					},
					{
						Field:        "spec.name",
						EnvRefKey:    "spec-name",
						DefaultValue: "spectacles",
					},
					{
						Field:     "spec.associations[0].name",
						EnvRefKey: "label-meta-name",
					},
				}
				assert.Equal(t, expectedEnvRefs, actual[0].EnvReferences)
			})
		})

		t.Run("handles bad config", func(t *testing.T) {
			tests := []struct {
				kind   Kind
				resErr testTemplateResourceError
			}{
				{
					kind: KindNotificationEndpointSlack,
					resErr: testTemplateResourceError{
						name:           "missing slack url",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldNotificationEndpointURL},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: NotificationEndpointSlack
metadata:
  name: slack-notification-endpoint
spec:
`,
					},
				},
				{
					kind: KindNotificationEndpointPagerDuty,
					resErr: testTemplateResourceError{
						name:           "missing pager duty url",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldNotificationEndpointURL},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: NotificationEndpointPagerDuty
metadata:
  name: pager-duty-notification-endpoint
spec:
`,
					},
				},
				{
					kind: KindNotificationEndpointHTTP,
					resErr: testTemplateResourceError{
						name:           "missing http url",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldNotificationEndpointURL},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: NotificationEndpointHTTP
metadata:
  name: http-none-auth-notification-endpoint
spec:
  type: none
  method: get
`,
					},
				},
				{
					kind: KindNotificationEndpointHTTP,
					resErr: testTemplateResourceError{
						name:           "bad url",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldNotificationEndpointURL},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: NotificationEndpointHTTP
metadata:
  name: http-none-auth-notification-endpoint
spec:
  type: none
  method: get
  url: d_____-_8**(*https://www.examples.coms
`,
					},
				},
				{
					kind: KindNotificationEndpointHTTP,
					resErr: testTemplateResourceError{
						name:           "missing http method",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldNotificationEndpointHTTPMethod},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: NotificationEndpointHTTP
metadata:
  name: http-none-auth-notification-endpoint
spec:
  type: none
  url:  https://www.example.com/endpoint/noneauth
`,
					},
				},
				{
					kind: KindNotificationEndpointHTTP,
					resErr: testTemplateResourceError{
						name:           "invalid http method",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldNotificationEndpointHTTPMethod},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: NotificationEndpointHTTP
metadata:
  name: http-basic-auth-notification-endpoint
spec:
  type: none
  description: http none auth desc
  method: GHOST
  url:  https://www.example.com/endpoint/noneauth
`,
					},
				},
				{
					kind: KindNotificationEndpointHTTP,
					resErr: testTemplateResourceError{
						name:           "missing basic username",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldNotificationEndpointUsername},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: NotificationEndpointHTTP
metadata:
  name: http-basic-auth-notification-endpoint
spec:
  type: basic
  method: POST
  url:  https://www.example.com/endpoint/basicauth
  password: "secret password"
`,
					},
				},
				{
					kind: KindNotificationEndpointHTTP,
					resErr: testTemplateResourceError{
						name:           "missing basic password",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldNotificationEndpointPassword},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: NotificationEndpointHTTP
metadata:
  name: http-basic-auth-notification-endpoint
spec:
  type: basic
  method: POST
  url:  https://www.example.com/endpoint/basicauth
  username: username
`,
					},
				},
				{
					kind: KindNotificationEndpointHTTP,
					resErr: testTemplateResourceError{
						name:           "missing basic password and username",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldNotificationEndpointPassword, fieldNotificationEndpointUsername},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: NotificationEndpointHTTP
metadata:
  name: http-basic-auth-notification-endpoint
spec:
  description: http basic auth desc
  type: basic
  method: pOsT
  url:  https://www.example.com/endpoint/basicauth
`,
					},
				},
				{
					kind: KindNotificationEndpointHTTP,
					resErr: testTemplateResourceError{
						name:           "missing bearer token",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldNotificationEndpointToken},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: NotificationEndpointHTTP
metadata:
  name: http-bearer-auth-notification-endpoint
spec:
  description: http bearer auth desc
  type: bearer
  method: puT
  url:  https://www.example.com/endpoint/bearerauth
`,
					},
				},
				{
					kind: KindNotificationEndpointHTTP,
					resErr: testTemplateResourceError{
						name:           "invalid http type",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldType},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: NotificationEndpointHTTP
metadata:
  name: http-basic-auth-notification-endpoint
spec:
  type: RANDOM WRONG TYPE
  description: http none auth desc
  method: get
  url:  https://www.example.com/endpoint/noneauth
`,
					},
				},
				{
					kind: KindNotificationEndpointSlack,
					resErr: testTemplateResourceError{
						name:           "duplicate endpoints",
						validationErrs: 1,
						valFields:      []string{fieldMetadata, fieldName},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: NotificationEndpointSlack
metadata:
  name: slack-notification-endpoint
spec:
  url: https://hooks.slack.com/services/bip/piddy/boppidy
---
apiVersion: influxdata.com/v2alpha1
kind: NotificationEndpointSlack
metadata:
  name: slack_notification_endpoint
spec:
  url: https://hooks.slack.com/services/bip/piddy/boppidy
`,
					},
				},
				{
					kind: KindNotificationEndpointSlack,
					resErr: testTemplateResourceError{
						name:           "invalid status",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldStatus},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: NotificationEndpointSlack
metadata:
  name: slack-notification-endpoint
spec:
  description: slack desc
  url: https://hooks.slack.com/services/bip/piddy/boppidy
  status: RANDO STATUS
`,
					},
				},
				{
					kind: KindNotificationEndpointSlack,
					resErr: testTemplateResourceError{
						name:           "duplicate meta name and spec name",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldName},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: NotificationEndpointSlack
metadata:
  name: slack
spec:
  description: slack desc
  url: https://hooks.slack.com/services/bip/piddy/boppidy
---
apiVersion: influxdata.com/v2alpha1
kind: NotificationEndpointSlack
metadata:
  name: slack-notification-endpoint
spec:
  name: slack
  description: slack desc
  url: https://hooks.slack.com/services/bip/piddy/boppidy
`,
					},
				},
			}

			for _, tt := range tests {
				testTemplateErrors(t, tt.kind, tt.resErr)
			}
		})
	})

	t.Run("template with notification rules", func(t *testing.T) {
		t.Run("happy path", func(t *testing.T) {
			testfileRunner(t, "testdata/notification_rule", func(t *testing.T, template *Template) {
				sum := template.Summary()
				rules := sum.NotificationRules
				require.Len(t, rules, 1)

				rule := rules[0]
				assert.Equal(t, KindNotificationRule, rule.Kind)
				assert.Equal(t, "rule_0", rule.Name)
				assert.Equal(t, "endpoint-0", rule.EndpointMetaName)
				assert.Equal(t, "desc_0", rule.Description)
				assert.Equal(t, (10 * time.Minute).String(), rule.Every)
				assert.Equal(t, (30 * time.Second).String(), rule.Offset)
				expectedMsgTempl := "Notification Rule: ${ r._notification_rule_name } triggered by check: ${ r._check_name }: ${ r._message }"
				assert.Equal(t, expectedMsgTempl, rule.MessageTemplate)
				assert.Equal(t, influxdb.Active, rule.Status)

				expectedStatusRules := []SummaryStatusRule{
					{CurrentLevel: "CRIT", PreviousLevel: "OK"},
					{CurrentLevel: "WARN"},
				}
				assert.Equal(t, expectedStatusRules, rule.StatusRules)

				expectedTagRules := []SummaryTagRule{
					{Key: "k1", Value: "v1", Operator: "equal"},
					{Key: "k1", Value: "v2", Operator: "equal"},
				}
				assert.Equal(t, expectedTagRules, rule.TagRules)

				require.Len(t, sum.Labels, 2)
				require.Len(t, rule.LabelAssociations, 2)
				assert.Equal(t, "label-1", rule.LabelAssociations[0].MetaName)
				assert.Equal(t, "label-2", rule.LabelAssociations[1].MetaName)
			})
		})

		t.Run("with env refs should be valid", func(t *testing.T) {
			testfileRunner(t, "testdata/notification_rule_ref.yml", func(t *testing.T, template *Template) {
				actual := template.Summary().NotificationRules
				require.Len(t, actual, 1)

				expectedEnvRefs := []SummaryReference{
					{
						Field:        "metadata.name",
						EnvRefKey:    "meta-name",
						DefaultValue: "meta",
					},
					{
						Field:        "spec.name",
						EnvRefKey:    "spec-name",
						DefaultValue: "spectacles",
					},
					{
						Field:     "spec.associations[0].name",
						EnvRefKey: "label-meta-name",
					},
					{
						Field:     "spec.endpointName",
						EnvRefKey: "endpoint-meta-name",
					},
				}
				assert.Equal(t, expectedEnvRefs, actual[0].EnvReferences)
			})
		})

		t.Run("handles bad config", func(t *testing.T) {
			templateWithValidEndpint := func(resource string) string {
				return fmt.Sprintf(`
apiVersion: influxdata.com/v2alpha1
kind: NotificationEndpointSlack
metadata:
  name: endpoint-0
spec:
  url: https://hooks.slack.com/services/bip/piddy/boppidy
---
%s
`, resource)
			}

			tests := []struct {
				kind   Kind
				resErr testTemplateResourceError
			}{
				{
					kind: KindNotificationRule,
					resErr: testTemplateResourceError{
						name:      "missing name",
						valFields: []string{fieldMetadata, fieldName},
						templateStr: templateWithValidEndpint(`apiVersion: influxdata.com/v2alpha1
kind: NotificationRule
metadata:
spec:
  endpointName: endpoint-0
  every: 10m
  messageTemplate: "Notification Rule: ${ r._notification_rule_name } triggered by check: ${ r._check_name }: ${ r._message }"
  statusRules:
    - currentLevel: WARN
`),
					},
				},
				{
					kind: KindNotificationRule,
					resErr: testTemplateResourceError{
						name:      "missing endpoint name",
						valFields: []string{fieldSpec, fieldNotificationRuleEndpointName},
						templateStr: templateWithValidEndpint(`apiVersion: influxdata.com/v2alpha1
kind: NotificationRule
metadata:
  name: rule-0
spec:
  every: 10m
  messageTemplate: "Notification Rule: ${ r._notification_rule_name } triggered by check: ${ r._check_name }: ${ r._message }"
  statusRules:
    - currentLevel: WARN
`),
					},
				},
				{
					kind: KindNotificationRule,
					resErr: testTemplateResourceError{
						name:      "missing every",
						valFields: []string{fieldSpec, fieldEvery},
						templateStr: templateWithValidEndpint(`apiVersion: influxdata.com/v2alpha1
kind: NotificationRule
metadata:
  name: rule-0
spec:
  endpointName: endpoint-0
  messageTemplate: "Notification Rule: ${ r._notification_rule_name } triggered by check: ${ r._check_name }: ${ r._message }"
  statusRules:
    - currentLevel: WARN
`),
					},
				},
				{
					kind: KindNotificationRule,
					resErr: testTemplateResourceError{
						name:      "missing status rules",
						valFields: []string{fieldSpec, fieldNotificationRuleStatusRules},
						templateStr: templateWithValidEndpint(`apiVersion: influxdata.com/v2alpha1
kind: NotificationRule
metadata:
  name: rule-0
spec:
  every: 10m
  endpointName: endpoint-0
  messageTemplate: "Notification Rule: ${ r._notification_rule_name } triggered by check: ${ r._check_name }: ${ r._message }"
`),
					},
				},
				{
					kind: KindNotificationRule,
					resErr: testTemplateResourceError{
						name:      "bad current status rule level",
						valFields: []string{fieldSpec, fieldNotificationRuleStatusRules},
						templateStr: templateWithValidEndpint(`apiVersion: influxdata.com/v2alpha1
kind: NotificationRule
metadata:
  name: rule-0
spec:
  every: 10m
  endpointName: endpoint-0
  messageTemplate: "Notification Rule: ${ r._notification_rule_name } triggered by check: ${ r._check_name }: ${ r._message }"
  statusRules:
    - currentLevel: WRONGO
`),
					},
				},
				{
					kind: KindNotificationRule,
					resErr: testTemplateResourceError{
						name:      "bad previous status rule level",
						valFields: []string{fieldSpec, fieldNotificationRuleStatusRules},
						templateStr: templateWithValidEndpint(`apiVersion: influxdata.com/v2alpha1
kind: NotificationRule
metadata:
  name: rule-0
spec:
  endpointName: endpoint-0
  every: 10m
  messageTemplate: "Notification Rule: ${ r._notification_rule_name } triggered by check: ${ r._check_name }: ${ r._message }"
  statusRules:
    - currentLevel: CRIT
      previousLevel: WRONG
`),
					},
				},
				{
					kind: KindNotificationRule,
					resErr: testTemplateResourceError{
						name:      "bad tag rule operator",
						valFields: []string{fieldSpec, fieldNotificationRuleTagRules},
						templateStr: templateWithValidEndpint(`apiVersion: influxdata.com/v2alpha1
kind: NotificationRule
metadata:
  name: rule-0
spec:
  endpointName: endpoint-0
  every: 10m
  messageTemplate: "Notification Rule: ${ r._notification_rule_name } triggered by check: ${ r._check_name }: ${ r._message }"
  statusRules:
    - currentLevel: WARN
  tagRules:
    - key: k1
      value: v2
      operator: WRONG
`),
					},
				},
				{
					kind: KindNotificationRule,
					resErr: testTemplateResourceError{
						name:      "bad status provided",
						valFields: []string{fieldSpec, fieldStatus},
						templateStr: templateWithValidEndpint(`apiVersion: influxdata.com/v2alpha1
kind: NotificationRule
metadata:
  name: rule-0
spec:
  endpointName: endpoint-0
  every: 10m
  messageTemplate: "Notification Rule: ${ r._notification_rule_name } triggered by check: ${ r._check_name }: ${ r._message }"
  status: RANDO STATUS
  statusRules:
    - currentLevel: WARN
`),
					},
				},
				{
					kind: KindNotificationRule,
					resErr: testTemplateResourceError{
						name:      "label association does not exist",
						valFields: []string{fieldSpec, fieldAssociations},
						templateStr: templateWithValidEndpint(`apiVersion: influxdata.com/v2alpha1
kind: NotificationRule
metadata:
  name: rule-0
spec:
  endpointName: endpoint-0
  every: 10m
  messageTemplate: "Notification Rule: ${ r._notification_rule_name } triggered by check: ${ r._check_name }: ${ r._message }"
  statusRules:
    - currentLevel: WARN
  associations:
    - kind: Label
      name: label-1
`),
					},
				},
				{
					kind: KindNotificationRule,
					resErr: testTemplateResourceError{
						name:      "label association dupe",
						valFields: []string{fieldSpec, fieldAssociations},
						templateStr: templateWithValidEndpint(`apiVersion: influxdata.com/v2alpha1
kind: Label
metadata:
  name: label-1
---
apiVersion: influxdata.com/v2alpha1
kind: NotificationRule
metadata:
  name: rule-0
spec:
  endpointName: endpoint-0
  every: 10m
  messageTemplate: "Notification Rule: ${ r._notification_rule_name } triggered by check: ${ r._check_name }: ${ r._message }"
  statusRules:
    - currentLevel: WARN
  associations:
    - kind: Label
      name: label-1
    - kind: Label
      name: label-1
`),
					},
				},
				{
					kind: KindNotificationRule,
					resErr: testTemplateResourceError{
						name:      "duplicate meta names",
						valFields: []string{fieldMetadata, fieldName},
						templateStr: templateWithValidEndpint(`
apiVersion: influxdata.com/v2alpha1
kind: NotificationRule
metadata:
  name: rule-0
spec:
  endpointName: endpoint-0
  every: 10m
  messageTemplate: "Notification Rule: ${ r._notification_rule_name } triggered by check: ${ r._check_name }: ${ r._message }"
  statusRules:
    - currentLevel: WARN
---
apiVersion: influxdata.com/v2alpha1
kind: NotificationRule
metadata:
  name: rule-0
spec:
  endpointName: endpoint-0
  every: 10m
  messageTemplate: "Notification Rule: ${ r._notification_rule_name } triggered by check: ${ r._check_name }: ${ r._message }"
  statusRules:
    - currentLevel: WARN
`),
					},
				},
				{
					kind: KindNotificationRule,
					resErr: testTemplateResourceError{
						name:      "missing endpoint association in template",
						valFields: []string{fieldSpec, fieldNotificationRuleEndpointName},
						templateStr: `
apiVersion: influxdata.com/v2alpha1
kind: NotificationRule
metadata:
  name: rule-0
spec:
  endpointName: RANDO_ENDPOINT_NAME
  every: 10m
  messageTemplate: "Notification Rule: ${ r._notification_rule_name } triggered by check: ${ r._check_name }: ${ r._message }"
  statusRules:
    - currentLevel: WARN
`,
					},
				},
			}

			for _, tt := range tests {
				testTemplateErrors(t, tt.kind, tt.resErr)
			}
		})
	})

	t.Run("template with tasks", func(t *testing.T) {
		t.Run("happy path", func(t *testing.T) {
			testfileRunner(t, "testdata/tasks", func(t *testing.T, template *Template) {
				sum := template.Summary()
				tasks := sum.Tasks
				require.Len(t, tasks, 2)

				for _, ta := range tasks {
					assert.Equal(t, KindTask, ta.Kind)
				}

				sort.Slice(tasks, func(i, j int) bool {
					return tasks[i].MetaName < tasks[j].MetaName
				})

				baseEqual := func(t *testing.T, i int, status influxdb.Status, actual SummaryTask) {
					t.Helper()

					assert.Equal(t, "task-"+strconv.Itoa(i), actual.Name)
					assert.Equal(t, "desc_"+strconv.Itoa(i), actual.Description)
					assert.Equal(t, status, actual.Status)

					expectedQuery := "from(bucket: \"rucket_1\")\n  |> range(start: -5d, stop: -1h)\n  |> filter(fn: (r) => r._measurement == \"cpu\")\n  |> filter(fn: (r) => r._field == \"usage_idle\")\n  |> aggregateWindow(every: 1m, fn: mean)\n  |> yield(name: \"mean\")"
					assert.Equal(t, expectedQuery, actual.Query)

					require.Len(t, actual.LabelAssociations, 1)
					assert.Equal(t, "label-1", actual.LabelAssociations[0].Name)
				}

				require.Len(t, sum.Labels, 1)

				task0 := tasks[0]
				baseEqual(t, 1, influxdb.Active, task0)
				assert.Equal(t, "15 * * * *", task0.Cron)

				task1 := tasks[1]
				baseEqual(t, 0, influxdb.Inactive, task1)
				assert.Equal(t, (25 * time.Hour).String(), task1.Every)
				assert.Equal(t, (15 * time.Second).String(), task1.Offset)
			})
		})

		t.Run("with params option should be parameterizable", func(t *testing.T) {
			testfileRunner(t, "testdata/tasks_params.yml", func(t *testing.T, template *Template) {
				sum := template.Summary()
				require.Len(t, sum.Tasks, 1)

				actual := sum.Tasks[0]
				assert.Equal(t, KindTask, actual.Kind)
				assert.Equal(t, "task-uuid", actual.MetaName)

				queryText := `option params = {
    bucket: "bar",
    start: -24h0m0s,
    stop: now(),
    name: "max",
    floatVal: 37.2,
    minVal: 10,
}

from(bucket: params.bucket)
    |> range(start: params.start, stop: params.stop)
    |> filter(fn: (r) => r._measurement == "processes")
    |> filter(fn: (r) => r.floater == params.floatVal)
    |> filter(fn: (r) => r._value > params.minVal)
    |> aggregateWindow(every: v.windowPeriod, fn: max)
    |> yield(name: params.name)
`

				assert.Equal(t, queryText, actual.Query)

				expectedRefs := []SummaryReference{
					{
						Field:        "spec.params.bucket",
						EnvRefKey:    `tasks[task-uuid].spec.params.bucket`,
						ValType:      "string",
						DefaultValue: "bar",
					},
					{
						Field:        "spec.params.floatVal",
						EnvRefKey:    `tasks[task-uuid].spec.params.floatVal`,
						ValType:      "float",
						DefaultValue: 37.2,
					},
					{
						Field:        "spec.params.minVal",
						EnvRefKey:    `tasks[task-uuid].spec.params.minVal`,
						ValType:      "integer",
						DefaultValue: int64(10),
					},
					{
						Field:        "spec.params.name",
						EnvRefKey:    `tasks[task-uuid].spec.params.name`,
						ValType:      "string",
						DefaultValue: "max",
					},
					{
						Field:        "spec.params.start",
						EnvRefKey:    `tasks[task-uuid].spec.params.start`,
						ValType:      "duration",
						DefaultValue: "-24h0m0s",
					},
					{
						Field:        "spec.params.stop",
						EnvRefKey:    `tasks[task-uuid].spec.params.stop`,
						ValType:      "time",
						DefaultValue: "now()",
					},
				}
				assert.Equal(t, expectedRefs, actual.EnvReferences)
			})
		})

		t.Run("with task option should be valid", func(t *testing.T) {
			testfileRunner(t, "testdata/task_v2.yml", func(t *testing.T, template *Template) {
				actual := template.Summary().Tasks
				require.Len(t, actual, 1)

				expectedEnvRefs := []SummaryReference{
					{
						Field:        "spec.task.every",
						EnvRefKey:    "tasks[task-1].spec.task.every",
						ValType:      "duration",
						DefaultValue: time.Minute,
					},
					{
						Field:        "spec.name",
						EnvRefKey:    "tasks[task-1].spec.task.name",
						ValType:      "string",
						DefaultValue: "bar",
					},
					{
						Field:        "spec.task.name",
						EnvRefKey:    "tasks[task-1].spec.task.name",
						ValType:      "string",
						DefaultValue: "bar",
					},
					{
						Field:        "spec.task.offset",
						EnvRefKey:    "tasks[task-1].spec.task.offset",
						ValType:      "duration",
						DefaultValue: time.Minute * 3,
					},
				}
				assert.Equal(t, expectedEnvRefs, actual[0].EnvReferences)
			})
		})

		t.Run("with task spec should be valid", func(t *testing.T) {
			testfileRunner(t, "testdata/task_v2_taskSpec.yml", func(t *testing.T, template *Template) {
				actual := template.Summary().Tasks
				require.Len(t, actual, 1)

				expectedEnvRefs := []SummaryReference{
					{
						Field:        "spec.task.every",
						EnvRefKey:    "tasks[task-1].spec.task.every",
						ValType:      "duration",
						DefaultValue: time.Minute,
					},
					{
						Field:        "spec.name",
						EnvRefKey:    "tasks[task-1].spec.task.name",
						ValType:      "string",
						DefaultValue: "foo",
					},
					{
						Field:        "spec.task.name",
						EnvRefKey:    "tasks[task-1].spec.task.name",
						ValType:      "string",
						DefaultValue: "foo",
					},
					{
						Field:        "spec.task.offset",
						EnvRefKey:    "tasks[task-1].spec.task.offset",
						ValType:      "duration",
						DefaultValue: time.Minute,
					},
				}

				queryText := `option task = {name: "foo", every: 1m0s, offset: 1m0s}

from(bucket: "rucket_1")
    |> range(start: -5d, stop: -1h)
    |> filter(fn: (r) => r._measurement == "cpu")
    |> filter(fn: (r) => r._field == "usage_idle")
    |> aggregateWindow(every: 1m, fn: mean)
    |> yield(name: "mean")
`

				assert.Equal(t, queryText, actual[0].Query)

				assert.Equal(t, expectedEnvRefs, actual[0].EnvReferences)
			})
		})

		t.Run("with params option should be valid", func(t *testing.T) {
			testfileRunner(t, "testdata/task_v2_params.yml", func(t *testing.T, template *Template) {
				actual := template.Summary().Tasks
				require.Len(t, actual, 1)

				expectedEnvRefs := []SummaryReference{
					{
						Field:        "spec.params.this",
						EnvRefKey:    "tasks[task-1].spec.params.this",
						ValType:      "string",
						DefaultValue: "foo",
					},
				}

				queryText := `option params = {this: "foo"}

from(bucket: "rucket_1")
    |> range(start: -5d, stop: -1h)
    |> filter(fn: (r) => r._measurement == params.this)
    |> filter(fn: (r) => r._field == "usage_idle")
    |> aggregateWindow(every: 1m, fn: mean)
    |> yield(name: "mean")
`

				assert.Equal(t, queryText, actual[0].Query)

				assert.Equal(t, expectedEnvRefs, actual[0].EnvReferences)
			})
		})

		t.Run("with env refs should be valid", func(t *testing.T) {
			testfileRunner(t, "testdata/task_ref.yml", func(t *testing.T, template *Template) {
				actual := template.Summary().Tasks
				require.Len(t, actual, 1)

				expectedEnvRefs := []SummaryReference{
					{
						Field:     "spec.associations[0].name",
						EnvRefKey: "label-meta-name",
					},
					{
						Field:        "metadata.name",
						EnvRefKey:    "meta-name",
						DefaultValue: "meta",
					},
					{
						Field:        "spec.name",
						EnvRefKey:    "spec-name",
						DefaultValue: "spectacles",
					},
				}
				assert.Equal(t, expectedEnvRefs, actual[0].EnvReferences)
			})
		})

		t.Run("handles bad config", func(t *testing.T) {
			tests := []struct {
				kind   Kind
				resErr testTemplateResourceError
			}{
				{
					kind: KindTask,
					resErr: testTemplateResourceError{
						name:           "missing name",
						validationErrs: 1,
						valFields:      []string{fieldMetadata, fieldName},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Task
metadata:
spec:
  description: desc_1
  cron: 15 * * * *
  query:  >
    from(bucket: "rucket_1") |> yield(name: "mean")
`,
					},
				},
				{
					kind: KindTask,
					resErr: testTemplateResourceError{
						name:           "invalid status",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldStatus},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Task
metadata:
  name: task-0
spec:
  cron: 15 * * * *
  query:  >
    from(bucket: "rucket_1") |> yield(name: "mean")
  status: RANDO WRONGO
`,
					},
				},
				{
					kind: KindTask,
					resErr: testTemplateResourceError{
						name:           "missing query",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldQuery},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Task
metadata:
  name: task-0
spec:
  description: desc_0
  every: 10m
  offset: 15s
`,
					},
				},
				{
					kind: KindTask,
					resErr: testTemplateResourceError{
						name:           "missing every and cron fields",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldEvery, fieldTaskCron},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Task
metadata:
  name: task-0
spec:
  description: desc_0
  offset: 15s
`,
					},
				},
				{
					kind: KindTask,
					resErr: testTemplateResourceError{
						name:           "invalid association",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldAssociations},
						templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Task
metadata:
  name: task-1
spec:
  cron: 15 * * * *
  query:  >
    from(bucket: "rucket_1") |> yield(name: "mean")
  associations:
    - kind: Label
      name: label-1
`,
					},
				},
				{
					kind: KindTask,
					resErr: testTemplateResourceError{
						name:           "duplicate association",
						validationErrs: 1,
						valFields:      []string{fieldSpec, fieldAssociations},
						templateStr: `---
apiVersion: influxdata.com/v2alpha1
kind: Label
metadata:
  name: label-1
---
apiVersion: influxdata.com/v2alpha1
kind: Task
metadata:
  name: task-0
spec:
  every: 10m
  offset: 15s
  query:  >
    from(bucket: "rucket_1") |> yield(name: "mean")
  status: inactive
  associations:
    - kind: Label
      name: label-1
    - kind: Label
      name: label-1
`,
					},
				},
				{
					kind: KindTask,
					resErr: testTemplateResourceError{
						name:           "duplicate meta names",
						validationErrs: 1,
						valFields:      []string{fieldMetadata, fieldName},
						templateStr: `
apiVersion: influxdata.com/v2alpha1
kind: Task
metadata:
  name: task-0
spec:
  every: 10m
  query:  >
    from(bucket: "rucket_1") |> yield(name: "mean")
---
apiVersion: influxdata.com/v2alpha1
kind: Task
metadata:
  name: task-0
spec:
  every: 10m
  query:  >
    from(bucket: "rucket_1") |> yield(name: "mean")
`,
					},
				},
			}

			for _, tt := range tests {
				testTemplateErrors(t, tt.kind, tt.resErr)
			}
		})
	})

	t.Run("template with telegraf config", func(t *testing.T) {
		t.Run("and associated labels should be successful", func(t *testing.T) {
			testfileRunner(t, "testdata/telegraf", func(t *testing.T, template *Template) {
				sum := template.Summary()
				require.Len(t, sum.TelegrafConfigs, 2)

				actual := sum.TelegrafConfigs[0]
				assert.Equal(t, KindTelegraf, actual.Kind)
				assert.Equal(t, "display name", actual.TelegrafConfig.Name)
				assert.Equal(t, "desc", actual.TelegrafConfig.Description)

				require.Len(t, actual.LabelAssociations, 2)
				assert.Equal(t, "label-1", actual.LabelAssociations[0].Name)
				assert.Equal(t, "label-2", actual.LabelAssociations[1].Name)

				actual = sum.TelegrafConfigs[1]
				assert.Equal(t, "tele-2", actual.TelegrafConfig.Name)
				assert.Empty(t, actual.LabelAssociations)

				require.Len(t, sum.LabelMappings, 2)
				expectedMapping := SummaryLabelMapping{
					Status:           StateStatusNew,
					ResourceMetaName: "first-tele-config",
					ResourceName:     "display name",
					LabelMetaName:    "label-1",
					LabelName:        "label-1",
					ResourceType:     influxdb.TelegrafsResourceType,
				}
				assert.Equal(t, expectedMapping, sum.LabelMappings[0])
				expectedMapping.LabelMetaName = "label-2"
				expectedMapping.LabelName = "label-2"
				assert.Equal(t, expectedMapping, sum.LabelMappings[1])
			})
		})

		t.Run("with env refs should be valid", func(t *testing.T) {
			testfileRunner(t, "testdata/telegraf_ref.yml", func(t *testing.T, template *Template) {
				actual := template.Summary().TelegrafConfigs
				require.Len(t, actual, 1)

				expectedEnvRefs := []SummaryReference{
					{
						Field:        "metadata.name",
						EnvRefKey:    "meta-name",
						DefaultValue: "meta",
					},
					{
						Field:        "spec.name",
						EnvRefKey:    "spec-name",
						DefaultValue: "spectacles",
					},
					{
						Field:     "spec.associations[0].name",
						EnvRefKey: "label-meta-name",
					},
				}
				assert.Equal(t, expectedEnvRefs, actual[0].EnvReferences)
			})
		})

		t.Run("handles bad config", func(t *testing.T) {
			tests := []testTemplateResourceError{
				{
					name:           "config missing",
					validationErrs: 1,
					valFields:      []string{fieldSpec, fieldTelegrafConfig},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Telegraf
metadata:
  name: first-tele-config
spec:
`,
				},
				{
					name:           "duplicate metadata names",
					validationErrs: 1,
					valFields:      []string{fieldMetadata, fieldName},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Telegraf
metadata:
  name: tele-0
spec:
  config: fake tele config
---
apiVersion: influxdata.com/v2alpha1
kind: Telegraf
metadata:
  name: tele-0
spec:
  config: fake tele config
`,
				},
			}

			for _, tt := range tests {
				testTemplateErrors(t, KindTelegraf, tt)
			}
		})
	})

	t.Run("template with a variable", func(t *testing.T) {
		t.Run("with valid fields should produce summary", func(t *testing.T) {
			testfileRunner(t, "testdata/variables", func(t *testing.T, template *Template) {
				sum := template.Summary()

				require.Len(t, sum.Variables, 4)
				for _, v := range sum.Variables {
					assert.Equal(t, KindVariable, v.Kind)
				}

				varEquals := func(t *testing.T, name, vType string, vals interface{}, selected []string, v SummaryVariable) {
					t.Helper()

					assert.Equal(t, name, v.Name)
					assert.Equal(t, name+" desc", v.Description)
					if selected == nil {
						selected = []string{}
					}
					assert.Equal(t, selected, v.Selected)
					require.NotNil(t, v.Arguments)
					assert.Equal(t, vType, v.Arguments.Type)
					assert.Equal(t, vals, v.Arguments.Values)
				}

				// validates we support all known variable types
				varEquals(t,
					"var-const-3",
					"constant",
					influxdb.VariableConstantValues([]string{"first val"}),
					nil,
					sum.Variables[0],
				)

				varEquals(t,
					"var-map-4",
					"map",
					influxdb.VariableMapValues{"k1": "v1"},
					nil,
					sum.Variables[1],
				)

				varEquals(t,
					"query var",
					"query",
					influxdb.VariableQueryValues{
						Query:    `buckets()  |> filter(fn: (r) => r.name !~ /^_/)  |> rename(columns: {name: "_value"})  |> keep(columns: ["_value"])`,
						Language: "flux",
					},
					[]string{"rucket"},
					sum.Variables[2],
				)

				varEquals(t,
					"var-query-2",
					"query",
					influxdb.VariableQueryValues{
						Query:    "an influxql query of sorts",
						Language: "influxql",
					},
					nil,
					sum.Variables[3],
				)
			})
		})

		t.Run("with env refs should be valid", func(t *testing.T) {
			testfileRunner(t, "testdata/variable_ref.yml", func(t *testing.T, template *Template) {
				actual := template.Summary().Variables
				require.Len(t, actual, 1)

				expectedEnvRefs := []SummaryReference{
					{
						Field:        "metadata.name",
						EnvRefKey:    "meta-name",
						DefaultValue: "meta",
					},
					{
						Field:        "spec.name",
						EnvRefKey:    "spec-name",
						DefaultValue: "spectacles",
					},
					{
						Field:     "spec.associations[0].name",
						EnvRefKey: "label-meta-name",
					},
					{
						Field:        "spec.selected[0]",
						EnvRefKey:    "the-selected",
						DefaultValue: "second val",
					},
					{
						Field:     "spec.selected[1]",
						EnvRefKey: "the-2nd",
					},
				}
				assert.Equal(t, expectedEnvRefs, actual[0].EnvReferences)
			})
		})

		t.Run("and labels associated", func(t *testing.T) {
			testfileRunner(t, "testdata/variable_associates_label.yml", func(t *testing.T, template *Template) {
				sum := template.Summary()
				require.Len(t, sum.Labels, 1)

				vars := sum.Variables
				require.Len(t, vars, 1)

				expectedLabelMappings := []struct {
					varName string
					labels  []string
				}{
					{
						varName: "var-1",
						labels:  []string{"label-1"},
					},
				}
				for i, expected := range expectedLabelMappings {
					v := vars[i]
					require.Len(t, v.LabelAssociations, len(expected.labels))

					for j, label := range expected.labels {
						assert.Equal(t, label, v.LabelAssociations[j].Name)
					}
				}

				expectedMappings := []SummaryLabelMapping{
					{
						Status:           StateStatusNew,
						ResourceMetaName: "var-1",
						ResourceName:     "var-1",
						LabelMetaName:    "label-1",
						LabelName:        "label-1",
					},
				}

				require.Len(t, sum.LabelMappings, len(expectedMappings))
				for i, expected := range expectedMappings {
					expected.ResourceType = influxdb.VariablesResourceType
					assert.Equal(t, expected, sum.LabelMappings[i])
				}
			})
		})

		t.Run("handles bad config", func(t *testing.T) {
			tests := []testTemplateResourceError{
				{
					name:           "name missing",
					validationErrs: 1,
					valFields:      []string{fieldMetadata, fieldName},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Variable
metadata:
spec:
  description: var-map-4 desc
  type: map
  values:
    k1: v1
`,
				},
				{
					name:           "map var missing values",
					validationErrs: 1,
					valFields:      []string{fieldSpec, fieldValues},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Variable
metadata:
  name:  var-map-4
spec:
  description: var-map-4 desc
  type: map
`,
				},
				{
					name:           "const var missing values",
					validationErrs: 1,
					valFields:      []string{fieldSpec, fieldValues},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Variable
metadata:
  name:  var-const-3
spec:
  description: var-const-3 desc
  type: constant
`,
				},
				{
					name:           "query var missing query",
					validationErrs: 1,
					valFields:      []string{fieldSpec, fieldQuery},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Variable
metadata:
  name:  var-query-2
spec:
  description: var-query-2 desc
  type: query
  language: influxql
`,
				},
				{
					name:           "query var missing query language",
					validationErrs: 1,
					valFields:      []string{fieldSpec, fieldLanguage},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Variable
metadata:
  name:  var-query-2
spec:
  description: var-query-2 desc
  type: query
  query: an influxql query of sorts
`,
				},
				{
					name:           "query var provides incorrect query language",
					validationErrs: 1,
					valFields:      []string{fieldSpec, fieldLanguage},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Variable
metadata:
  name:  var-query-2
spec:
  description: var-query-2 desc
  type: query
  query: an influxql query of sorts
  language: wrong Language
`,
				},
				{
					name:           "duplicate var names",
					validationErrs: 1,
					valFields:      []string{fieldMetadata, fieldName},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Variable
metadata:
  name:  var-query-2
spec:
  description: var-query-2 desc
  type: query
  query: an influxql query of sorts
  language: influxql
---
apiVersion: influxdata.com/v2alpha1
kind: Variable
metadata:
  name:  var-query-2
spec:
  description: var-query-2 desc
  type: query
  query: an influxql query of sorts
  language: influxql
`,
				},
				{
					name:           "duplicate meta name and spec name",
					validationErrs: 1,
					valFields:      []string{fieldSpec, fieldName},
					templateStr: `apiVersion: influxdata.com/v2alpha1
kind: Variable
metadata:
  name:  var-query-2
spec:
  description: var-query-2 desc
  type: query
  query: an influxql query of sorts
  language: influxql
---
apiVersion: influxdata.com/v2alpha1
kind: Variable
metadata:
  name:  valid-query
spec:
  name: var-query-2
  description: var-query-2 desc
  type: query
  query: an influxql query of sorts
  language: influxql
`,
				},
			}

			for _, tt := range tests {
				testTemplateErrors(t, KindVariable, tt)
			}
		})
	})

	t.Run("referencing secrets", func(t *testing.T) {
		hasSecret := func(t *testing.T, refs map[string]bool, key string) {
			t.Helper()
			b, ok := refs[key]
			assert.True(t, ok)
			assert.False(t, b)
		}

		testfileRunner(t, "testdata/notification_endpoint_secrets.yml", func(t *testing.T, template *Template) {
			sum := template.Summary()

			endpoints := sum.NotificationEndpoints
			require.Len(t, endpoints, 1)

			expected := &endpoint.PagerDuty{
				Base: endpoint.Base{
					Name:   "pager-duty-notification-endpoint",
					Status: taskmodel.TaskStatusActive,
				},
				ClientURL:  "http://localhost:8080/orgs/7167eb6719fa34e5/alert-history",
				RoutingKey: influxdb.SecretField{Key: "-routing-key", Value: strPtr("not empty")},
			}
			actual, ok := endpoints[0].NotificationEndpoint.(*endpoint.PagerDuty)
			require.True(t, ok)
			assert.Equal(t, expected.Base.Name, actual.Name)
			require.Nil(t, actual.RoutingKey.Value)
			assert.Equal(t, "routing-key", actual.RoutingKey.Key)

			hasSecret(t, template.mSecrets, "routing-key")
		})
	})

	t.Run("referencing env", func(t *testing.T) {
		hasEnv := func(t *testing.T, refs map[string]bool, key string) {
			t.Helper()
			_, ok := refs[key]
			assert.True(t, ok)
		}

		testfileRunner(t, "testdata/env_refs.yml", func(t *testing.T, template *Template) {
			sum := template.Summary()

			require.Len(t, sum.Buckets, 1)
			assert.Equal(t, "env-bkt-1-name-ref", sum.Buckets[0].Name)
			assert.Len(t, sum.Buckets[0].LabelAssociations, 1)
			hasEnv(t, template.mEnv, "bkt-1-name-ref")

			require.Len(t, sum.Checks, 1)
			assert.Equal(t, "env-check-1-name-ref", sum.Checks[0].Check.GetName())
			assert.Len(t, sum.Checks[0].LabelAssociations, 1)
			hasEnv(t, template.mEnv, "check-1-name-ref")

			require.Len(t, sum.Dashboards, 1)
			assert.Equal(t, "env-dash-1-name-ref", sum.Dashboards[0].Name)
			assert.Len(t, sum.Dashboards[0].LabelAssociations, 1)
			hasEnv(t, template.mEnv, "dash-1-name-ref")

			require.Len(t, sum.NotificationEndpoints, 1)
			assert.Equal(t, "env-endpoint-1-name-ref", sum.NotificationEndpoints[0].NotificationEndpoint.GetName())
			hasEnv(t, template.mEnv, "endpoint-1-name-ref")

			require.Len(t, sum.Labels, 1)
			assert.Equal(t, "env-label-1-name-ref", sum.Labels[0].Name)
			hasEnv(t, template.mEnv, "label-1-name-ref")

			require.Len(t, sum.NotificationRules, 1)
			assert.Equal(t, "env-rule-1-name-ref", sum.NotificationRules[0].Name)
			assert.Equal(t, "env-endpoint-1-name-ref", sum.NotificationRules[0].EndpointMetaName)
			hasEnv(t, template.mEnv, "rule-1-name-ref")

			require.Len(t, sum.Tasks, 1)
			assert.Equal(t, "env-task-1-name-ref", sum.Tasks[0].Name)
			hasEnv(t, template.mEnv, "task-1-name-ref")

			require.Len(t, sum.TelegrafConfigs, 1)
			assert.Equal(t, "env-telegraf-1-name-ref", sum.TelegrafConfigs[0].TelegrafConfig.Name)
			hasEnv(t, template.mEnv, "telegraf-1-name-ref")

			require.Len(t, sum.Variables, 1)
			assert.Equal(t, "env-var-1-name-ref", sum.Variables[0].Name)
			hasEnv(t, template.mEnv, "var-1-name-ref")

			t.Log("applying env vars should populate env fields")
			{
				err := template.applyEnvRefs(map[string]interface{}{
					"bkt-1-name-ref":   "bucket-1",
					"label-1-name-ref": "label-1",
				})
				require.NoError(t, err)

				sum := template.Summary()

				require.Len(t, sum.Buckets, 1)
				assert.Equal(t, "bucket-1", sum.Buckets[0].Name)
				assert.Len(t, sum.Buckets[0].LabelAssociations, 1)
				hasEnv(t, template.mEnv, "bkt-1-name-ref")

				require.Len(t, sum.Labels, 1)
				assert.Equal(t, "label-1", sum.Labels[0].Name)
				hasEnv(t, template.mEnv, "label-1-name-ref")
			}
		})
	})

	t.Run("jsonnet support disabled by default", func(t *testing.T) {
		template := validParsedTemplateFromFile(t, "testdata/bucket_associates_labels.jsonnet", EncodingJsonnet)
		require.Equal(t, &Template{}, template)
	})

	t.Run("jsonnet support", func(t *testing.T) {
		template := validParsedTemplateFromFile(t, "testdata/bucket_associates_labels.jsonnet", EncodingJsonnet, EnableJsonnet())

		sum := template.Summary()

		labels := []SummaryLabel{
			sumLabelGen("label-1", "label-1", "#eee888", "desc_1"),
		}
		assert.Equal(t, labels, sum.Labels)

		bkts := []SummaryBucket{
			{
				SummaryIdentifier: SummaryIdentifier{
					Kind:          KindBucket,
					MetaName:      "rucket-1",
					EnvReferences: []SummaryReference{},
				},
				Name:              "rucket-1",
				Description:       "desc_1",
				RetentionPeriod:   10000 * time.Second,
				LabelAssociations: labels,
			},
			{
				SummaryIdentifier: SummaryIdentifier{
					Kind:          KindBucket,
					MetaName:      "rucket-2",
					EnvReferences: []SummaryReference{},
				},
				Name:              "rucket-2",
				Description:       "desc-2",
				RetentionPeriod:   20000 * time.Second,
				LabelAssociations: labels,
			},
			{
				SummaryIdentifier: SummaryIdentifier{
					Kind:          KindBucket,
					MetaName:      "rucket-3",
					EnvReferences: []SummaryReference{},
				},
				Name:              "rucket-3",
				Description:       "desc_3",
				RetentionPeriod:   30000 * time.Second,
				LabelAssociations: labels,
			},
		}
		assert.Equal(t, bkts, sum.Buckets)
	})
}

func TestCombine(t *testing.T) {
	newTemplateFromYmlStr := func(t *testing.T, templateStr string) *Template {
		t.Helper()
		return newParsedTemplate(t, FromString(templateStr), EncodingYAML, ValidSkipParseError())
	}

	associationsEqual := func(t *testing.T, summaryLabels []SummaryLabel, names ...string) {
		t.Helper()

		require.Len(t, summaryLabels, len(names))

		m := make(map[string]bool)
		for _, n := range names {
			m[n] = true
		}

		for _, l := range summaryLabels {
			if !m[l.Name] {
				assert.Fail(t, "did not find label: "+l.Name)
			}
			delete(m, l.Name)
		}

		if len(m) > 0 {
			var unexpectedLabels []string
			for name := range m {
				unexpectedLabels = append(unexpectedLabels, name)
			}
			assert.Failf(t, "additional labels found", "got: %v", unexpectedLabels)
		}
	}

	t.Run("multiple templates with associations across files", func(t *testing.T) {
		var templates []*Template
		numLabels := 5
		for i := 0; i < numLabels; i++ {
			template := newTemplateFromYmlStr(t, fmt.Sprintf(`
apiVersion: %[1]s
kind: Label
metadata:
  name: label-%d
`, APIVersion, i))
			templates = append(templates, template)
		}

		templates = append(templates, newTemplateFromYmlStr(t, fmt.Sprintf(`
apiVersion: %[1]s
kind: Bucket
metadata:
  name: rucket-1
spec:
  associations:
    - kind: Label
      name: label-1
`, APIVersion)))

		templates = append(templates, newTemplateFromYmlStr(t, fmt.Sprintf(`
apiVersion: %[1]s
kind: Bucket
metadata:
  name: rucket-2
spec:
  associations:
    - kind: Label
      name: label-2
`, APIVersion)))

		templates = append(templates, newTemplateFromYmlStr(t, fmt.Sprintf(`
apiVersion: %[1]s
kind: Bucket
metadata:
  name: rucket-3
spec:
  associations:
    - kind: Label
      name: label-1
    - kind: Label
      name: label-2
`, APIVersion)))

		combinedTemplate, err := Combine(templates)
		require.NoError(t, err)

		sum := combinedTemplate.Summary()

		require.Len(t, sum.Labels, numLabels)
		for i := 0; i < numLabels; i++ {
			assert.Equal(t, fmt.Sprintf("label-%d", i), sum.Labels[i].Name)
		}

		require.Len(t, sum.Labels, numLabels)
		for i := 0; i < numLabels; i++ {
			assert.Equal(t, fmt.Sprintf("label-%d", i), sum.Labels[i].Name)
		}

		require.Len(t, sum.Buckets, 3)
		assert.Equal(t, "rucket-1", sum.Buckets[0].Name)
		associationsEqual(t, sum.Buckets[0].LabelAssociations, "label-1")
		assert.Equal(t, "rucket-2", sum.Buckets[1].Name)
		associationsEqual(t, sum.Buckets[1].LabelAssociations, "label-2")
		assert.Equal(t, "rucket-3", sum.Buckets[2].Name)
		associationsEqual(t, sum.Buckets[2].LabelAssociations, "label-1", "label-2")
	})
}

func Test_normalizeGithubURLToContent(t *testing.T) {
	tests := []struct {
		name     string
		input    string
		expected string
	}{
		{
			name:     "raw url passes untouched",
			input:    "https://raw.githubusercontent.com/influxdata/community-templates/master/github/github.yml",
			expected: "https://raw.githubusercontent.com/influxdata/community-templates/master/github/github.yml",
		},
		{
			name:     "URL that is to short is unchanged",
			input:    "https://github.com/influxdata/community-templates",
			expected: "https://github.com/influxdata/community-templates",
		},
		{
			name:     "URL that does not end in required extention is unchanged",
			input:    "https://github.com/influxdata/community-templates/master/github",
			expected: "https://github.com/influxdata/community-templates/master/github",
		},
		{
			name:     "converts base url with ext yaml to raw content url",
			input:    "https://github.com/influxdata/community-templates/blob/master/github/github.yaml",
			expected: "https://raw.githubusercontent.com/influxdata/community-templates/master/github/github.yaml",
		},
		{
			name:     "converts base url with ext yml to raw content url",
			input:    "https://github.com/influxdata/community-templates/blob/master/github/github.yml",
			expected: "https://raw.githubusercontent.com/influxdata/community-templates/master/github/github.yml",
		},
		{
			name:     "converts base url with ext json to raw content url",
			input:    "https://github.com/influxdata/community-templates/blob/master/github/github.json",
			expected: "https://raw.githubusercontent.com/influxdata/community-templates/master/github/github.json",
		},
		{
			name:     "converts base url with ext jsonnet to raw content url",
			input:    "https://github.com/influxdata/community-templates/blob/master/github/github.jsonnet",
			expected: "https://raw.githubusercontent.com/influxdata/community-templates/master/github/github.jsonnet",
		},
		{
			name:     "url with unexpected content type is unchanged 1",
			input:    "https://github.com/influxdata/community-templates/blob/master/github/github.jason",
			expected: "https://github.com/influxdata/community-templates/blob/master/github/github.jason",
		},
		{
			name:     "url with unexpected content type is unchanged 2",
			input:    "https://github.com/influxdata/community-templates/blob/master/github/github.rando",
			expected: "https://github.com/influxdata/community-templates/blob/master/github/github.rando",
		},
	}

	for _, tt := range tests {
		fn := func(t *testing.T) {
			actual := normalizeGithubURLToContent(tt.input)

			assert.Equal(t, tt.expected, actual)
		}

		t.Run(tt.name, fn)
	}
}

func Test_IsParseError(t *testing.T) {
	tests := []struct {
		name     string
		err      error
		expected bool
	}{
		{
			name:     "base case",
			err:      &parseErr{},
			expected: true,
		},
		{
			name: "wrapped by influxdb error",
			err: &errors2.Error{
				Err: &parseErr{},
			},
			expected: true,
		},
		{
			name: "deeply nested in influxdb error",
			err: &errors2.Error{
				Err: &errors2.Error{
					Err: &errors2.Error{
						Err: &errors2.Error{
							Err: &parseErr{},
						},
					},
				},
			},
			expected: true,
		},
		{
			name: "influxdb error without nested parse err",
			err: &errors2.Error{
				Err: errors.New("nope"),
			},
			expected: false,
		},
		{
			name:     "plain error",
			err:      errors.New("nope"),
			expected: false,
		},
	}

	for _, tt := range tests {
		fn := func(t *testing.T) {
			isParseErr := IsParseErr(tt.err)
			assert.Equal(t, tt.expected, isParseErr)
		}
		t.Run(tt.name, fn)
	}
}

func Test_TemplateValidationErr(t *testing.T) {
	iPtr := func(i int) *int {
		return &i
	}

	compIntSlcs := func(t *testing.T, expected []int, actuals []*int) {
		t.Helper()

		if len(expected) >= len(actuals) {
			require.FailNow(t, "expected array is larger than actuals")
		}

		for i, actual := range actuals {
			if i == len(expected) {
				assert.Nil(t, actual)
				continue
			}
			assert.Equal(t, expected[i], *actual)
		}
	}

	pErr := &parseErr{
		Resources: []resourceErr{
			{
				Kind: KindDashboard.String(),
				Idx:  intPtr(0),
				ValidationErrs: []validationErr{
					{
						Field: "charts",
						Index: iPtr(1),
						Nested: []validationErr{
							{
								Field: "colors",
								Index: iPtr(0),
								Nested: []validationErr{
									{
										Field: "hex",
										Msg:   "hex value required",
									},
								},
							},
							{
								Field: "kind",
								Msg:   "chart kind must be provided",
							},
						},
					},
				},
			},
		},
	}

	errs := pErr.ValidationErrs()
	require.Len(t, errs, 2)
	assert.Equal(t, KindDashboard.String(), errs[0].Kind)
	assert.Equal(t, []string{"root", "charts", "colors", "hex"}, errs[0].Fields)
	compIntSlcs(t, []int{0, 1, 0}, errs[0].Indexes)
	assert.Equal(t, "hex value required", errs[0].Reason)

	assert.Equal(t, KindDashboard.String(), errs[1].Kind)
	assert.Equal(t, []string{"root", "charts", "kind"}, errs[1].Fields)
	compIntSlcs(t, []int{0, 1}, errs[1].Indexes)
	assert.Equal(t, "chart kind must be provided", errs[1].Reason)
}

func Test_validGeometry(t *testing.T) {
	tests := []struct {
		geom     string
		expected bool
	}{
		{
			geom: "line", expected: true,
		},
		{
			geom: "step", expected: true,
		},
		{
			geom: "stacked", expected: true,
		},
		{
			geom: "monotoneX", expected: true,
		},
		{
			geom: "bar", expected: true,
		},
		{
			geom: "rando", expected: false,
		},
		{
			geom: "not a valid geom", expected: false,
		},
	}

	for _, tt := range tests {
		fn := func(t *testing.T) {
			isValid := len(validGeometry(tt.geom)) == 0
			assert.Equal(t, tt.expected, isValid)
		}

		t.Run(tt.geom, fn)
	}
}

func Test_FromFile(t *testing.T) {
	dir := t.TempDir()

	// create empty test file
	emptyFn := filepath.Join(dir, "empty")
	fe, err := os.Create(emptyFn)
	require.NoError(t, err)
	require.NoError(t, fe.Close())

	// create too big test file
	bigFn := filepath.Join(dir, "big")
	fb, err := os.Create(bigFn)
	require.NoError(t, err)
	require.NoError(t, fb.Close())
	require.NoError(t, os.Truncate(bigFn, limitReadFileMaxSize+1))

	// create symlink to /dev/null (linux only)
	devNullSym := filepath.Join(dir, uuid.NewString())
	if runtime.GOOS == "linux" {
		require.NoError(t, os.Symlink("/dev/null", devNullSym))
	}

	type testCase struct {
		path   string
		extra  bool
		expErr string
		oses   []string // list of OSes to run test on, empty means all.
	}

	// Static test suites
	tests := []testCase{
		// valid
		{
			path:   "testdata/bucket_schema.yml",
			extra:  false,
			expErr: "",
		},
		{
			path:   "testdata/bucket_schema.yml",
			extra:  true,
			expErr: "",
		},
		// invalid
		{
			path:   "i\nvalid:///foo",
			extra:  false,
			expErr: "invalid filepath provided",
		},
		{
			path:   "testdata/nonexistent (darwin|linux)",
			extra:  false,
			expErr: "no such file or directory",
			oses:   []string{"darwin", "linux"},
		},
		{
			path:   "testdata/nonexistent (windows)",
			extra:  false,
			expErr: "The system cannot find the file specified.",
			oses:   []string{"windows"},
		},
		// invalid with extra
		{
			path:   "/dev/null",
			extra:  true,
			expErr: "file in special file system",
			oses:   []string{"linux"},
		},
		// symlink to /dev/null, invalid with extra
		{
			path:   devNullSym,
			extra:  true,
			expErr: "file in special file system",
			oses:   []string{"linux"},
		},
		// invalid with extra
		{
			path:   "/dev/null",
			extra:  true,
			expErr: "not a regular file",
			oses:   []string{"darwin"},
		},
		{
			path:   "/",
			extra:  true,
			expErr: "not a regular file",
		},
		{
			path:   "testdata/nonexistent (darwin|linux)",
			extra:  true,
			expErr: "no such file or directory",
			oses:   []string{"darwin", "linux"},
		},
		{
			path:   "testdata/nonexistent (windows)",
			extra:  true,
			expErr: "The system cannot find the file specified.",
			oses:   []string{"windows"},
		},
		{
			path:   emptyFn,
			extra:  true,
			expErr: "file empty",
		},
		{
			path:   bigFn,
			extra:  true,
			expErr: "file too big",
		},
	}

	// Add tmpfs special file system exception tests for Linux.
	// We don't consider errors creating the tests cases (e.g. no tmpfs mounts, no write
	// permissions to any tmpfs mount) errors for the test itself. It's possible we
	// can't run these tests in some locked down environments, but we will warn the
	// use we couldn't run the tests.
	if runtime.GOOS == "linux" {
		tmpfsDirs, err := func() ([]string, error) {
			t.Helper()
			// Quick and dirty parse of  /proc/mounts to find tmpfs mount points on system.
			mounts, err := os.ReadFile("/proc/mounts")
			if err != nil {
				return nil, fmt.Errorf("error reading /proc/mounts: %w", err)
			}
			mountsLines := strings.Split(string(mounts), "\n")
			var tmpfsMounts []string
			for _, line := range mountsLines {
				if line == "" {
					continue
				}
				cols := strings.Split(line, " ")
				if len(cols) != 6 {
					return nil, fmt.Errorf("unexpected /proc/mounts line format (%d columns): %q", len(cols), line)
				}
				if cols[0] == "tmpfs" {
					tmpfsMounts = append(tmpfsMounts, cols[1])
				}
			}
			if len(tmpfsMounts) == 0 {
				return nil, errors.New("no tmpfs mount points found")
			}

			// Find which common tmpfs directories are actually mounted as tmpfs.
			candidateDirs := []string{
				"/dev/shm",
				"/run",
				"/tmp",
				os.Getenv("XDG_RUNTIME_DIR"),
				os.TempDir(),
			}
			var actualDirs []string
			for _, dir := range candidateDirs {
				if dir == "" {
					continue
				}
				for _, mount := range tmpfsMounts {
					if strings.HasPrefix(dir, mount) {
						actualDirs = append(actualDirs, dir)
					}
				}
			}
			if len(actualDirs) == 0 {
				return nil, errors.New("no common tmpfs directories on tmpfs mount points")
			}
			return actualDirs, nil
		}()
		if err == nil {
			// Create test files in the tmpfs directories and create test cases
			var tmpfsTests []testCase
			var tmpfsErrs []error
			contents, err := os.ReadFile("testdata/bucket_schema.yml")
			require.NoError(t, err)
			require.NotEmpty(t, contents)
			for _, dir := range tmpfsDirs {
				testFile, err := os.CreateTemp(dir, "fromfile_*")
				if err == nil {
					testPath := testFile.Name()
					defer os.Remove(testPath)
					_, err = testFile.Write(contents)
					require.NoError(t, err)
					require.NoError(t, testFile.Close())
					tmpfsTests = append(tmpfsTests, testCase{
						path:   testPath,
						extra:  true,
						expErr: "",
					})

					// Create a test that's a symlink to the tmpfs file
					symPath := path.Join(dir, uuid.NewString())
					require.NoError(t, os.Symlink(testPath, symPath))
					tmpfsTests = append(tmpfsTests, testCase{
						path:   symPath,
						extra:  true,
						expErr: "",
					})
				} else {
					tmpfsErrs = append(tmpfsErrs, fmt.Errorf("error tmpfs test file in %q: %w", dir, err))
				}
			}
			// Ignore errors creating tmpfs files if we got at least one test case from the bunch
			if len(tmpfsTests) > 0 {
				tests = append(tests, tmpfsTests...)
			} else {
				t.Logf("WARNING: could not create files for tmpfs special file system tests: %s", errors.Join(tmpfsErrs...))
			}
		} else {
			t.Logf("WARNING: unable to run tmpfs special file system tests: %s", err)
		}
	}

	for _, tt := range tests {
		fn := func(t *testing.T) {
			if len(tt.oses) > 0 {
				osFound := false
				for _, os := range tt.oses {
					if runtime.GOOS == os {
						osFound = true
						break
					}
				}
				if !osFound {
					t.Skipf("skipping test for %q OS", runtime.GOOS)
				}
			}

			urlPath := pathToURLPath(tt.path)
			readFn := FromFile(urlPath, tt.extra)
			assert.NotNil(t, readFn)

			reader, path, err := readFn()
			if tt.expErr == "" {
				assert.NotNil(t, reader)
				assert.Nil(t, err)
				assert.Equal(t, fmt.Sprintf("file://%s", urlPath), path)
			} else {
				assert.Nil(t, reader)
				assert.NotNil(t, err)
				assert.Contains(t, err.Error(), tt.expErr)
			}
		}
		t.Run(tt.path, fn)
	}
}

type testTemplateResourceError struct {
	name           string
	encoding       Encoding
	templateStr    string
	resourceErrs   int
	validationErrs int
	valFields      []string
	assErrs        int
	assIdxs        []int
}

// defaults to yaml encoding if encoding not provided
// defaults num resources to 1 if resource errs not provided.
func testTemplateErrors(t *testing.T, k Kind, tt testTemplateResourceError) {
	t.Helper()
	encoding := EncodingYAML
	if tt.encoding != EncodingUnknown {
		encoding = tt.encoding
	}

	resErrs := 1
	if tt.resourceErrs > 0 {
		resErrs = tt.resourceErrs
	}

	fn := func(t *testing.T) {
		t.Helper()

		_, err := Parse(encoding, FromString(tt.templateStr))
		require.Error(t, err)

		require.True(t, IsParseErr(err), err)

		pErr := err.(*parseErr)
		require.Len(t, pErr.Resources, resErrs)

		defer func() {
			if t.Failed() {
				t.Logf("received unexpected err: %s", pErr)
			}
		}()

		resErr := pErr.Resources[0]
		assert.Equal(t, k.String(), resErr.Kind)

		for i, vFail := range resErr.ValidationErrs {
			if len(tt.valFields) == i {
				break
			}
			expectedField := tt.valFields[i]
			findErr(t, expectedField, vFail)
		}

		if tt.assErrs == 0 {
			return
		}

		assFails := pErr.Resources[0].AssociationErrs
		for i, assFail := range assFails {
			if len(tt.valFields) == i {
				break
			}
			expectedField := tt.valFields[i]
			findErr(t, expectedField, assFail)
		}
	}
	t.Run(tt.name, fn)
}

func findErr(t *testing.T, expectedField string, vErr validationErr) validationErr {
	t.Helper()

	fields := strings.Split(expectedField, ".")
	if len(fields) == 1 {
		require.Equal(t, expectedField, vErr.Field)
		return vErr
	}

	currentFieldName, idx := nextField(t, fields[0])
	if idx > -1 {
		require.NotNil(t, vErr.Index)
		require.Equal(t, idx, *vErr.Index)
	}
	require.Equal(t, currentFieldName, vErr.Field)

	next := strings.Join(fields[1:], ".")
	nestedField, _ := nextField(t, next)
	for _, n := range vErr.Nested {
		if n.Field == nestedField {
			return findErr(t, next, n)
		}
	}
	assert.Fail(t, "did not find field: "+expectedField)

	return vErr
}

func nextField(t *testing.T, field string) (string, int) {
	t.Helper()

	fields := strings.Split(field, ".")
	if len(fields) == 1 && !strings.HasSuffix(fields[0], "]") {
		return field, -1
	}
	parts := strings.Split(fields[0], "[")
	if len(parts) == 1 {
		return parts[0], -1
	}
	fieldName := parts[0]

	if strIdx := strings.Index(parts[1], "]"); strIdx > -1 {
		idx, err := strconv.Atoi(parts[1][:strIdx])
		require.NoError(t, err)
		return fieldName, idx
	}
	return "", -1
}

// pathToURL converts file paths to URLs. This is a simple operation on Unix,
// but is complicated by correct handling of drive letters on Windows.
func pathToURLPath(p string) string {
	return filepath.ToSlash(p)
}

func validParsedTemplateFromFile(t *testing.T, path string, encoding Encoding, opts ...ValidateOptFn) *Template {
	t.Helper()

	var readFn ReaderFn
	templateBytes, ok := availableTemplateFiles[path]
	if ok {
		readFn = FromReader(bytes.NewBuffer(templateBytes), "file://"+pathToURLPath(path))
	} else {
		readFn = FromFile(path, false)
		atomic.AddInt64(&missedTemplateCacheCounter, 1)
	}

	opt := &validateOpt{}
	for _, o := range opts {
		o(opt)
	}

	template := newParsedTemplate(t, readFn, encoding, opts...)
	if encoding == EncodingJsonnet && !opt.enableJsonnet {
		require.Equal(t, &Template{}, template)
		return template
	}

	u := url.URL{
		Scheme: "file",
		Path:   path,
	}
	require.Equal(t, []string{u.String()}, template.Sources())
	return template
}

func newParsedTemplate(t *testing.T, fn ReaderFn, encoding Encoding, opts ...ValidateOptFn) *Template {
	t.Helper()

	opt := &validateOpt{}
	for _, o := range opts {
		o(opt)
	}

	template, err := Parse(encoding, fn, opts...)
	if encoding == EncodingJsonnet && !opt.enableJsonnet {
		require.Error(t, err)
		return &Template{}
	}
	require.NoError(t, err)

	for _, k := range template.Objects {
		require.Contains(t, k.APIVersion, "influxdata.com/v2alpha")
	}

	require.True(t, template.isParsed)
	return template
}

func testfileRunner(t *testing.T, path string, testFn func(t *testing.T, template *Template)) {
	t.Helper()

	tests := []struct {
		name      string
		extension string
		encoding  Encoding
	}{
		{
			name:      "yaml",
			extension: ".yml",
			encoding:  EncodingYAML,
		},
		{
			name:      "json",
			extension: ".json",
			encoding:  EncodingJSON,
		},
	}

	ext := filepath.Ext(path)
	switch ext {
	case ".yml":
		tests = tests[:1]
	case ".json":
		tests = tests[1:]
	}

	path = strings.TrimSuffix(path, ext)

	for _, tt := range tests {
		fn := func(t *testing.T) {
			t.Helper()

			template := validParsedTemplateFromFile(t, path+tt.extension, tt.encoding)
			if testFn != nil {
				testFn(t, template)
			}
		}
		t.Run(tt.name, fn)
	}
}

func sumLabelGen(metaName, name, color, desc string, envRefs ...SummaryReference) SummaryLabel {
	if envRefs == nil {
		envRefs = make([]SummaryReference, 0)
	}
	return SummaryLabel{
		SummaryIdentifier: SummaryIdentifier{
			Kind:          KindLabel,
			MetaName:      metaName,
			EnvReferences: envRefs,
		},
		Name: name,
		Properties: struct {
			Color       string `json:"color"`
			Description string `json:"description"`
		}{
			Color:       color,
			Description: desc,
		},
	}
}

func strPtr(s string) *string {
	return &s
}

func mustDuration(t *testing.T, d time.Duration) *notification.Duration {
	t.Helper()
	dur, err := notification.FromTimeDuration(d)
	require.NoError(t, err)
	return &dur
}
