package mathexp

import (
	"fmt"
	"math"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/grafana/grafana-plugin-sdk-go/data"
	"github.com/grafana/grafana/pkg/expr/mathexp/parse"
	"github.com/grafana/grafana/pkg/infra/tracing"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestNaN(t *testing.T) {
	var tests = []struct {
		name      string
		expr      string
		vars      Vars
		newErrIs  assert.ErrorAssertionFunc
		execErrIs assert.ErrorAssertionFunc
		results   Results
		willPanic bool
	}{
		{
			name:      "unary !: Op Number(NaN) is NaN",
			expr:      "! $A",
			vars:      Vars{"A": resultValuesNoErr(makeNumber("", nil, NaN))},
			newErrIs:  assert.NoError,
			execErrIs: assert.NoError,
			results:   resultValuesNoErr(makeNumber("", nil, NaN)),
		},
		{
			name:      "unary -: Op Number(NaN) is NaN",
			expr:      "-$A",
			vars:      Vars{"A": resultValuesNoErr(makeNumber("", nil, NaN))},
			newErrIs:  assert.NoError,
			execErrIs: assert.NoError,
			results:   resultValuesNoErr(makeNumber("", nil, NaN)),
		},
		{
			name:      "binary: Scalar Op(Non-AND/OR) Number(NaN) is NaN",
			expr:      "1 * $A",
			vars:      Vars{"A": resultValuesNoErr(makeNumber("", nil, NaN))},
			newErrIs:  assert.NoError,
			execErrIs: assert.NoError,
			results:   resultValuesNoErr(makeNumber("", nil, NaN)),
		},
		{
			name:      "binary: Scalar Op(AND/OR) Number(NaN) is 0/1",
			expr:      "1 || $A",
			vars:      Vars{"A": resultValuesNoErr(makeNumber("", nil, NaN))},
			newErrIs:  assert.NoError,
			execErrIs: assert.NoError,
			results:   resultValuesNoErr(makeNumber("", nil, float64Pointer(1))),
		},
		{
			name: "binary: Scalar Op(Non-AND/OR) Series(with NaN value) is NaN)",
			expr: "1 - $A",
			vars: Vars{
				"A": resultValuesNoErr(
					makeSeries("temp", nil, tp{
						time.Unix(5, 0), float64Pointer(2),
					}, tp{
						time.Unix(10, 0), NaN,
					}),
				),
			},
			newErrIs:  assert.NoError,
			execErrIs: assert.NoError,
			results: resultValuesNoErr(
				makeSeries("", nil, tp{
					time.Unix(5, 0), float64Pointer(-1),
				}, tp{
					time.Unix(10, 0), NaN,
				}),
			),
		},
		{
			name: "binary: Number Op(Non-AND/OR) Series(with NaN value) is Series with NaN",
			expr: "$A == $B",
			vars: Vars{
				"A": resultValuesNoErr(
					makeSeries("temp", nil, tp{
						time.Unix(5, 0), float64Pointer(2),
					}, tp{
						time.Unix(10, 0), NaN,
					}),
				),
				"B": resultValuesNoErr(makeNumber("", nil, float64Pointer(0))),
			},
			newErrIs:  assert.NoError,
			execErrIs: assert.NoError,
			results: resultValuesNoErr(
				makeSeries("", nil, tp{
					time.Unix(5, 0), float64Pointer(0),
				}, tp{
					time.Unix(10, 0), NaN,
				}),
			),
		},
		{
			name: "binary: Number(NaN) Op Series(with NaN value) is Series with NaN",
			expr: "$A + $B",
			vars: Vars{
				"A": resultValuesNoErr(
					makeSeries("temp", nil, tp{
						time.Unix(5, 0), float64Pointer(2),
					}, tp{
						time.Unix(10, 0), NaN,
					}),
				),
				"B": resultValuesNoErr(makeNumber("", nil, NaN)),
			},
			newErrIs:  assert.NoError,
			execErrIs: assert.NoError,
			results: resultValuesNoErr(
				makeSeries("", nil, tp{
					time.Unix(5, 0), NaN,
				}, tp{
					time.Unix(10, 0), NaN,
				}),
			),
		},
	}

	opt := cmp.Comparer(func(x, y float64) bool {
		return (math.IsNaN(x) && math.IsNaN(y)) || x == y
	})
	options := append([]cmp.Option{opt}, data.FrameTestCompareOptions()...)

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			testBlock := func() {
				e, err := New(tt.expr)
				tt.newErrIs(t, err)
				if e != nil {
					res, err := e.Execute("", tt.vars, tracing.InitializeTracerForTest())
					tt.execErrIs(t, err)
					if diff := cmp.Diff(res, tt.results, options...); diff != "" {
						assert.FailNow(t, tt.name, diff)
					}
				}
			}
			if tt.willPanic {
				assert.Panics(t, testBlock)
			} else {
				assert.NotPanics(t, testBlock)
			}
		})
	}
}

