// SPDX-FileCopyrightText: 2022 Free Mobile
// SPDX-License-Identifier: AGPL-3.0-only

package console

import (
	"fmt"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"go.uber.org/mock/gomock"

	"akvorado/common/helpers"
	"akvorado/common/schema"
	"akvorado/console/query"
)

func TestGraphLineInputReverseDirection(t *testing.T) {
	input := graphLineHandlerInput{
		graphCommonHandlerInput: graphCommonHandlerInput{
			schema: schema.NewMock(t),
			Start:  time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
			End:    time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
			Dimensions: query.Columns{
				query.NewColumn("ExporterName"),
				query.NewColumn("InIfProvider"),
			},
			Filter: query.NewFilter("DstCountry = 'FR' AND SrcCountry = 'US'"),
			Units:  "l3bps",
		},
		Points: 100,
	}
	original1 := fmt.Sprintf("%+v", input)
	expected := graphLineHandlerInput{
		graphCommonHandlerInput: graphCommonHandlerInput{
			Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
			End:   time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
			Dimensions: query.Columns{
				query.NewColumn("ExporterName"),
				query.NewColumn("OutIfProvider"),
			},
			Filter: query.NewFilter("SrcCountry = 'FR' AND DstCountry = 'US'"),
			Units:  "l3bps",
		},
		Points: 100,
	}
	input.Filter.Validate(input.schema)
	expected.Filter.Validate(input.schema)
	query.Columns(input.Dimensions).Validate(input.schema)
	query.Columns(expected.Dimensions).Validate(input.schema)
	got := input.reverseDirection()
	original2 := fmt.Sprintf("%+v", input)
	if diff := helpers.Diff(got, expected); diff != "" {
		t.Fatalf("reverseDirection() (-got, +want):\n%s", diff)
	}
	if original1 != original2 {
		t.Fatalf("reverseDirection() modified original to:\n-%s\n+%s", original1, original2)
	}
}

func TestGraphPreviousPeriod(t *testing.T) {
	const longForm = "Jan 2, 2006 at 15:04"
	cases := []struct {
		Pos           helpers.Pos
		Start         string
		End           string
		ExpectedStart string
		ExpectedEnd   string
	}{
		{
			helpers.Mark(),
			"Jan 2, 2020 at 15:04", "Jan 2, 2020 at 16:04",
			"Jan 2, 2020 at 14:04", "Jan 2, 2020 at 15:04",
		}, {
			helpers.Mark(),
			"Jan 2, 2020 at 15:04", "Jan 2, 2020 at 16:34",
			"Jan 2, 2020 at 14:04", "Jan 2, 2020 at 15:34",
		}, {
			helpers.Mark(),
			"Jan 2, 2020 at 15:04", "Jan 2, 2020 at 17:34",
			"Jan 1, 2020 at 15:04", "Jan 1, 2020 at 17:34",
		}, {
			helpers.Mark(),
			"Jan 2, 2020 at 15:04", "Jan 3, 2020 at 17:34",
			"Jan 1, 2020 at 15:04", "Jan 2, 2020 at 17:34",
		}, {
			helpers.Mark(),
			"Jan 10, 2020 at 15:04", "Jan 13, 2020 at 17:34",
			"Jan 3, 2020 at 15:04", "Jan 6, 2020 at 17:34",
		}, {
			helpers.Mark(),
			"Jan 10, 2020 at 15:04", "Jan 15, 2020 at 17:34",
			"Jan 3, 2020 at 15:04", "Jan 8, 2020 at 17:34",
		}, {
			helpers.Mark(),
			"Jan 10, 2020 at 15:04", "Jan 20, 2020 at 17:34",
			"Jan 3, 2020 at 15:04", "Jan 13, 2020 at 17:34",
		}, {
			helpers.Mark(),
			"Feb 10, 2020 at 15:04", "Feb 25, 2020 at 17:34",
			"Jan 13, 2020 at 15:04", "Jan 28, 2020 at 17:34",
		}, {
			helpers.Mark(),
			"Feb 10, 2020 at 15:04", "Mar 25, 2020 at 17:34",
			"Jan 13, 2020 at 15:04", "Feb 26, 2020 at 17:34",
		}, {
			helpers.Mark(),
			"Feb 10, 2020 at 15:04", "Jul 25, 2020 at 17:34",
			"Feb 10, 2019 at 15:04", "Jul 25, 2019 at 17:34",
		}, {
			helpers.Mark(),
			"Feb 10, 2019 at 15:04", "Jul 25, 2020 at 17:34",
			"Feb 10, 2018 at 15:04", "Jul 25, 2019 at 17:34",
		},
	}
	for _, tc := range cases {
		t.Run(fmt.Sprintf("%s to %s", tc.Start, tc.End), func(t *testing.T) {
			start, err := time.Parse(longForm, tc.Start)
			if err != nil {
				t.Fatalf("%stime.Parse(%q) error:\n%+v", tc.Pos, tc.Start, err)
			}
			end, err := time.Parse(longForm, tc.End)
			if err != nil {
				t.Fatalf("%stime.Parse(%q) error:\n%+v", tc.Pos, tc.End, err)
			}
			expectedStart, err := time.Parse(longForm, tc.ExpectedStart)
			if err != nil {
				t.Fatalf("%stime.Parse(%q) error:\n%+v", tc.Pos, tc.ExpectedStart, err)
			}
			expectedEnd, err := time.Parse(longForm, tc.ExpectedEnd)
			if err != nil {
				t.Fatalf("%stime.Parse(%q) error:\n%+v", tc.Pos, tc.ExpectedEnd, err)
			}
			input := graphLineHandlerInput{
				graphCommonHandlerInput: graphCommonHandlerInput{
					schema: schema.NewMock(t),
					Start:  start,
					End:    end,
					Dimensions: query.Columns{
						query.NewColumn("ExporterAddress"),
						query.NewColumn("ExporterName"),
					},
				},
			}
			query.Columns(input.Dimensions).Validate(input.schema)
			got := input.previousPeriod()
			expected := graphLineHandlerInput{
				graphCommonHandlerInput: graphCommonHandlerInput{
					Start:      expectedStart,
					End:        expectedEnd,
					Dimensions: []query.Column{},
				},
			}
			if diff := helpers.Diff(got, expected); diff != "" {
				t.Fatalf("%spreviousPeriod() (-got, +want):\n%s", tc.Pos, diff)
			}
		})
	}
}

