package influxdb

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

	"github.com/influxdata/influxdb/v2/kit/platform"
	"github.com/influxdata/influxdb/v2/kit/platform/errors"
	"github.com/stretchr/testify/require"
)

var (
	testTime  time.Time = time.Now()
	testTime2 time.Time = testTime.Add(time.Minute)

	annID, _ = platform.IDFromString("2345678901234567")
)

func nowFunc() time.Time {
	return testTime
}

func TestAnnotationCreate(t *testing.T) {
	type tst struct {
		name     string
		input    AnnotationCreate
		expected AnnotationCreate
		err      *errors.Error
	}

	tests := []tst{
		{
			name: "minimum valid create request",
			input: AnnotationCreate{
				Summary: "this is a default annotation",
			},
			expected: AnnotationCreate{
				StreamTag: "default",
				Summary:   "this is a default annotation",
				EndTime:   &testTime,
				StartTime: &testTime,
			},
		},
		{
			name: "full valid create request",
			input: AnnotationCreate{
				StreamTag: "other",
				Summary:   "this is another annotation",
				Message:   "This is a much longer description or message to add to the annotation summary",
				Stickers:  map[string]string{"product": "cloud"},
				EndTime:   &testTime2,
				StartTime: &testTime,
			},
			expected: AnnotationCreate{
				StreamTag: "other",
				Summary:   "this is another annotation",
				Message:   "This is a much longer description or message to add to the annotation summary",
				Stickers:  map[string]string{"product": "cloud"},
				EndTime:   &testTime2,
				StartTime: &testTime,
			},
		},
		{
			name:  "empty create request",
			input: AnnotationCreate{},
			err:   errEmptySummary,
		},
		{
			name: "end time before start create request",
			input: AnnotationCreate{
				Summary:   "this is a default annotation",
				EndTime:   &testTime,
				StartTime: &testTime2,
			},
			err: errReversedTimes,
		},
		{
			name: "default end time before start create request",
			input: AnnotationCreate{
				Summary:   "this is a default annotation",
				StartTime: &testTime2,
			},
			err: errReversedTimes,
		},
		{
			name: "summary too long",
			input: AnnotationCreate{
				Summary: strings.Repeat("a", 256),
			},
			err: errSummaryTooLong,
		},
		{
			name: "message too long",
			input: AnnotationCreate{
				Summary: "longTom",
				Message: strings.Repeat("a", 4097),
			},
			err: errMsgTooLong,
		},
		{
			name: "stream tag too long",
			input: AnnotationCreate{
				Summary:   "longTom",
				StreamTag: strings.Repeat("a", 256),
			},
			err: errStreamTagTooLong,
		},
		{
			name: "sticker key too long",
			input: AnnotationCreate{
				Summary:  "longTom",
				Stickers: map[string]string{strings.Repeat("1", 256): "val"},
			},
			err: errStickerTooLong,
		},
		{
			name: "sticker val too long",
			input: AnnotationCreate{
				Summary:  "longTom",
				Stickers: map[string]string{"key": strings.Repeat("1", 256)},
			},
			err: errStickerTooLong,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			err := test.input.Validate(nowFunc)
			if test.err != nil {
				require.Equal(t, test.err, err)
				return
			}

			require.NoError(t, err)
			require.Equal(t, test.expected, test.input)
		})
	}
}

func TestDeleteFilter(t *testing.T) {
	type tst struct {
		name     string
		input    AnnotationDeleteFilter
		expected AnnotationDeleteFilter
		err      *errors.Error
	}

	tests := []tst{
		{
			name: "minimum valid delete",
			input: AnnotationDeleteFilter{
				StreamTag: "default",
				EndTime:   &testTime,
				StartTime: &testTime,
			},
			expected: AnnotationDeleteFilter{
				StreamTag: "default",
				EndTime:   &testTime,
				StartTime: &testTime,
			},
		},
		{
			name: "full valid delete",
			input: AnnotationDeleteFilter{
				StreamTag: "default",
				Stickers:  map[string]string{"product": "oss"},
				EndTime:   &testTime,
				StartTime: &testTime,
			},
			expected: AnnotationDeleteFilter{
				StreamTag: "default",
				Stickers:  map[string]string{"product": "oss"},
				EndTime:   &testTime,
				StartTime: &testTime,
			},
		},
		{
			name: "missing stream tag",
			input: AnnotationDeleteFilter{
				Stickers:  map[string]string{"product": "oss"},
				EndTime:   &testTime,
				StartTime: &testTime,
			},
			err: errMissingStreamTagOrId,
		},
		{
			name: "missing start time",
			input: AnnotationDeleteFilter{
				StreamTag: "default",
				Stickers:  map[string]string{"product": "oss"},
				EndTime:   &testTime,
			},
			err: errMissingStartTime,
		},
		{
			name: "missing end time",
			input: AnnotationDeleteFilter{
				StreamTag: "default",
				Stickers:  map[string]string{"product": "oss"},
				StartTime: &testTime,
			},
			err: errMissingEndTime,
		},
		{
			name: "end time before start create request",
			input: AnnotationDeleteFilter{
				StreamTag: "default",
				Stickers:  map[string]string{"product": "oss"},
				EndTime:   &testTime,
				StartTime: &testTime2,
			},
			err: errReversedTimes,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			err := test.input.Validate()
			if test.err != nil {
				require.Equal(t, test.err, err)
				return
			}

			require.NoError(t, err)
			require.Equal(t, test.expected, test.input)
		})
	}
}

