package promql

import (
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/influxdata/flux"
	"github.com/influxdata/flux/ast"
	"github.com/influxdata/flux/interpreter"
	"github.com/influxdata/flux/semantic"
	"github.com/influxdata/flux/semantic/semantictest"
	"github.com/influxdata/flux/stdlib/universe"
	"github.com/influxdata/influxdb/v2/query/stdlib/influxdata/influxdb"
)

func TestParsePromQL(t *testing.T) {
	tests := []struct {
		name    string
		promql  string
		opts    []Option
		want    interface{}
		wantErr bool
	}{
		{
			name:   "testing comments",
			promql: "# http_requests_total",
			want: &Comment{
				Source: "# http_requests_total",
			},
		},
		{
			name:   "vector",
			promql: "http_requests_total",
			want: &Selector{
				Name: "http_requests_total",
			},
		},
		{
			name:   "vector with label matching",
			promql: `http_requests_total{a="b"}`,
			want: &Selector{
				Name: "http_requests_total",
				LabelMatchers: []*LabelMatcher{
					&LabelMatcher{
						Name: "a",
						Kind: Equal,
						Value: &StringLiteral{
							String: "b",
						},
					},
				},
			},
		},
		{
			name:   "vector with two labels matching",
			promql: `http_requests_total{a="b", c!="d"}`,
			want: &Selector{
				Name: "http_requests_total",
				LabelMatchers: []*LabelMatcher{
					&LabelMatcher{
						Name: "a",
						Kind: Equal,
						Value: &StringLiteral{
							String: "b",
						},
					},
					&LabelMatcher{
						Name: "c",
						Kind: NotEqual,
						Value: &StringLiteral{
							String: "d",
						},
					},
				},
			},
		},
		{
			name:   "vector with numeric label matcher",
			promql: `http_requests_total{a=500}`,
			want: &Selector{
				Name: "http_requests_total",
				LabelMatchers: []*LabelMatcher{
					&LabelMatcher{
						Name: "a",
						Kind: Equal,
						Value: &Number{
							Val: 500,
						},
					},
				},
			},
		},
		{
			name:    "invalid operator in label matcher",
			promql:  `http_requests_total{a > 500}`,
			wantErr: true,
			want:    "",
		},
		{
			name:    "no metric name",
			promql:  `{}`,
			wantErr: true,
			want:    "",
		},
		{
			name:   "vector with multiple regular expressions",
			promql: `foo{a="b", foo!="bar", test=~"test", bar!~"baz"}`,
			want: &Selector{
				Name: "foo",
				LabelMatchers: []*LabelMatcher{
					&LabelMatcher{
						Name: "a",
						Kind: Equal,
						Value: &StringLiteral{
							String: "b",
						},
					},
					&LabelMatcher{
						Name: "foo",
						Kind: NotEqual,
						Value: &StringLiteral{
							String: "bar",
						},
					},
					&LabelMatcher{
						Name: "test",
						Kind: RegexMatch,
						Value: &StringLiteral{
							String: "test",
						},
					},
					&LabelMatcher{
						Name: "bar",
						Kind: RegexNoMatch,
						Value: &StringLiteral{
							String: "baz",
						},
					},
				},
			},
		},
		{
			name:   "vector with offset",
			promql: "http_requests_total OFFSET 5m",
			want: &Selector{
				Name:   "http_requests_total",
				Offset: time.Minute * 5,
			},
		},
		{
			name:   "vector with range",
			promql: "http_requests_total[5y]",
			want: &Selector{
				Name:  "http_requests_total",
				Range: time.Hour * 24 * 365 * 5,
			},
		},
		{
			name:   "vector with label matches, range, and offset",
			promql: `test{a="b"}[5y] OFFSET 3d`,
			want: &Selector{
				Name:   "test",
				Offset: time.Hour * 24 * 3,
				Range:  time.Hour * 24 * 365 * 5,
				LabelMatchers: []*LabelMatcher{
					&LabelMatcher{
						Name: "a",
						Kind: Equal,
						Value: &StringLiteral{
							String: "b",
						},
					},
				},
			},
		},
		{
			name:   "Min function with group by and keep common",
			promql: `MIN (some_metric) by (foo) keep_common`,
			want: &AggregateExpr{
				Op: &Operator{
					Kind: MinKind,
				},
				Selector: &Selector{
					Name: "some_metric",
				},
				Aggregate: &Aggregate{
					By: true,
					Labels: []*Identifier{
						&Identifier{
							Name: "foo",
						},
					},
				},
			},
		},
		{
			name:   "count function with group by and keep common reversed with label",
			promql: `COUNT by (foo) keep_common (some_metric)`,
			want: &AggregateExpr{
				Op: &Operator{
					Kind: CountKind,
				},
				Selector: &Selector{
					Name: "some_metric",
				},
				Aggregate: &Aggregate{
					By: true,
					Labels: []*Identifier{
						&Identifier{
							Name: "foo",
						},
					},
				},
			},
		},
		{
			name:   "avg function with group by and no keep common reversed with label",
			promql: `avg by (foo)(some_metric)`,
			want: &AggregateExpr{
				Op: &Operator{
					Kind: AvgKind,
				},
				Selector: &Selector{
					Name: "some_metric",
				},
				Aggregate: &Aggregate{
					By: true,
					Labels: []*Identifier{
						&Identifier{
							Name: "foo",
						},
					},
				},
			},
		},
		{
			name:   "sum function with multiple group by and keep common",
			promql: `sum (some_metric) by (foo,bar) keep_common`,
			want: &AggregateExpr{
				Op: &Operator{
					Kind: SumKind,
				},
				Selector: &Selector{
					Name: "some_metric",
				},
				Aggregate: &Aggregate{
					By: true,
					Labels: []*Identifier{
						&Identifier{
							Name: "foo",
						},
						&Identifier{
							Name: "bar",
						},
					},
				},
			},
		},
		{
			name:   "sum function with group by",
			promql: `sum by (foo)(some_metric)`,
			want: &AggregateExpr{
				Op: &Operator{
					Kind: SumKind,
				},
				Selector: &Selector{
					Name: "some_metric",
				},
				Aggregate: &Aggregate{
					By: true,
					Labels: []*Identifier{
						&Identifier{
							Name: "foo",
						},
					},
				},
			},
		},
		{
			name:   "sum function without reversed label",
			promql: `sum without (foo) (some_metric)`,
			want: &AggregateExpr{
				Op: &Operator{
					Kind: SumKind,
				},
				Selector: &Selector{
					Name: "some_metric",
				},
				Aggregate: &Aggregate{
					Without: true,
					Labels: []*Identifier{
						&Identifier{
							Name: "foo",
						},
					},
				},
			},
		},
		{
			name:   "count_values with argument",
			promql: `count_values("version", build_version)`,
			want: &AggregateExpr{
				Op: &Operator{
					Kind: CountValuesKind,
					Arg: &StringLiteral{
						String: "version",
					},
				},
				Selector: &Selector{
					Name: "build_version",
				},
			},
		},
		{
			name:   "sum over a range",
			promql: `sum(node_cpu{_measurement="m0"}[170h])`,
			want: &AggregateExpr{
				Op: &Operator{
					Kind: SumKind,
				},
				Selector: &Selector{
					Name:  "node_cpu",
					Range: 170 * time.Hour,
					LabelMatchers: []*LabelMatcher{
						{
							Name: "_measurement",
							Kind: Equal,
							Value: &StringLiteral{
								String: "m0",
							},
						},
					},
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := ParsePromQL(tt.promql, tt.opts...)
			if (err != nil) != tt.wantErr {
				t.Errorf("ParsePromQL() %s error = %v, wantErr %v", tt.promql, err, tt.wantErr)
				return
			}
			if !cmp.Equal(tt.want, got) {
				t.Errorf("ParsePromQL() = %s -got/+want %s", tt.promql, cmp.Diff(tt.want, got))
			}
		})
	}
}

func TestBuild(t *testing.T) {
	tests := []struct {
		name    string
		promql  string
		opts    []Option
		want    *flux.Spec
		wantErr bool
	}{
		{
			name:   "aggregate with count without a group by",
			promql: `count(node_cpu{mode="user",cpu="cpu2"})`,
			want: &flux.Spec{
				Operations: []*flux.Operation{
					{
						ID: flux.OperationID("from"),
						Spec: &influxdb.FromOpSpec{
							Bucket: influxdb.NameOrID{Name: "prometheus"},
						},
					},
					{
						ID: "where",
						Spec: &universe.FilterOpSpec{
							Fn: interpreter.ResolvedFunction{
								Scope: nil,
								Fn: &semantic.FunctionExpression{
									Parameters: &semantic.FunctionParameters{
										List: []*semantic.FunctionParameter{{Key: &semantic.Identifier{Name: "r"}}},
									},
									Block: &semantic.Block{
										Body: []semantic.Statement{
											&semantic.ReturnStatement{
												Argument: &semantic.LogicalExpression{
													Operator: ast.AndOperator,
													Left: &semantic.LogicalExpression{
														Operator: ast.AndOperator,
														Left: &semantic.BinaryExpression{
															Operator: ast.EqualOperator,
															Left: &semantic.MemberExpression{
																Object: &semantic.IdentifierExpression{
																	Name: "r",
																},
																Property: "_metric",
															},
															Right: &semantic.StringLiteral{
																Value: "node_cpu",
															},
														},
														Right: &semantic.BinaryExpression{
															Operator: ast.EqualOperator,
															Left: &semantic.MemberExpression{
																Object: &semantic.IdentifierExpression{
																	Name: "r",
																},
																Property: "mode",
															},
															Right: &semantic.StringLiteral{
																Value: "user",
															},
														},
													},
													Right: &semantic.BinaryExpression{
														Operator: ast.EqualOperator,
														Left: &semantic.MemberExpression{
															Object: &semantic.IdentifierExpression{
																Name: "r",
															},
															Property: "cpu",
														},
														Right: &semantic.StringLiteral{
															Value: "cpu2",
														},
													},
												},
											},
										},
									},
								},
							},
						},
					},
					{
						ID: flux.OperationID("count"), Spec: &universe.CountOpSpec{},
					},
				},
				Edges: []flux.Edge{
					{
						Parent: flux.OperationID("from"),
						Child:  flux.OperationID("where"),
					},
					{
						Parent: flux.OperationID("where"),
						Child:  flux.OperationID("count"),
					},
				},
			},
		},
		{
			name:   "range of time but no aggregates",
			promql: `node_cpu{mode="user"}[2m] offset 5m`,
			want: &flux.Spec{
				Operations: []*flux.Operation{
					{
						ID: flux.OperationID("from"),
						Spec: &influxdb.FromOpSpec{
							Bucket: influxdb.NameOrID{Name: "prometheus"},
						},
					},
					{
						ID: flux.OperationID("range"),
						Spec: &universe.RangeOpSpec{
							Start: flux.Time{Relative: -time.Minute * 7},
						},
					},
					{
						ID: "where",
						Spec: &universe.FilterOpSpec{
							Fn: interpreter.ResolvedFunction{
								Scope: nil,
								Fn: &semantic.FunctionExpression{
									Parameters: &semantic.FunctionParameters{
										List: []*semantic.FunctionParameter{{Key: &semantic.Identifier{Name: "r"}}},
									},
									Block: &semantic.Block{
										Body: []semantic.Statement{
											&semantic.ReturnStatement{
												Argument: &semantic.LogicalExpression{
													Operator: ast.AndOperator,
													Left: &semantic.BinaryExpression{
														Operator: ast.EqualOperator,
														Left: &semantic.MemberExpression{
															Object: &semantic.IdentifierExpression{
																Name: "r",
															},
															Property: "_metric",
														},
														Right: &semantic.StringLiteral{
															Value: "node_cpu",
														},
													},
													Right: &semantic.BinaryExpression{
														Operator: ast.EqualOperator,
														Left: &semantic.MemberExpression{
															Object: &semantic.IdentifierExpression{
																Name: "r",
															},
															Property: "mode",
														},
														Right: &semantic.StringLiteral{
															Value: "user",
														},
													},
												},
											},
										},
									},
								},
							},
						},
					},
				},
				Edges: []flux.Edge{
					{
						Parent: flux.OperationID("from"),
						Child:  flux.OperationID("range"),
					},
					{
						Parent: flux.OperationID("range"),
						Child:  flux.OperationID("where"),
					},
				},
			},
		},

		{
			name:   "sum over a range",
			promql: `sum(node_cpu{_measurement="m0"}[170h])`,
			want: &flux.Spec{
				Operations: []*flux.Operation{
					{
						ID: flux.OperationID("from"),
						Spec: &influxdb.FromOpSpec{
							Bucket: influxdb.NameOrID{Name: "prometheus"},
						},
					},
					{
						ID: flux.OperationID("range"),
						Spec: &universe.RangeOpSpec{
							Start: flux.Time{Relative: -170 * time.Hour},
						},
					},
					{
						ID: "where",
						Spec: &universe.FilterOpSpec{
							Fn: interpreter.ResolvedFunction{
								Scope: nil,
								Fn: &semantic.FunctionExpression{
									Parameters: &semantic.FunctionParameters{
										List: []*semantic.FunctionParameter{{Key: &semantic.Identifier{Name: "r"}}},
									},
									Block: &semantic.Block{
										Body: []semantic.Statement{
											&semantic.ReturnStatement{
												Argument: &semantic.LogicalExpression{
													Operator: ast.AndOperator,
													Left: &semantic.BinaryExpression{
														Operator: ast.EqualOperator,
														Left: &semantic.MemberExpression{
															Object: &semantic.IdentifierExpression{
																Name: "r",
															},
															Property: "_metric",
														},
														Right: &semantic.StringLiteral{
															Value: "node_cpu",
														},
													},
													Right: &semantic.BinaryExpression{
														Operator: ast.EqualOperator,
														Left: &semantic.MemberExpression{
															Object: &semantic.IdentifierExpression{
																Name: "r",
															},
															Property: "_measurement",
														},
														Right: &semantic.StringLiteral{
															Value: "m0",
														},
													},
												},
											},
										},
									},
								},
							},
						},
					},
					{
						ID: flux.OperationID("sum"), Spec: &universe.SumOpSpec{},
					},
				},
				Edges: []flux.Edge{
					{
						Parent: flux.OperationID("from"),
						Child:  flux.OperationID("range"),
					},
					{
						Parent: flux.OperationID("range"),
						Child:  flux.OperationID("where"),
					},
					{
						Parent: flux.OperationID("where"),
						Child:  flux.OperationID("sum"),
					},
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := Build(tt.promql, tt.opts...)

			if (err != nil) != tt.wantErr {
				t.Errorf("Build() %s error = %v, wantErr %v", tt.promql, err, tt.wantErr)
				return
			}
			opts := append(semantictest.CmpOptions, []cmp.Option{cmp.AllowUnexported(flux.Spec{}), cmpopts.IgnoreUnexported(flux.Spec{})}...)
			if !cmp.Equal(tt.want, got, opts...) {
				t.Errorf("Build() = %s -want/+got\n%s", tt.promql, cmp.Diff(tt.want, got, opts...))
			}
		})
	}
}