func TestGraphQuerySQL(t *testing.T) {
	cases := []struct {
		Description string
		Pos         helpers.Pos
		Input       graphLineHandlerInput
		Expected    []templateQuery
	}{
		{
			Description: "no dimensions, no filters, bps",
			Pos:         helpers.Mark(),
			Input: graphLineHandlerInput{
				graphCommonHandlerInput: graphCommonHandlerInput{
					Start:      time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
					End:        time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
					Dimensions: []query.Column{},
					Filter:     query.Filter{},
					Units:      "l3bps",
				},
				Points: 100,
			},
			Expected: []templateQuery{
				{
					Context: inputContext{
						Start:  time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
						End:    time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
						Points: 100,
						Units:  "l3bps",
					},
					Template: `WITH
 source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1)
SELECT 1 AS axis, * FROM (
SELECT
 {{ call .ToStartOfInterval "TimeReceived" }} AS time,
 {{ .Units }}/{{ .Interval }} AS xps,
 emptyArrayString() AS dimensions
FROM source
WHERE {{ .Timefilter }}
GROUP BY time, dimensions
ORDER BY time WITH FILL
 FROM {{ .TimefilterStart }}
 TO {{ .TimefilterEnd }} + INTERVAL 1 second
 STEP {{ .Interval }}
 INTERPOLATE (dimensions AS emptyArrayString()))`,
				},
			},
		}, {
			Description: "no dimensions, no filters, l2 bps",
			Pos:         helpers.Mark(),
			Input: graphLineHandlerInput{
				graphCommonHandlerInput: graphCommonHandlerInput{
					Start:      time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
					End:        time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
					Dimensions: []query.Column{},
					Filter:     query.Filter{},
					Units:      "l2bps",
				},
				Points: 100,
			},
			Expected: []templateQuery{
				{
					Context: inputContext{
						Start:  time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
						End:    time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
						Points: 100,
						Units:  "l2bps",
					},
					Template: `WITH
 source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1)
SELECT 1 AS axis, * FROM (
SELECT
 {{ call .ToStartOfInterval "TimeReceived" }} AS time,
 {{ .Units }}/{{ .Interval }} AS xps,
 emptyArrayString() AS dimensions
FROM source
WHERE {{ .Timefilter }}
GROUP BY time, dimensions
ORDER BY time WITH FILL
 FROM {{ .TimefilterStart }}
 TO {{ .TimefilterEnd }} + INTERVAL 1 second
 STEP {{ .Interval }}
 INTERPOLATE (dimensions AS emptyArrayString()))`,
				},
			},
		}, {
			Description: "no dimensions, no filters, pps",
			Pos:         helpers.Mark(),
			Input: graphLineHandlerInput{
				graphCommonHandlerInput: graphCommonHandlerInput{
					Start:      time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
					End:        time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
					Dimensions: []query.Column{},
					Filter:     query.Filter{},
					Units:      "pps",
				},
				Points: 100,
			},
			Expected: []templateQuery{
				{
					Context: inputContext{
						Start:  time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
						End:    time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
						Points: 100,
						Units:  "pps",
					},
					Template: `WITH
 source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1)
SELECT 1 AS axis, * FROM (
SELECT
 {{ call .ToStartOfInterval "TimeReceived" }} AS time,
 {{ .Units }}/{{ .Interval }} AS xps,
 emptyArrayString() AS dimensions
FROM source
WHERE {{ .Timefilter }}
GROUP BY time, dimensions
ORDER BY time WITH FILL
 FROM {{ .TimefilterStart }}
 TO {{ .TimefilterEnd }} + INTERVAL 1 second
 STEP {{ .Interval }}
 INTERPOLATE (dimensions AS emptyArrayString()))`,
				},
			},
		}, {
			Description: "truncated source address",
			Pos:         helpers.Mark(),
			Input: graphLineHandlerInput{
				graphCommonHandlerInput: graphCommonHandlerInput{
					Start:          time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
					End:            time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
					Dimensions:     []query.Column{query.NewColumn("SrcAddr")},
					Filter:         query.NewFilter("SrcAddr << 1.0.0.0/8"),
					TruncateAddrV4: 24,
					TruncateAddrV6: 48,
					Units:          "l3bps",
				},
				Points: 100,
			},
			Expected: []templateQuery{
				{
					Context: inputContext{
						Start:             time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
						End:               time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
						Points:            100,
						Units:             "l3bps",
						MainTableRequired: true,
					},
					Template: `WITH
 source AS (SELECT * REPLACE (tupleElement(IPv6CIDRToRange(SrcAddr, if(tupleElement(IPv6CIDRToRange(SrcAddr, 96), 1) = toIPv6('::ffff:0.0.0.0'), 120, 48)), 1) AS SrcAddr) FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1),
 rows AS (SELECT SrcAddr FROM source WHERE {{ .Timefilter }} AND (SrcAddr BETWEEN toIPv6('::ffff:1.0.0.0') AND toIPv6('::ffff:1.255.255.255')) GROUP BY SrcAddr ORDER BY {{ .Units }} DESC LIMIT 0)
SELECT 1 AS axis, * FROM (
SELECT
 {{ call .ToStartOfInterval "TimeReceived" }} AS time,
 {{ .Units }}/{{ .Interval }} AS xps,
 if((SrcAddr) IN rows, [replaceRegexpOne(IPv6NumToString(SrcAddr), '^::ffff:', '')], ['Other']) AS dimensions
FROM source
WHERE {{ .Timefilter }} AND (SrcAddr BETWEEN toIPv6('::ffff:1.0.0.0') AND toIPv6('::ffff:1.255.255.255'))
GROUP BY time, dimensions
ORDER BY time WITH FILL
 FROM {{ .TimefilterStart }}
 TO {{ .TimefilterEnd }} + INTERVAL 1 second
 STEP {{ .Interval }}
 INTERPOLATE (dimensions AS ['Other']))`,
				},
			},
		}, {
			Description: "no dimensions",
			Pos:         helpers.Mark(),
			Input: graphLineHandlerInput{
				graphCommonHandlerInput: graphCommonHandlerInput{
					Start:      time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
					End:        time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
					Dimensions: []query.Column{},
					Filter:     query.NewFilter("DstCountry = 'FR' AND SrcCountry = 'US'"),
					Units:      "l3bps",
				},
				Points: 100,
			},
			Expected: []templateQuery{
				{
					Context: inputContext{
						Start:  time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
						End:    time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
						Points: 100,
						Units:  "l3bps",
					},
					Template: `WITH
 source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1)
SELECT 1 AS axis, * FROM (
SELECT
 {{ call .ToStartOfInterval "TimeReceived" }} AS time,
 {{ .Units }}/{{ .Interval }} AS xps,
 emptyArrayString() AS dimensions
FROM source
WHERE {{ .Timefilter }} AND (DstCountry = 'FR' AND SrcCountry = 'US')
GROUP BY time, dimensions
ORDER BY time WITH FILL
 FROM {{ .TimefilterStart }}
 TO {{ .TimefilterEnd }} + INTERVAL 1 second
 STEP {{ .Interval }}
 INTERPOLATE (dimensions AS emptyArrayString()))`,
				},
			},
		}, {
			Description: "no dimensions, escaped filter",
			Pos:         helpers.Mark(),
			Input: graphLineHandlerInput{
				graphCommonHandlerInput: graphCommonHandlerInput{
					Start:      time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
					End:        time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
					Dimensions: []query.Column{},
					Filter:     query.NewFilter("InIfDescription = '{{ hello }}' AND SrcCountry = 'US'"),
					Units:      "l3bps",
				},
				Points: 100,
			},
			Expected: []templateQuery{
				{
					Context: inputContext{
						Start:  time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
						End:    time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
						Points: 100,
						Units:  "l3bps",
					},
					Template: `WITH
 source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1)
SELECT 1 AS axis, * FROM (
SELECT
 {{ call .ToStartOfInterval "TimeReceived" }} AS time,
 {{ .Units }}/{{ .Interval }} AS xps,
 emptyArrayString() AS dimensions
FROM source
WHERE {{ .Timefilter }} AND (InIfDescription = '{{"{{"}} hello }}' AND SrcCountry = 'US')
GROUP BY time, dimensions
ORDER BY time WITH FILL
 FROM {{ .TimefilterStart }}
 TO {{ .TimefilterEnd }} + INTERVAL 1 second
 STEP {{ .Interval }}
 INTERPOLATE (dimensions AS emptyArrayString()))`,
				},
			},
		}, {
			Description: "no dimensions, reverse direction",
			Pos:         helpers.Mark(),
			Input: graphLineHandlerInput{
				graphCommonHandlerInput: graphCommonHandlerInput{
					Start:      time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
					End:        time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
					Dimensions: []query.Column{},
					Filter:     query.NewFilter("DstCountry = 'FR' AND SrcCountry = 'US'"),
					Units:      "l3bps",
				},
				Points:        100,
				Bidirectional: true,
			},
			Expected: []templateQuery{
				{
					Context: inputContext{
						Start:  time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
						End:    time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
						Points: 100,
						Units:  "l3bps",
					},
					Template: `WITH
 source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1)
SELECT 1 AS axis, * FROM (
SELECT
 {{ call .ToStartOfInterval "TimeReceived" }} AS time,
 {{ .Units }}/{{ .Interval }} AS xps,
 emptyArrayString() AS dimensions
FROM source
WHERE {{ .Timefilter }} AND (DstCountry = 'FR' AND SrcCountry = 'US')
GROUP BY time, dimensions
ORDER BY time WITH FILL
 FROM {{ .TimefilterStart }}
 TO {{ .TimefilterEnd }} + INTERVAL 1 second
 STEP {{ .Interval }}
 INTERPOLATE (dimensions AS emptyArrayString()))`,
				}, {
					Context: inputContext{
						Start:  time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
						End:    time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
						Points: 100,
						Units:  "l3bps",
					},
					Template: `SELECT 2 AS axis, * FROM (
SELECT
 {{ call .ToStartOfInterval "TimeReceived" }} AS time,
 {{ .Units }}/{{ .Interval }} AS xps,
 emptyArrayString() AS dimensions
FROM source
WHERE {{ .Timefilter }} AND (SrcCountry = 'FR' AND DstCountry = 'US')
GROUP BY time, dimensions
ORDER BY time WITH FILL
 FROM {{ .TimefilterStart }}
 TO {{ .TimefilterEnd }} + INTERVAL 1 second
 STEP {{ .Interval }}
 INTERPOLATE (dimensions AS emptyArrayString()))`,
				},
			},
		}, {
			Description: "no dimensions, reverse direction, inl2%",
			Pos:         helpers.Mark(),
			Input: graphLineHandlerInput{
				graphCommonHandlerInput: graphCommonHandlerInput{
					Start:      time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
					End:        time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
					Dimensions: []query.Column{},
					Filter:     query.NewFilter("DstCountry = 'FR' AND SrcCountry = 'US'"),
					Units:      "inl2%",
				},
				Points:        100,
				Bidirectional: true,
			},
			Expected: []templateQuery{
				{
					Context: inputContext{
						Start:  time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
						End:    time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
						Points: 100,
						Units:  "inl2%",
					},
					Template: `WITH
 source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1)
SELECT 1 AS axis, * FROM (
SELECT
 {{ call .ToStartOfInterval "TimeReceived" }} AS time,
 {{ .Units }}/{{ .Interval }} AS xps,
 emptyArrayString() AS dimensions
FROM source
WHERE {{ .Timefilter }} AND (DstCountry = 'FR' AND SrcCountry = 'US')
GROUP BY time, dimensions
ORDER BY time WITH FILL
 FROM {{ .TimefilterStart }}
 TO {{ .TimefilterEnd }} + INTERVAL 1 second
 STEP {{ .Interval }}
 INTERPOLATE (dimensions AS emptyArrayString()))`,
				}, {
					Context: inputContext{
						Start:  time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
						End:    time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
						Points: 100,
						Units:  "outl2%",
					},
					Template: `SELECT 2 AS axis, * FROM (
SELECT
 {{ call .ToStartOfInterval "TimeReceived" }} AS time,
 {{ .Units }}/{{ .Interval }} AS xps,
 emptyArrayString() AS dimensions
FROM source
WHERE {{ .Timefilter }} AND (SrcCountry = 'FR' AND DstCountry = 'US')
GROUP BY time, dimensions
ORDER BY time WITH FILL
 FROM {{ .TimefilterStart }}
 TO {{ .TimefilterEnd }} + INTERVAL 1 second
 STEP {{ .Interval }}
 INTERPOLATE (dimensions AS emptyArrayString()))`,
				},
			},
		}, {
			Description: "no filters",
			Pos:         helpers.Mark(),
			Input: graphLineHandlerInput{
				graphCommonHandlerInput: graphCommonHandlerInput{
					Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
					End:   time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
					Limit: 20,
					Dimensions: []query.Column{
						query.NewColumn("ExporterName"),
						query.NewColumn("InIfProvider"),
					},
					Filter: query.Filter{},
					Units:  "l3bps",
				},
				Points: 100,
			},
			Expected: []templateQuery{
				{
					Context: inputContext{
						Start:  time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
						End:    time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
						Points: 100,
						Units:  "l3bps",
					},
					Template: `WITH
 source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1),
 rows AS (SELECT ExporterName, InIfProvider FROM source WHERE {{ .Timefilter }} GROUP BY ExporterName, InIfProvider ORDER BY {{ .Units }} DESC LIMIT 20)
SELECT 1 AS axis, * FROM (
SELECT
 {{ call .ToStartOfInterval "TimeReceived" }} AS time,
 {{ .Units }}/{{ .Interval }} AS xps,
 if((ExporterName, InIfProvider) IN rows, [ExporterName, InIfProvider], ['Other', 'Other']) AS dimensions
FROM source
WHERE {{ .Timefilter }}
GROUP BY time, dimensions
ORDER BY time WITH FILL
 FROM {{ .TimefilterStart }}
 TO {{ .TimefilterEnd }} + INTERVAL 1 second
 STEP {{ .Interval }}
 INTERPOLATE (dimensions AS ['Other', 'Other']))`,
				},
			},
		}, {
			Description: "no filters, limitType by max",
			Pos:         helpers.Mark(),
			Input: graphLineHandlerInput{
				graphCommonHandlerInput: graphCommonHandlerInput{
					Start:     time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
					End:       time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
					Limit:     20,
					LimitType: "max",
					Dimensions: []query.Column{
						query.NewColumn("ExporterName"),
						query.NewColumn("InIfProvider"),
					},
					Filter: query.Filter{},
					Units:  "l3bps",
				},
				Points: 100,
			},
			Expected: []templateQuery{
				{
					Context: inputContext{
						Start:  time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
						End:    time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
						Points: 100,
						Units:  "l3bps",
					},
					Template: `WITH
 source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1),
 rows AS (SELECT ExporterName, InIfProvider FROM ( SELECT ExporterName, InIfProvider, {{ .Units }} AS sum_at_time FROM source WHERE {{ .Timefilter }} GROUP BY ExporterName, InIfProvider ) GROUP BY ExporterName, InIfProvider ORDER BY MAX(sum_at_time) DESC LIMIT 20)
SELECT 1 AS axis, * FROM (
SELECT
 {{ call .ToStartOfInterval "TimeReceived" }} AS time,
 {{ .Units }}/{{ .Interval }} AS xps,
 if((ExporterName, InIfProvider) IN rows, [ExporterName, InIfProvider], ['Other', 'Other']) AS dimensions
FROM source
WHERE {{ .Timefilter }}
GROUP BY time, dimensions
ORDER BY time WITH FILL
 FROM {{ .TimefilterStart }}
 TO {{ .TimefilterEnd }} + INTERVAL 1 second
 STEP {{ .Interval }}
 INTERPOLATE (dimensions AS ['Other', 'Other']))`,
				},
			},
		}, {
			Description: "no filters, reverse",
			Pos:         helpers.Mark(),
			Input: graphLineHandlerInput{
				graphCommonHandlerInput: graphCommonHandlerInput{
					Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
					End:   time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
					Limit: 20,
					Dimensions: []query.Column{
						query.NewColumn("ExporterName"),
						query.NewColumn("InIfProvider"),
					},
					Filter: query.Filter{},
					Units:  "l3bps",
				},
				Points:        100,
				Bidirectional: true,
			},
			Expected: []templateQuery{
				{
					Context: inputContext{
						Start:  time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
						End:    time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
						Points: 100,
						Units:  "l3bps",
					},
					Template: `WITH
 source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1),
 rows AS (SELECT ExporterName, InIfProvider FROM source WHERE {{ .Timefilter }} GROUP BY ExporterName, InIfProvider ORDER BY {{ .Units }} DESC LIMIT 20)
SELECT 1 AS axis, * FROM (
SELECT
 {{ call .ToStartOfInterval "TimeReceived" }} AS time,
 {{ .Units }}/{{ .Interval }} AS xps,
 if((ExporterName, InIfProvider) IN rows, [ExporterName, InIfProvider], ['Other', 'Other']) AS dimensions
FROM source
WHERE {{ .Timefilter }}
GROUP BY time, dimensions
ORDER BY time WITH FILL
 FROM {{ .TimefilterStart }}
 TO {{ .TimefilterEnd }} + INTERVAL 1 second
 STEP {{ .Interval }}
 INTERPOLATE (dimensions AS ['Other', 'Other']))`,
				}, {
					Context: inputContext{
						Start:  time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
						End:    time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
						Points: 100,
						Units:  "l3bps",
					},
					Template: `SELECT 2 AS axis, * FROM (
SELECT
 {{ call .ToStartOfInterval "TimeReceived" }} AS time,
 {{ .Units }}/{{ .Interval }} AS xps,
 if((ExporterName, OutIfProvider) IN rows, [ExporterName, OutIfProvider], ['Other', 'Other']) AS dimensions
FROM source
WHERE {{ .Timefilter }}
GROUP BY time, dimensions
ORDER BY time WITH FILL
 FROM {{ .TimefilterStart }}
 TO {{ .TimefilterEnd }} + INTERVAL 1 second
 STEP {{ .Interval }}
 INTERPOLATE (dimensions AS ['Other', 'Other']))`,
				},
			},
		}, {
			Description: "no filters, previous period",
			Pos:         helpers.Mark(),
			Input: graphLineHandlerInput{
				graphCommonHandlerInput: graphCommonHandlerInput{
					Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
					End:   time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
					Limit: 20,
					Dimensions: []query.Column{
						query.NewColumn("ExporterName"),
						query.NewColumn("InIfProvider"),
					},
					Filter: query.Filter{},
					Units:  "l3bps",
				},
				Points:         100,
				PreviousPeriod: true,
			},
			Expected: []templateQuery{
				{
					Context: inputContext{
						Start:  time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
						End:    time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
						Points: 100,
						Units:  "l3bps",
					},
					Template: `WITH
 source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1),
 rows AS (SELECT ExporterName, InIfProvider FROM source WHERE {{ .Timefilter }} GROUP BY ExporterName, InIfProvider ORDER BY {{ .Units }} DESC LIMIT 20)
SELECT 1 AS axis, * FROM (
SELECT
 {{ call .ToStartOfInterval "TimeReceived" }} AS time,
 {{ .Units }}/{{ .Interval }} AS xps,
 if((ExporterName, InIfProvider) IN rows, [ExporterName, InIfProvider], ['Other', 'Other']) AS dimensions
FROM source
WHERE {{ .Timefilter }}
GROUP BY time, dimensions
ORDER BY time WITH FILL
 FROM {{ .TimefilterStart }}
 TO {{ .TimefilterEnd }} + INTERVAL 1 second
 STEP {{ .Interval }}
 INTERPOLATE (dimensions AS ['Other', 'Other']))`,
				}, {
					Context: inputContext{
						Start: time.Date(2022, 4, 9, 15, 45, 10, 0, time.UTC),
						End:   time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
						StartForTableSelection: func() *time.Time {
							t := time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC)
							return &t
						}(),
						Points: 100,
						Units:  "l3bps",
					},
					Template: `SELECT 3 AS axis, * FROM (
SELECT
 {{ call .ToStartOfInterval "TimeReceived" }} + INTERVAL 86400 second AS time,
 {{ .Units }}/{{ .Interval }} AS xps,
 emptyArrayString() AS dimensions
FROM source
WHERE {{ .Timefilter }}
GROUP BY time, dimensions
ORDER BY time WITH FILL
 FROM {{ .TimefilterStart }} + INTERVAL 86400 second
 TO {{ .TimefilterEnd }} + INTERVAL 1 second + INTERVAL 86400 second
 STEP {{ .Interval }}
 INTERPOLATE (dimensions AS emptyArrayString()))`,
				},
			},
		}, {
			Description: "previous period while main table is required",
			Pos:         helpers.Mark(),
			Input: graphLineHandlerInput{
				graphCommonHandlerInput: graphCommonHandlerInput{
					Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
					End:   time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
					Dimensions: []query.Column{
						query.NewColumn("SrcAddr"),
						query.NewColumn("DstAddr"),
					},
					Filter: query.NewFilter("InIfBoundary = external"),
					Units:  "l3bps",
				},
				Points:         100,
				PreviousPeriod: true,
			},
			Expected: []templateQuery{
				{
					Context: inputContext{
						Start:             time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
						End:               time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
						MainTableRequired: true,
						Points:            100,
						Units:             "l3bps",
					},
					Template: `WITH
 source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1),
 rows AS (SELECT SrcAddr, DstAddr FROM source WHERE {{ .Timefilter }} AND (InIfBoundary = 'external') GROUP BY SrcAddr, DstAddr ORDER BY {{ .Units }} DESC LIMIT 0)
SELECT 1 AS axis, * FROM (
SELECT
 {{ call .ToStartOfInterval "TimeReceived" }} AS time,
 {{ .Units }}/{{ .Interval }} AS xps,
 if((SrcAddr, DstAddr) IN rows, [replaceRegexpOne(IPv6NumToString(SrcAddr), '^::ffff:', ''), replaceRegexpOne(IPv6NumToString(DstAddr), '^::ffff:', '')], ['Other', 'Other']) AS dimensions
FROM source
WHERE {{ .Timefilter }} AND (InIfBoundary = 'external')
GROUP BY time, dimensions
ORDER BY time WITH FILL
 FROM {{ .TimefilterStart }}
 TO {{ .TimefilterEnd }} + INTERVAL 1 second
 STEP {{ .Interval }}
 INTERPOLATE (dimensions AS ['Other', 'Other']))`,
				}, {
					Context: inputContext{
						Start: time.Date(2022, 4, 9, 15, 45, 10, 0, time.UTC),
						End:   time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
						StartForTableSelection: func() *time.Time {
							t := time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC)
							return &t
						}(),
						MainTableRequired: true,
						Points:            100,
						Units:             "l3bps",
					},
					Template: `SELECT 3 AS axis, * FROM (
SELECT
 {{ call .ToStartOfInterval "TimeReceived" }} + INTERVAL 86400 second AS time,
 {{ .Units }}/{{ .Interval }} AS xps,
 emptyArrayString() AS dimensions
FROM source
WHERE {{ .Timefilter }} AND (InIfBoundary = 'external')
GROUP BY time, dimensions
ORDER BY time WITH FILL
 FROM {{ .TimefilterStart }} + INTERVAL 86400 second
 TO {{ .TimefilterEnd }} + INTERVAL 1 second + INTERVAL 86400 second
 STEP {{ .Interval }}
 INTERPOLATE (dimensions AS emptyArrayString()))`,
				},
			},
		},
	}
	for _, tc := range cases {
		tc.Input.schema = schema.NewMock(t)
		if err := query.Columns(tc.Input.Dimensions).Validate(tc.Input.schema); err != nil {
			t.Fatalf("%sValidate() error:\n%+v", tc.Pos, err)
		}
		if err := tc.Input.Filter.Validate(tc.Input.schema); err != nil {
			t.Fatalf("%sValidate() error:\n%+v", tc.Pos, err)
		}
		t.Run(tc.Description, func(t *testing.T) {
			got := tc.Input.toSQL()
			if diff := helpers.Diff(got, tc.Expected); diff != "" {
				t.Errorf("%stoSQL (-got, +want):\n%s", tc.Pos, diff)
			}
		})
	}
}