func TestNullValues(t *testing.T) {
	var tests = []struct {
		name      string
		expr      string
		vars      Vars
		newErrIs  assert.ErrorAssertionFunc
		execErrIs assert.ErrorAssertionFunc
		results   Results
		willPanic bool
	}{
		{
			name:      "scalar: unary ! null(): is null",
			expr:      "! null()",
			newErrIs:  assert.NoError,
			execErrIs: assert.NoError,
			results:   NewScalarResults("", nil),
		},
		{
			name:      "scalar: binary null() + null(): is null",
			expr:      "null() + null()",
			newErrIs:  assert.NoError,
			execErrIs: assert.NoError,
			results:   NewScalarResults("", nil),
		},
		{
			name:      "scalar: binary 1 + null(): is null",
			expr:      "1 + null()",
			newErrIs:  assert.NoError,
			execErrIs: assert.NoError,
			results:   NewScalarResults("", nil),
		},
		{
			name: "series: unary with a null value in it has a null value in result",
			expr: "- $A",
			vars: Vars{
				"A": resultValuesNoErr(
					makeSeries("", nil, tp{
						time.Unix(5, 0), float64Pointer(1),
					}, tp{
						time.Unix(10, 0), nil,
					}),
				),
			},
			newErrIs:  assert.NoError,
			execErrIs: assert.NoError,
			results: resultValuesNoErr(
				makeSeries("", nil, tp{
					time.Unix(5, 0), float64Pointer(-1),
				}, tp{
					time.Unix(10, 0), nil,
				}),
			),
		},
		{
			name: "series: binary with a null value in it has a null value in result",
			expr: "$A - $A",
			vars: Vars{
				"A": resultValuesNoErr(
					makeSeries("", nil, tp{
						time.Unix(5, 0), float64Pointer(1),
					}, tp{
						time.Unix(10, 0), nil,
					}),
				),
			},
			newErrIs:  assert.NoError,
			execErrIs: assert.NoError,
			results: resultValuesNoErr(
				makeSeries("", nil, tp{
					time.Unix(5, 0), float64Pointer(0),
				}, tp{
					time.Unix(10, 0), nil,
				}),
			),
		},
		{
			name: "series and scalar: binary with a null value in it has a nil value in result",
			expr: "$A - 1",
			vars: Vars{
				"A": resultValuesNoErr(
					makeSeries("", nil, tp{
						time.Unix(5, 0), float64Pointer(1),
					}, tp{
						time.Unix(10, 0), nil,
					}),
				),
			},
			newErrIs:  assert.NoError,
			execErrIs: assert.NoError,
			results: resultValuesNoErr(
				makeSeries("", nil, tp{
					time.Unix(5, 0), float64Pointer(0),
				}, tp{
					time.Unix(10, 0), nil,
				}),
			),
		},
		{
			name: "number: unary ! null number: is null",
			expr: "! $A",
			vars: Vars{
				"A": resultValuesNoErr(makeNumber("", nil, nil)),
			},
			newErrIs:  assert.NoError,
			execErrIs: assert.NoError,
			results:   resultValuesNoErr(makeNumber("", nil, nil)),
		},
		{
			name: "number: binary null number and null number: is null",
			expr: "$A + $A",
			vars: Vars{
				"A": resultValuesNoErr(makeNumber("", nil, nil)),
			},
			newErrIs:  assert.NoError,
			execErrIs: assert.NoError,
			results:   resultValuesNoErr(makeNumber("", nil, nil)),
		},
		{
			name: "number: binary non-null number and null number: is null",
			expr: "$A * $B",
			vars: Vars{
				"A": resultValuesNoErr(makeNumber("", nil, nil)),
				"B": resultValuesNoErr(makeNumber("", nil, float64Pointer(1))),
			},
			newErrIs:  assert.NoError,
			execErrIs: assert.NoError,
			results:   resultValuesNoErr(makeNumber("", nil, nil)),
		},
		{
			name: "number and series: binary non-null number and series with a null: is null",
			expr: "$A * $B",
			vars: Vars{
				"A": resultValuesNoErr(makeNumber("", nil, float64Pointer(1))),
				"B": resultValuesNoErr(
					makeSeries("", nil, tp{
						time.Unix(5, 0), float64Pointer(1),
					}, tp{
						time.Unix(10, 0), nil,
					}),
				),
			},
			newErrIs:  assert.NoError,
			execErrIs: assert.NoError,
			results: resultValuesNoErr(
				makeSeries("", nil, tp{
					time.Unix(5, 0), float64Pointer(1),
				}, tp{
					time.Unix(10, 0), nil,
				}),
			),
		},
		{
			name: "number and series: binary null number and series with non-null and null: is null and null",
			expr: "$A * $B",
			vars: Vars{
				"A": resultValuesNoErr(makeNumber("", nil, nil)),
				"B": resultValuesNoErr(
					makeSeries("", nil, tp{
						time.Unix(5, 0), float64Pointer(1),
					}, tp{
						time.Unix(10, 0), nil,
					}),
				),
			},
			newErrIs:  assert.NoError,
			execErrIs: assert.NoError,
			results: resultValuesNoErr(
				makeSeries("", nil, tp{
					time.Unix(5, 0), nil,
				}, tp{
					time.Unix(10, 0), nil,
				}),
			),
		},
	}

	// go-cmp instead of testify assert is used to compare results here
	// because it supports an option for NaN equality.
	// https://github.com/stretchr/testify/pull/691#issuecomment-528457166
	opt := cmp.Comparer(func(x, y float64) bool {
		return (math.IsNaN(x) && math.IsNaN(y)) || x == y
	})
	options := append([]cmp.Option{opt}, data.FrameTestCompareOptions()...)

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			testBlock := func() {
				e, err := New(tt.expr)
				tt.newErrIs(t, err)
				if e != nil {
					res, err := e.Execute("", tt.vars, tracing.InitializeTracerForTest())
					tt.execErrIs(t, err)
					if diff := cmp.Diff(tt.results, res, options...); diff != "" {
						t.Errorf("Result mismatch (-want +got):\n%s", diff)
					}
				}
			}
			if tt.willPanic {
				assert.Panics(t, testBlock)
			} else {
				testBlock()
			}
		})
	}
}