func TestAnnotationListFilter(t *testing.T) {
	type tst struct {
		name       string
		input      AnnotationListFilter
		expected   AnnotationListFilter
		checkValue bool
		err        *errors.Error
	}

	tests := []tst{
		{
			name: "minimum valid",
			input: AnnotationListFilter{
				BasicFilter: BasicFilter{
					EndTime:   &testTime,
					StartTime: &testTime,
				},
			},
			expected: AnnotationListFilter{
				BasicFilter: BasicFilter{
					EndTime:   &testTime,
					StartTime: &testTime,
				},
			},
		},
		{
			name:  "empty valid",
			input: AnnotationListFilter{},
			expected: AnnotationListFilter{
				BasicFilter: BasicFilter{
					EndTime:   &testTime,
					StartTime: &testTime,
				},
			},
			checkValue: true,
		},
		{
			name: "invalid due to reversed times",
			input: AnnotationListFilter{
				BasicFilter: BasicFilter{
					EndTime:   &testTime,
					StartTime: &testTime2,
				},
			},
			err: errReversedTimes,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			err := test.input.Validate(nowFunc)
			if test.err != nil {
				require.Equal(t, test.err, err)
				return
			}

			require.NoError(t, err)
			if test.checkValue {
				require.Equal(t, *test.expected.BasicFilter.StartTime, *test.expected.BasicFilter.EndTime)
			} else {
				require.Equal(t, test.expected, test.input)
			}
		})
	}
}

func TestStreamListFilter(t *testing.T) {
	type tst struct {
		name       string
		input      StreamListFilter
		expected   StreamListFilter
		checkValue bool
		err        *errors.Error
	}

	tests := []tst{
		{
			name: "minimum valid",
			input: StreamListFilter{
				BasicFilter: BasicFilter{
					EndTime:   &testTime,
					StartTime: &testTime,
				},
			},
			expected: StreamListFilter{
				BasicFilter: BasicFilter{
					EndTime:   &testTime,
					StartTime: &testTime,
				},
			},
		},
		{
			name:  "empty valid",
			input: StreamListFilter{},
			expected: StreamListFilter{
				BasicFilter: BasicFilter{
					EndTime:   &testTime,
					StartTime: &testTime,
				},
			},
			checkValue: true,
		},
		{
			name: "invalid due to reversed times",
			input: StreamListFilter{
				BasicFilter: BasicFilter{
					EndTime:   &testTime,
					StartTime: &testTime2,
				},
			},
			err: errReversedTimes,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			err := test.input.Validate(nowFunc)
			if test.err != nil {
				require.Equal(t, test.err, err)
				return
			}

			require.NoError(t, err)
			if test.checkValue {
				require.Equal(t, *test.expected.BasicFilter.StartTime, *test.expected.BasicFilter.EndTime)
			} else {
				require.Equal(t, test.expected, test.input)
			}
		})
	}
}

func TestStreamIsValid(t *testing.T) {
	type tst struct {
		name  string
		input Stream
		err   *errors.Error
	}

	tests := []tst{
		{
			name: "minimum valid",
			input: Stream{
				Name: "default",
			},
		},
		{
			name:  "empty valid",
			input: Stream{},
		},
		{
			name: "invalid name too long",
			input: Stream{
				Name: strings.Repeat("a", 512),
			},
			err: errStreamNameTooLong,
		},
		{
			name: "invalid description too long",
			input: Stream{
				Name:        "longTom",
				Description: strings.Repeat("a", 2048),
			},
			err: errStreamDescTooLong,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if test.err != nil {
				require.Equal(t, test.err, test.input.Validate(false))
			} else {
				require.NoError(t, test.input.Validate(false))
			}
		})
	}
}