func TestGraphLineHandler(t *testing.T) {
	_, h, mockConn, _ := NewMock(t, DefaultConfiguration())
	base := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)

	t.Run("sort by avg", func(t *testing.T) {
		// Single direction
		expectedSQL := []struct {
			Axis       uint8     `ch:"axis"`
			Time       time.Time `ch:"time"`
			Xps        float64   `ch:"xps"`
			Dimensions []string  `ch:"dimensions"`
		}{
			{1, base, 1000, []string{"router1", "provider1"}},
			{1, base, 2000, []string{"router1", "provider2"}},
			{1, base, 1200, []string{"router2", "provider2"}},
			{1, base, 1100, []string{"router2", "provider3"}},
			{1, base, 1900, []string{"Other", "Other"}},
			{1, base.Add(time.Minute), 500, []string{"router1", "provider1"}},
			{1, base.Add(time.Minute), 5000, []string{"router1", "provider2"}},
			{1, base.Add(time.Minute), 900, []string{"router2", "provider4"}},
			{1, base.Add(time.Minute), 100, []string{"Other", "Other"}},
			{1, base.Add(2 * time.Minute), 100, []string{"router1", "provider1"}},
			{1, base.Add(2 * time.Minute), 3000, []string{"router1", "provider2"}},
			{1, base.Add(2 * time.Minute), 100, []string{"router2", "provider4"}},
			{1, base.Add(2 * time.Minute), 100, []string{"Other", "Other"}},
		}
		mockConn.EXPECT().
			Select(gomock.Any(), gomock.Any(), gomock.Any()).
			SetArg(1, expectedSQL).
			Return(nil)

		// Bidirectional
		expectedSQL = []struct {
			Axis       uint8     `ch:"axis"`
			Time       time.Time `ch:"time"`
			Xps        float64   `ch:"xps"`
			Dimensions []string  `ch:"dimensions"`
		}{
			{1, base, 1000, []string{"router1", "provider1"}},
			{1, base, 2000, []string{"router1", "provider2"}},
			{1, base, 1200, []string{"router2", "provider2"}},
			{1, base, 1100, []string{"router2", "provider3"}},
			{1, base, 1900, []string{"Other", "Other"}},
			{1, base.Add(time.Minute), 500, []string{"router1", "provider1"}},
			{1, base.Add(time.Minute), 5000, []string{"router1", "provider2"}},
			{1, base.Add(time.Minute), 900, []string{"router2", "provider4"}},

			// Axes can be mixed. In reality, it seems they cannot
			// be interleaved, but ClickHouse documentation does
			// not say it is not possible.
			{2, base, 101, []string{"router1", "provider1"}},
			{2, base, 200, []string{"router1", "provider2"}},
			{2, base, 120, []string{"router2", "provider2"}},

			{1, base.Add(time.Minute), 100, []string{"Other", "Other"}},
			{1, base.Add(2 * time.Minute), 100, []string{"router1", "provider1"}},

			{2, base, 111, []string{"router2", "provider3"}},
			{2, base, 190, []string{"Other", "Other"}},
			{2, base.Add(time.Minute), 50, []string{"router1", "provider1"}},
			{2, base.Add(time.Minute), 500, []string{"router1", "provider2"}},

			{1, base.Add(2 * time.Minute), 3000, []string{"router1", "provider2"}},
			{1, base.Add(2 * time.Minute), 100, []string{"router2", "provider4"}},
			{1, base.Add(2 * time.Minute), 100, []string{"Other", "Other"}},

			{2, base.Add(time.Minute), 90, []string{"router2", "provider4"}},
			{2, base.Add(time.Minute), 10, []string{"Other", "Other"}},
			{2, base.Add(2 * time.Minute), 10, []string{"router1", "provider1"}},
			{2, base.Add(2 * time.Minute), 300, []string{"router1", "provider2"}},
			{2, base.Add(2 * time.Minute), 10, []string{"router2", "provider4"}},
			{2, base.Add(2 * time.Minute), 10, []string{"Other", "Other"}},
		}
		mockConn.EXPECT().
			Select(gomock.Any(), gomock.Any(), gomock.Any()).
			SetArg(1, expectedSQL).
			Return(nil)

		// Previous period
		expectedSQL = []struct {
			Axis       uint8     `ch:"axis"`
			Time       time.Time `ch:"time"`
			Xps        float64   `ch:"xps"`
			Dimensions []string  `ch:"dimensions"`
		}{
			{1, base, 1000, []string{"router1", "provider1"}},
			{1, base, 2000, []string{"router1", "provider2"}},
			{1, base, 1200, []string{"router2", "provider2"}},
			{1, base, 1100, []string{"router2", "provider3"}},
			{1, base, 1900, []string{"Other", "Other"}},
			{1, base.Add(time.Minute), 500, []string{"router1", "provider1"}},
			{1, base.Add(time.Minute), 5000, []string{"router1", "provider2"}},
			{1, base.Add(time.Minute), 900, []string{"router2", "provider4"}},
			{1, base.Add(time.Minute), 100, []string{"Other", "Other"}},
			{1, base.Add(2 * time.Minute), 100, []string{"router1", "provider1"}},
			{1, base.Add(2 * time.Minute), 3000, []string{"router1", "provider2"}},
			{1, base.Add(2 * time.Minute), 100, []string{"router2", "provider4"}},
			{1, base.Add(2 * time.Minute), 100, []string{"Other", "Other"}},

			{3, base, 8000, []string{}},
			{3, base.Add(time.Minute), 6000, []string{}},
			{3, base.Add(2 * time.Minute), 4500, []string{}},
		}
		mockConn.EXPECT().
			Select(gomock.Any(), gomock.Any(), gomock.Any()).
			SetArg(1, expectedSQL).
			Return(nil)

		helpers.TestHTTPEndpoints(t, h.LocalAddr(), helpers.HTTPEndpointCases{
			{
				Description: "single direction",
				URL:         "/api/v0/console/graph/line",
				JSONInput: gin.H{
					"start":         time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
					"end":           time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
					"points":        100,
					"limit":         20,
					"limitType":     "avg",
					"dimensions":    []string{"ExporterName", "InIfProvider"},
					"filter":        "DstCountry = 'FR' AND SrcCountry = 'US'",
					"units":         "l3bps",
					"bidirectional": false,
				},
				JSONOutput: gin.H{
					// Sorted by sum of bps
					"rows": [][]string{
						{"router1", "provider2"}, // 10000
						{"router1", "provider1"}, // 1600
						{"router2", "provider2"}, // 1200
						{"router2", "provider3"}, // 1100
						{"router2", "provider4"}, // 1000
						{"Other", "Other"},       // 2100
					},
					"t": []string{
						"2009-11-10T23:00:00Z",
						"2009-11-10T23:01:00Z",
						"2009-11-10T23:02:00Z",
					},
					"points": [][]int{
						{2000, 5000, 3000},
						{1000, 500, 100},
						{1200, 0, 0},
						{1100, 0, 0},
						{0, 900, 100},
						{1900, 100, 100},
					},
					"min": []int{
						2000,
						100,
						1200,
						1100,
						100,
						100,
					},
					"max": []int{
						5000,
						1000,
						1200,
						1100,
						900,
						1900,
					},
					"last": []int{
						5000,
						500,
						0,
						0,
						900,
						100,
					},
					"average": []int{
						3333,
						533,
						400,
						366,
						333,
						700,
					},
					"95th": []int{
						4800,
						950,
						1080,
						990,
						820,
						1720,
					},
					"axis": []int{
						1, 1, 1, 1, 1, 1,
					},
					"axis-names": map[int]string{
						1: "Direct",
					},
				},
			}, {
				Description: "bidirectional",
				URL:         "/api/v0/console/graph/line",
				JSONInput: gin.H{
					"start":         time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
					"end":           time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
					"points":        100,
					"limit":         20,
					"limitType":     "avg",
					"dimensions":    []string{"ExporterName", "InIfProvider"},
					"filter":        "DstCountry = 'FR' AND SrcCountry = 'US'",
					"units":         "l3bps",
					"bidirectional": true,
				},
				JSONOutput: gin.H{
					// Sorted by sum of bps
					"rows": [][]string{
						{"router1", "provider2"}, // 10000
						{"router1", "provider1"}, // 1600
						{"router2", "provider2"}, // 1200
						{"router2", "provider3"}, // 1100
						{"router2", "provider4"}, // 1000
						{"Other", "Other"},       // 2100

						{"router1", "provider2"}, // 1000
						{"router1", "provider1"}, // 160
						{"router2", "provider2"}, // 120
						{"router2", "provider3"}, // 110
						{"router2", "provider4"}, // 100
						{"Other", "Other"},       // 210
					},
					"t": []string{
						"2009-11-10T23:00:00Z",
						"2009-11-10T23:01:00Z",
						"2009-11-10T23:02:00Z",
					},
					"points": [][]int{
						{2000, 5000, 3000},
						{1000, 500, 100},
						{1200, 0, 0},
						{1100, 0, 0},
						{0, 900, 100},
						{1900, 100, 100},

						{200, 500, 300},
						{101, 50, 10},
						{120, 0, 0},
						{111, 0, 0},
						{0, 90, 10},
						{190, 10, 10},
					},
					"min": []int{
						2000,
						100,
						1200,
						1100,
						100,
						100,

						200,
						10,
						120,
						111,
						10,
						10,
					},
					"max": []int{
						5000,
						1000,
						1200,
						1100,
						900,
						1900,

						500,
						101,
						120,
						111,
						90,
						190,
					},
					"last": []int{
						5000,
						500,
						0,
						0,
						900,
						100,

						500,
						50,
						0,
						0,
						90,
						10,
					},
					"average": []int{
						3333,
						533,
						400,
						366,
						333,
						700,

						333,
						53,
						40,
						37,
						33,
						70,
					},
					"95th": []int{
						4800,
						950,
						1080,
						990,
						820,
						1720,

						480,
						96,
						108,
						100,
						82,
						172,
					},
					"axis": []int{
						1, 1, 1, 1, 1, 1,
						2, 2, 2, 2, 2, 2,
					},
					"axis-names": map[int]string{
						1: "Direct",
						2: "Reverse",
					},
				},
			}, {
				Description: "previous period",
				URL:         "/api/v0/console/graph/line",
				JSONInput: gin.H{
					"start":           time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
					"end":             time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
					"points":          100,
					"limit":           20,
					"limitType":       "avg",
					"dimensions":      []string{"ExporterName", "InIfProvider"},
					"filter":          "DstCountry = 'FR' AND SrcCountry = 'US'",
					"units":           "l3bps",
					"bidirectional":   false,
					"previous-period": true,
				},
				JSONOutput: gin.H{
					// Sorted by sum of bps
					"rows": [][]string{
						{"router1", "provider2"}, // 10000
						{"router1", "provider1"}, // 1600
						{"router2", "provider2"}, // 1200
						{"router2", "provider3"}, // 1100
						{"router2", "provider4"}, // 1000
						{"Other", "Other"},       // 2100
						{"Other", "Other"},       // Previous day
					},
					"t": []string{
						"2009-11-10T23:00:00Z",
						"2009-11-10T23:01:00Z",
						"2009-11-10T23:02:00Z",
					},
					"points": [][]int{
						{2000, 5000, 3000},
						{1000, 500, 100},
						{1200, 0, 0},
						{1100, 0, 0},
						{0, 900, 100},
						{1900, 100, 100},
						{8000, 6000, 4500},
					},
					"min": []int{
						2000,
						100,
						1200,
						1100,
						100,
						100,
						4500,
					},
					"max": []int{
						5000,
						1000,
						1200,
						1100,
						900,
						1900,
						8000,
					},
					"last": []int{
						5000,
						500,
						0,
						0,
						900,
						100,
						6000,
					},
					"average": []int{
						3333,
						533,
						400,
						366,
						333,
						700,
						6166,
					},
					"95th": []int{
						4800,
						950,
						1080,
						990,
						820,
						1720,
						7800,
					},
					"axis": []int{
						1, 1, 1, 1, 1, 1,
						3,
					},
					"axis-names": map[int]string{
						1: "Direct",
						3: "Previous day",
					},
				},
			},
		})
	})

	t.Run("sort by max", func(t *testing.T) {
		// Single direction
		expectedSQL := []struct {
			Axis       uint8     `ch:"axis"`
			Time       time.Time `ch:"time"`
			Xps        float64   `ch:"xps"`
			Dimensions []string  `ch:"dimensions"`
		}{
			{1, base, 1000, []string{"router1", "provider1"}},
			{1, base, 2000, []string{"router1", "provider2"}},
			{1, base, 1200, []string{"router2", "provider2"}},
			{1, base, 1100, []string{"router2", "provider3"}},
			{1, base, 1900, []string{"Other", "Other"}},
			{1, base.Add(time.Minute), 500, []string{"router1", "provider1"}},
			{1, base.Add(time.Minute), 5000, []string{"router1", "provider2"}},
			{1, base.Add(time.Minute), 900, []string{"router2", "provider4"}},
			{1, base.Add(time.Minute), 100, []string{"Other", "Other"}},
			{1, base.Add(2 * time.Minute), 100, []string{"router1", "provider1"}},
			{1, base.Add(2 * time.Minute), 3000, []string{"router1", "provider2"}},
			{1, base.Add(2 * time.Minute), 100, []string{"router2", "provider4"}},
			{1, base.Add(2 * time.Minute), 100, []string{"Other", "Other"}},
		}
		mockConn.EXPECT().
			Select(gomock.Any(), gomock.Any(), gomock.Any()).
			SetArg(1, expectedSQL).
			Return(nil)

		// Bidirectional
		expectedSQL = []struct {
			Axis       uint8     `ch:"axis"`
			Time       time.Time `ch:"time"`
			Xps        float64   `ch:"xps"`
			Dimensions []string  `ch:"dimensions"`
		}{
			{1, base, 1000, []string{"router1", "provider1"}},
			{1, base, 2000, []string{"router1", "provider2"}},
			{1, base, 1200, []string{"router2", "provider2"}},
			{1, base, 1100, []string{"router2", "provider3"}},
			{1, base, 1900, []string{"Other", "Other"}},
			{1, base.Add(time.Minute), 500, []string{"router1", "provider1"}},
			{1, base.Add(time.Minute), 5000, []string{"router1", "provider2"}},
			{1, base.Add(time.Minute), 900, []string{"router2", "provider4"}},

			// Axes can be mixed. In reality, it seems they cannot
			// be interleaved, but ClickHouse documentation does
			// not say it is not possible.
			{2, base, 101, []string{"router1", "provider1"}},
			{2, base, 200, []string{"router1", "provider2"}},
			{2, base, 120, []string{"router2", "provider2"}},

			{1, base.Add(time.Minute), 100, []string{"Other", "Other"}},
			{1, base.Add(2 * time.Minute), 100, []string{"router1", "provider1"}},

			{2, base, 111, []string{"router2", "provider3"}},
			{2, base, 190, []string{"Other", "Other"}},
			{2, base.Add(time.Minute), 50, []string{"router1", "provider1"}},
			{2, base.Add(time.Minute), 500, []string{"router1", "provider2"}},

			{1, base.Add(2 * time.Minute), 3000, []string{"router1", "provider2"}},
			{1, base.Add(2 * time.Minute), 100, []string{"router2", "provider4"}},
			{1, base.Add(2 * time.Minute), 100, []string{"Other", "Other"}},

			{2, base.Add(time.Minute), 90, []string{"router2", "provider4"}},
			{2, base.Add(time.Minute), 10, []string{"Other", "Other"}},
			{2, base.Add(2 * time.Minute), 10, []string{"router1", "provider1"}},
			{2, base.Add(2 * time.Minute), 300, []string{"router1", "provider2"}},
			{2, base.Add(2 * time.Minute), 10, []string{"router2", "provider4"}},
			{2, base.Add(2 * time.Minute), 10, []string{"Other", "Other"}},
		}
		mockConn.EXPECT().
			Select(gomock.Any(), gomock.Any(), gomock.Any()).
			SetArg(1, expectedSQL).
			Return(nil)

		// Previous period
		expectedSQL = []struct {
			Axis       uint8     `ch:"axis"`
			Time       time.Time `ch:"time"`
			Xps        float64   `ch:"xps"`
			Dimensions []string  `ch:"dimensions"`
		}{
			{1, base, 1000, []string{"router1", "provider1"}},
			{1, base, 2000, []string{"router1", "provider2"}},
			{1, base, 1200, []string{"router2", "provider2"}},
			{1, base, 1100, []string{"router2", "provider3"}},
			{1, base, 1900, []string{"Other", "Other"}},
			{1, base.Add(time.Minute), 500, []string{"router1", "provider1"}},
			{1, base.Add(time.Minute), 5000, []string{"router1", "provider2"}},
			{1, base.Add(time.Minute), 900, []string{"router2", "provider4"}},
			{1, base.Add(time.Minute), 100, []string{"Other", "Other"}},
			{1, base.Add(2 * time.Minute), 100, []string{"router1", "provider1"}},
			{1, base.Add(2 * time.Minute), 3000, []string{"router1", "provider2"}},
			{1, base.Add(2 * time.Minute), 100, []string{"router2", "provider4"}},
			{1, base.Add(2 * time.Minute), 100, []string{"Other", "Other"}},

			{3, base, 8000, []string{}},
			{3, base.Add(time.Minute), 6000, []string{}},
			{3, base.Add(2 * time.Minute), 4500, []string{}},
		}
		mockConn.EXPECT().
			Select(gomock.Any(), gomock.Any(), gomock.Any()).
			SetArg(1, expectedSQL).
			Return(nil)

		helpers.TestHTTPEndpoints(t, h.LocalAddr(), helpers.HTTPEndpointCases{
			{
				Description: "single direction",
				URL:         "/api/v0/console/graph/line",
				JSONInput: gin.H{
					"start":         time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
					"end":           time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
					"points":        100,
					"limit":         20,
					"limitType":     "max",
					"dimensions":    []string{"ExporterName", "InIfProvider"},
					"filter":        "DstCountry = 'FR' AND SrcCountry = 'US'",
					"units":         "l3bps",
					"bidirectional": false,
				},
				JSONOutput: gin.H{
					// Sorted by max of bps
					"rows": [][]string{
						{"router1", "provider2"},
						{"router2", "provider2"},
						{"router2", "provider3"},
						{"router1", "provider1"},
						{"router2", "provider4"},
						{"Other", "Other"},
					},
					"t": []string{
						"2009-11-10T23:00:00Z",
						"2009-11-10T23:01:00Z",
						"2009-11-10T23:02:00Z",
					},
					"points": [][]int{
						{2000, 5000, 3000},
						{1200, 0, 0},
						{1100, 0, 0},
						{1000, 500, 100},
						{0, 900, 100},
						{1900, 100, 100},
					},
					"min": []int{
						2000,
						1200,
						1100,
						100,
						100,
						100,
					},
					"max": []int{
						5000,
						1200,
						1100,
						1000,
						900,
						1900,
					},
					"last": []int{
						5000,
						0,
						0,
						500,
						900,
						100,
					},
					"average": []int{
						3333,
						400,
						366,
						533,
						333,
						700,
					},
					"95th": []int{
						4800,
						1080,
						990,
						950,
						820,
						1720,
					},
					"axis": []int{
						1, 1, 1, 1, 1, 1,
					},
					"axis-names": map[int]string{
						1: "Direct",
					},
				},
			}, {
				Description: "bidirectional",
				URL:         "/api/v0/console/graph/line",
				JSONInput: gin.H{
					"start":         time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
					"end":           time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
					"points":        100,
					"limit":         20,
					"limitType":     "max",
					"dimensions":    []string{"ExporterName", "InIfProvider"},
					"filter":        "DstCountry = 'FR' AND SrcCountry = 'US'",
					"units":         "l3bps",
					"bidirectional": true,
				},
				JSONOutput: gin.H{
					// Sorted by sum of bps
					"rows": [][]string{
						{"router1", "provider2"}, // 10000
						{"router2", "provider2"}, // 1200
						{"router2", "provider3"}, // 1100
						{"router1", "provider1"}, // 1600
						{"router2", "provider4"}, // 1000
						{"Other", "Other"},       // 2100

						{"router1", "provider2"}, // 1000
						{"router2", "provider2"}, // 120
						{"router2", "provider3"}, // 110
						{"router1", "provider1"}, // 160
						{"router2", "provider4"}, // 100
						{"Other", "Other"},       // 210
					},
					"t": []string{
						"2009-11-10T23:00:00Z",
						"2009-11-10T23:01:00Z",
						"2009-11-10T23:02:00Z",
					},
					"points": [][]int{
						{2000, 5000, 3000},
						{1200, 0, 0},
						{1100, 0, 0},
						{1000, 500, 100},
						{0, 900, 100},
						{1900, 100, 100},

						{200, 500, 300},
						{120, 0, 0},
						{111, 0, 0},
						{101, 50, 10},
						{0, 90, 10},
						{190, 10, 10},
					},
					"min": []int{
						2000,
						1200,
						1100,
						100,
						100,
						100,

						200,
						120,
						111,
						10,
						10,
						10,
					},
					"max": []int{
						5000,
						1200,
						1100,
						1000,
						900,
						1900,

						500,
						120,
						111,
						101,
						90,
						190,
					},
					"last": []int{
						5000,
						0,
						0,
						500,
						900,
						100,

						500,
						0,
						0,
						50,
						90,
						10,
					},
					"average": []int{
						3333,
						400,
						366,
						533,
						333,
						700,

						333,
						40,
						37,
						53,
						33,
						70,
					},
					"95th": []int{
						4800,
						1080,
						990,
						950,
						820,
						1720,

						480,
						108,
						100,
						96,
						82,
						172,
					},
					"axis": []int{
						1, 1, 1, 1, 1, 1,
						2, 2, 2, 2, 2, 2,
					},
					"axis-names": map[int]string{
						1: "Direct",
						2: "Reverse",
					},
				},
			}, {
				Description: "previous period",
				URL:         "/api/v0/console/graph/line",
				JSONInput: gin.H{
					"start":           time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
					"end":             time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
					"points":          100,
					"limit":           20,
					"limitType":       "max",
					"dimensions":      []string{"ExporterName", "InIfProvider"},
					"filter":          "DstCountry = 'FR' AND SrcCountry = 'US'",
					"units":           "l3bps",
					"bidirectional":   false,
					"previous-period": true,
				},
				JSONOutput: gin.H{
					// Sorted by sum of bps
					"rows": [][]string{
						{"router1", "provider2"}, // 10000
						{"router2", "provider2"}, // 1200
						{"router2", "provider3"}, // 1100
						{"router1", "provider1"}, // 1600
						{"router2", "provider4"}, // 1000
						{"Other", "Other"},       // 2100
						{"Other", "Other"},       // Previous day
					},
					"t": []string{
						"2009-11-10T23:00:00Z",
						"2009-11-10T23:01:00Z",
						"2009-11-10T23:02:00Z",
					},
					"points": [][]int{
						{2000, 5000, 3000},
						{1200, 0, 0},
						{1100, 0, 0},
						{1000, 500, 100},
						{0, 900, 100},
						{1900, 100, 100},
						{8000, 6000, 4500},
					},
					"min": []int{
						2000,
						1200,
						1100,
						100,
						100,
						100,
						4500,
					},
					"max": []int{
						5000,
						1200,
						1100,
						1000,
						900,
						1900,
						8000,
					},
					"last": []int{
						5000,
						0,
						0,
						500,
						900,
						100,
						6000,
					},
					"average": []int{
						3333,
						400,
						366,
						533,
						333,
						700,
						6166,
					},
					"95th": []int{
						4800,
						1080,
						990,
						950,
						820,
						1720,
						7800,
					},
					"axis": []int{
						1, 1, 1, 1, 1, 1,
						3,
					},
					"axis-names": map[int]string{
						1: "Direct",
						3: "Previous day",
					},
				},
			},
		})
	})
}

func TestGetTableInterval(t *testing.T) {
	_, h, _, mockClock := NewMock(t, DefaultConfiguration())
	mockClock.Set(time.Date(2022, 4, 12, 15, 45, 10, 0, time.UTC))
	helpers.TestHTTPEndpoints(t, h.LocalAddr(), helpers.HTTPEndpointCases{
		{
			Description: "simple query",
			URL:         "/api/v0/console/graph/table-interval",
			JSONInput: gin.H{
				"start":  time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
				"end":    time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
				"points": 300,
			},
			JSONOutput: gin.H{
				"table":    "flows",
				"interval": 1,
			},
		}, {
			Description: "too many points",
			URL:         "/api/v0/console/graph/table-interval",
			JSONInput: gin.H{
				"start":  time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC),
				"end":    time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC),
				"points": 86400,
			},
			StatusCode: 400,
			JSONOutput: gin.H{
				"message": `Key: 'tableIntervalInput.Points' Error:Field validation for 'Points' failed on the 'max' tag`,
			},
		},
	})
}
