package influx

import (
	"context"
	"encoding/json"
	"fmt"
	"reflect"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/influxdata/influxdb/v2/chronograf"
	"github.com/influxdata/influxdb/v2/chronograf/mocks"
)

func Test_toPoint(t *testing.T) {
	tests := []struct {
		name string
		anno *chronograf.Annotation
		now  time.Time
		want chronograf.Point
	}{
		0: {
			name: "convert annotation to point w/o start and end times",
			anno: &chronograf.Annotation{
				ID:   "1",
				Text: "mytext",
				Type: "mytype",
			},
			now: time.Unix(0, 0),
			want: chronograf.Point{
				Database:        AnnotationsDB,
				RetentionPolicy: DefaultRP,
				Measurement:     DefaultMeasurement,
				Time:            time.Time{}.UnixNano(),
				Tags: map[string]string{
					"id": "1",
				},
				Fields: map[string]interface{}{
					"deleted":          false,
					"start_time":       time.Time{}.UnixNano(),
					"modified_time_ns": int64(time.Unix(0, 0).UnixNano()),
					"text":             "mytext",
					"type":             "mytype",
				},
			},
		},
		1: {
			name: "convert annotation to point with start/end time",
			anno: &chronograf.Annotation{
				ID:        "1",
				Text:      "mytext",
				Type:      "mytype",
				StartTime: time.Unix(100, 0),
				EndTime:   time.Unix(200, 0),
			},
			now: time.Unix(0, 0),
			want: chronograf.Point{
				Database:        AnnotationsDB,
				RetentionPolicy: DefaultRP,
				Measurement:     DefaultMeasurement,
				Time:            time.Unix(200, 0).UnixNano(),
				Tags: map[string]string{
					"id": "1",
				},
				Fields: map[string]interface{}{
					"deleted":          false,
					"start_time":       time.Unix(100, 0).UnixNano(),
					"modified_time_ns": int64(time.Unix(0, 0).UnixNano()),
					"text":             "mytext",
					"type":             "mytype",
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := toPoint(tt.anno, tt.now); !reflect.DeepEqual(got, tt.want) {
				t.Errorf("toPoint() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_toDeletedPoint(t *testing.T) {
	tests := []struct {
		name string
		anno *chronograf.Annotation
		now  time.Time
		want chronograf.Point
	}{
		0: {
			name: "convert annotation to point w/o start and end times",
			anno: &chronograf.Annotation{
				ID:      "1",
				EndTime: time.Unix(0, 0),
			},
			now: time.Unix(0, 0),
			want: chronograf.Point{
				Database:        AnnotationsDB,
				RetentionPolicy: DefaultRP,
				Measurement:     DefaultMeasurement,
				Time:            0,
				Tags: map[string]string{
					"id": "1",
				},
				Fields: map[string]interface{}{
					"deleted":          true,
					"start_time":       int64(0),
					"modified_time_ns": int64(0),
					"text":             "",
					"type":             "",
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := toDeletedPoint(tt.anno, tt.now); !cmp.Equal(got, tt.want) {
				t.Errorf("toDeletedPoint() = %s", cmp.Diff(got, tt.want))
			}
		})
	}
}

func Test_value_Int64(t *testing.T) {
	tests := []struct {
		name    string
		v       value
		idx     int
		want    int64
		wantErr bool
	}{
		{
			name:    "index out of range returns error",
			idx:     1,
			wantErr: true,
		},
		{
			name: "converts a string to int64",
			v: value{
				json.Number("1"),
			},
			idx:  0,
			want: int64(1),
		},
		{
			name: "when not a json.Number, return error",
			v: value{
				"howdy",
			},
			idx:     0,
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := tt.v.Int64(tt.idx)
			if (err != nil) != tt.wantErr {
				t.Errorf("value.Int64() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got != tt.want {
				t.Errorf("value.Int64() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_value_Time(t *testing.T) {
	tests := []struct {
		name    string
		v       value
		idx     int
		want    time.Time
		wantErr bool
	}{
		{
			name:    "index out of range returns error",
			idx:     1,
			wantErr: true,
		},
		{
			name: "converts a string to int64",
			v: value{
				json.Number("1"),
			},
			idx:  0,
			want: time.Unix(0, 1),
		},
		{
			name: "when not a json.Number, return error",
			v: value{
				"howdy",
			},
			idx:     0,
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := tt.v.Time(tt.idx)
			if (err != nil) != tt.wantErr {
				t.Errorf("value.Time() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("value.Time() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_value_String(t *testing.T) {
	tests := []struct {
		name    string
		v       value
		idx     int
		want    string
		wantErr bool
	}{
		{
			name:    "index out of range returns error",
			idx:     1,
			wantErr: true,
		},
		{
			name: "converts a string",
			v: value{
				"howdy",
			},
			idx:  0,
			want: "howdy",
		},
		{
			name: "when not a string, return error",
			v: value{
				0,
			},
			idx:     0,
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := tt.v.String(tt.idx)
			if (err != nil) != tt.wantErr {
				t.Errorf("value.String() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got != tt.want {
				t.Errorf("value.String() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestAnnotationStore_queryAnnotations(t *testing.T) {
	type args struct {
		ctx   context.Context
		query string
	}
	tests := []struct {
		name    string
		client  chronograf.TimeSeries
		args    args
		want    []chronograf.Annotation
		wantErr bool
	}{
		{
			name: "query error returns an error",
			client: &mocks.TimeSeries{
				QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
					return nil, fmt.Errorf("error")
				},
			},
			wantErr: true,
		},
		{
			name: "response marshal error returns an error",
			client: &mocks.TimeSeries{
				QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
					return mocks.NewResponse("", fmt.Errorf("")), nil
				},
			},
			wantErr: true,
		},
		{
			name: "Bad JSON returns an error",
			client: &mocks.TimeSeries{
				QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
					return mocks.NewResponse(`{}`, nil), nil
				},
			},
			wantErr: true,
		},

		{
			name: "Incorrect fields returns error",
			client: &mocks.TimeSeries{
				QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
					return mocks.NewResponse(`[{
						"series": [
							{
								"name": "annotations",
								"columns": [
									"time",
									"deleted",
									"id",
									"modified_time_ns",
									"start_time",
									"text",
									"type"
								],
								"values": [
									[
										1516920117000000000,
										true,
										"4ba9f836-20e8-4b8e-af51-e1363edd7b6d",
										1517425994487495051,
										0,
										"",
										""
									]
								]
							}
						]
					}
				]}]`, nil), nil
				},
			},
			wantErr: true,
		},
		{
			name: "two annotation response",
			client: &mocks.TimeSeries{
				QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
					return mocks.NewResponse(`[
						{
							"series": [
								{
									"name": "annotations",
									"columns": [
										"time",
										"start_time",
										"modified_time_ns",
										"text",
										"type",
										"id"
									],
									"values": [
										[
											1516920177345000000,
											0,
											1516989242129417403,
											"mytext",
											"mytype",
											"ecf3a75d-f1c0-40e8-9790-902701467e92"
										],
										[
											1516920177345000000,
											0,
											1517425914433539296,
											"mytext2",
											"mytype2",
											"ea0aa94b-969a-4cd5-912a-5db61d502268"
										]
									]
								}
							]
						}
					]`, nil), nil
				},
			},
			want: []chronograf.Annotation{
				{
					EndTime:   time.Unix(0, 1516920177345000000),
					StartTime: time.Unix(0, 0),
					Text:      "mytext2",
					Type:      "mytype2",
					ID:        "ea0aa94b-969a-4cd5-912a-5db61d502268",
				},
				{
					EndTime:   time.Unix(0, 1516920177345000000),
					StartTime: time.Unix(0, 0),
					Text:      "mytext",
					Type:      "mytype",
					ID:        "ecf3a75d-f1c0-40e8-9790-902701467e92",
				},
			},
		},
		{
			name: "same id returns one",
			client: &mocks.TimeSeries{
				QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
					return mocks.NewResponse(`[
						{
							"series": [
								{
									"name": "annotations",
									"columns": [
										"time",
										"start_time",
										"modified_time_ns",
										"text",
										"type",
										"id"
									],
									"values": [
										[
											1516920177345000000,
											0,
											1516989242129417403,
											"mytext",
											"mytype",
											"ea0aa94b-969a-4cd5-912a-5db61d502268"
										],
										[
											1516920177345000000,
											0,
											1517425914433539296,
											"mytext2",
											"mytype2",
											"ea0aa94b-969a-4cd5-912a-5db61d502268"
										]
									]
								}
							]
						}
					]`, nil), nil
				},
			},
			want: []chronograf.Annotation{
				{
					EndTime:   time.Unix(0, 1516920177345000000),
					StartTime: time.Unix(0, 0),
					Text:      "mytext2",
					Type:      "mytype2",
					ID:        "ea0aa94b-969a-4cd5-912a-5db61d502268",
				},
			},
		},
		{
			name: "no responses returns empty array",
			client: &mocks.TimeSeries{
				QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
					return mocks.NewResponse(`[ { } ]`, nil), nil
				},
			},
			want: []chronograf.Annotation{},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			a := &AnnotationStore{
				client: tt.client,
			}
			got, err := a.queryAnnotations(tt.args.ctx, tt.args.query)
			if (err != nil) != tt.wantErr {
				t.Errorf("AnnotationStore.queryAnnotations() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("AnnotationStore.queryAnnotations() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestAnnotationStore_Update(t *testing.T) {
	type fields struct {
		client chronograf.TimeSeries
		now    Now
	}
	type args struct {
		ctx  context.Context
		anno *chronograf.Annotation
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		wantErr bool
	}{
		{
			name: "no responses returns error",
			fields: fields{
				client: &mocks.TimeSeries{
					QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
						return mocks.NewResponse(`[ { } ]`, nil), nil
					},
					WriteF: func(context.Context, []chronograf.Point) error {
						return nil
					},
				},
			},
			args: args{
				ctx: context.Background(),
				anno: &chronograf.Annotation{
					ID: "1",
				},
			},
			wantErr: true,
		},
		{
			name: "error writing returns error",
			fields: fields{
				now: func() time.Time { return time.Time{} },
				client: &mocks.TimeSeries{
					QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
						return mocks.NewResponse(`[
							{
								"series": [
									{
										"name": "annotations",
										"columns": [
											"time",
											"start_time",
											"modified_time_ns",
											"text",
											"type",
											"id"
										],
										"values": [
											[
												1516920177345000000,
												0,
												1516989242129417403,
												"mytext",
												"mytype",
												"ecf3a75d-f1c0-40e8-9790-902701467e92"
											],
											[
												1516920177345000000,
												0,
												1517425914433539296,
												"mytext2",
												"mytype2",
												"ea0aa94b-969a-4cd5-912a-5db61d502268"
											]
										]
									}
								]
							}
						]`, nil), nil
					},
					WriteF: func(context.Context, []chronograf.Point) error {
						return fmt.Errorf("error")
					},
				},
			},
			args: args{
				ctx: context.Background(),
				anno: &chronograf.Annotation{
					ID: "1",
				},
			},
			wantErr: true,
		},
		{
			name: "Update with delete",
			fields: fields{
				now: func() time.Time { return time.Time{} },
				client: &mocks.TimeSeries{
					QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
						return mocks.NewResponse(`[
							{
								"series": [
									{
										"name": "annotations",
										"columns": [
											"time",
											"start_time",
											"modified_time_ns",
											"text",
											"type",
											"id"
										],
										"values": [
											[
												1516920177345000000,
												0,
												1516989242129417403,
												"mytext",
												"mytype",
												"ecf3a75d-f1c0-40e8-9790-902701467e92"
											]
										]
									}
								]
							}
						]`, nil), nil
					},
					WriteF: func(context.Context, []chronograf.Point) error {
						return nil
					},
				},
			},
			args: args{
				ctx: context.Background(),
				anno: &chronograf.Annotation{
					ID: "1",
				},
			},
		},
		{
			name: "Update with delete no delete",
			fields: fields{
				now: func() time.Time { return time.Time{} },
				client: &mocks.TimeSeries{
					QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
						return mocks.NewResponse(`[
							{
								"series": [
									{
										"name": "annotations",
										"columns": [
											"time",
											"start_time",
											"modified_time_ns",
											"text",
											"type",
											"id"
										],
										"values": [
											[
												1516920177345000000,
												0,
												1516989242129417403,
												"mytext",
												"mytype",
												"ecf3a75d-f1c0-40e8-9790-902701467e92"
											]
										]
									}
								]
							}
						]`, nil), nil
					},
					WriteF: func(context.Context, []chronograf.Point) error {
						return nil
					},
				},
			},
			args: args{
				ctx: context.Background(),
				anno: &chronograf.Annotation{
					ID:      "ecf3a75d-f1c0-40e8-9790-902701467e92",
					EndTime: time.Unix(0, 1516920177345000000),
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			a := &AnnotationStore{
				client: tt.fields.client,
				now:    tt.fields.now,
			}
			if err := a.Update(tt.args.ctx, tt.args.anno); (err != nil) != tt.wantErr {
				t.Errorf("AnnotationStore.Update() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}