func TestBasicStreamIsValid(t *testing.T) {
	type tst struct {
		name     string
		input    BasicStream
		expected bool
	}

	tests := []tst{
		{
			name: "minimum valid",
			input: BasicStream{
				Names: []string{"default"},
			},
			expected: true,
		},
		{
			name:     "invalid",
			input:    BasicStream{},
			expected: false,
		},
		{
			name:     "empty name",
			input:    BasicStream{Names: []string{""}},
			expected: false,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			require.Equal(t, test.expected, test.input.IsValid())
		})
	}
}

func TestMashallReadAnnotations(t *testing.T) {
	ra := ReadAnnotations{
		"default": []ReadAnnotation{
			{
				ID:        *annID,
				Summary:   "this is one annotation",
				Stickers:  map[string]string{"env": "testing"},
				StartTime: testTime.Format(time.RFC3339Nano),
				EndTime:   testTime2.Format(time.RFC3339Nano),
			},
			{
				ID:        *annID,
				Summary:   "this is another annotation",
				Stickers:  map[string]string{"env": "testing"},
				StartTime: testTime.Format(time.RFC3339Nano),
				EndTime:   testTime.Format(time.RFC3339Nano),
			},
		},
		"testing": []ReadAnnotation{
			{
				ID:        *annID,
				Summary:   "this is yet another annotation",
				Stickers:  map[string]string{"env": "testing"},
				StartTime: testTime.Format(time.RFC3339Nano),
				EndTime:   testTime.Format(time.RFC3339Nano),
			},
		},
	}

	b, err := json.Marshal(ra)
	require.NoError(t, err)
	require.Greater(t, len(b), 0)
}

func TestSetStickerIncludes(t *testing.T) {
	type tst struct {
		name     string
		input    map[string][]string
		expected AnnotationStickers
	}

	tests := []tst{
		{
			name: "with stickerIncludes",
			input: map[string][]string{
				"stickerIncludes[product]": {"oss"},
				"stickerIncludes[author]":  {"russ"},
				"streams":                  {"default", "blogs"},
			},
			expected: map[string]string{
				"product": "oss",
				"author":  "russ",
			},
		},
		{
			name: "no sticker includes",
			input: map[string][]string{
				"startTime": {"2021-01-13T22%3A17%3A37.953Z"},
				"endTime":   {"2021-01-13T22%3A17%3A37.953Z"},
				"streams":   {"default", "blogs"},
			},
			expected: map[string]string{},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			f := AnnotationListFilter{}
			f.SetStickerIncludes(test.input)
			require.Equal(t, test.expected, f.StickerIncludes)
		})
	}
}

func TestSetStickers(t *testing.T) {
	type tst struct {
		name     string
		input    map[string][]string
		expected map[string]string
	}

	tests := []tst{
		{
			name: "with stickers",
			input: map[string][]string{
				"stickers[product]": {"oss"},
				"stickers[author]":  {"russ"},
				"streams":           {"default", "blogs"},
			},
			expected: map[string]string{
				"product": "oss",
				"author":  "russ",
			},
		},
		{
			name: "no stickers",
			input: map[string][]string{
				"startTime": {"2021-01-13T22%3A17%3A37.953Z"},
				"endTime":   {"2021-01-13T22%3A17%3A37.953Z"},
				"streams":   {"default", "blogs"},
			},
			expected: map[string]string{},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			f := AnnotationDeleteFilter{}
			f.SetStickers(test.input)
			require.Equal(t, test.expected, f.Stickers)
		})
	}
}

func TestStickerSliceToMap(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name     string
		stickers []string
		want     map[string]string
		wantErr  error
	}{
		{
			"good stickers",
			[]string{"good1=val1", "good2=val2"},
			map[string]string{"good1": "val1", "good2": "val2"},
			nil,
		},
		{
			"bad stickers",
			[]string{"this is an invalid sticker", "shouldbe=likethis"},
			nil,
			invalidStickerError("this is an invalid sticker"),
		},
		{
			"no stickers",
			[]string{},
			map[string]string{},
			nil,
		},
	}

	for _, tt := range tests {
		got, err := stickerSliceToMap(tt.stickers)
		require.Equal(t, tt.want, got)
		require.Equal(t, tt.wantErr, err)
	}
}