func TestNoData(t *testing.T) {
	t.Run("unary operation return NoData if input NoData", func(t *testing.T) {
		unaryOps := []string{
			"abs($A)",
			"is_inf($A)",
			"is_nan($A)",
			"is_null($A)",
			"is_number($A)",
			"log($A)",
			"round($A)",
			"ceil($A)",
			"floor($A)",
			"!$A",
			"-$A",
		}
		vars := Vars{"A": resultValuesNoErr(NewNoData())}
		for _, expr := range unaryOps {
			t.Run(fmt.Sprintf("op: %s", expr), func(t *testing.T) {
				e, err := New(expr)
				require.NoError(t, err)
				if e != nil {
					res, err := e.Execute("", vars, tracing.InitializeTracerForTest())
					require.NoError(t, err)
					require.Len(t, res.Values, 1)
					require.Equal(t, NewNoData(), res.Values[0])
				}
			})
		}
	})

	makeVars := func(a, b Value) Vars {
		return Vars{
			"A": resultValuesNoErr(a),
			"B": resultValuesNoErr(b),
		}
	}

	bin_ops := []string{
		"$A || $B",
		"$A && $B",
		"$A + $B",
		"$A * $B",
		"$A - $B",
		"$A / $B",
		"$A ** $B",
		"$A % $B",
		"$A == $B",
		"$A > $B",
		"$A != $B",
		"$A < $B",
		"$A >= $B",
		"$A <= $B",
		"$A || $B",
		"$A && $B",
	}
	series := makeSeries("test", nil, tp{time.Unix(5, 0), float64Pointer(2)})
	for _, expr := range bin_ops {
		t.Run(fmt.Sprintf("op: %s", expr), func(t *testing.T) {
			e, err := New(expr)
			require.NoError(t, err)
			if e != nil {
				t.Run("$A,$B=nodata", func(t *testing.T) {
					res, err := e.Execute("", makeVars(NewNoData(), NewNoData()), tracing.InitializeTracerForTest())
					require.NoError(t, err)
					require.Len(t, res.Values, 1)
					require.Equal(t, parse.TypeNoData, res.Values[0].Type())
				})

				t.Run("$A=nodata, $B=series", func(t *testing.T) {
					res, err := e.Execute("", makeVars(NewNoData(), series), tracing.InitializeTracerForTest())
					require.NoError(t, err)
					require.Len(t, res.Values, 1)
					require.Equal(t, parse.TypeNoData, res.Values[0].Type())
				})

				t.Run("$A=series, $B=nodata", func(t *testing.T) {
					res, err := e.Execute("", makeVars(NewNoData(), series), tracing.InitializeTracerForTest())
					require.NoError(t, err)
					require.Len(t, res.Values, 1)
					require.Equal(t, parse.TypeNoData, res.Values[0].Type())
				})
			}
		})
	}
}
