// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
	"net/url"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"golang.org/x/mod/semver"
	"golang.org/x/telemetry/internal/config"
	"golang.org/x/telemetry/internal/telemetry"
)

var exampleReports = []telemetry.Report{
	{
		Week:     "2999-01-01",
		LastWeek: "2998-01-01",
		X:        0.1,
		Programs: []*telemetry.ProgramReport{
			{
				Program:   "cmd/go",
				Version:   "go1.2.3",
				GoVersion: "go1.2.3",
				GOOS:      "darwin",
				GOARCH:    "arm64",
				Counters: map[string]int64{
					"main": 1,
				},
			},
			{
				Program:   "example.com/mod/pkg",
				Version:   "v2.3.4",
				GoVersion: "go1.2.3",
				GOOS:      "darwin",
				GOARCH:    "arm64",
				Counters: map[string]int64{
					"main":   1,
					"flag:a": 2,
					"flag:b": 3,
				},
				// TODO: add support for stacks
				Stacks: map[string]int64{
					"panic": 4,
				},
			},
			{
				Program:   "example.com/mod/pkg",
				Version:   "v2.3.4-pre.1",
				GoVersion: "go1.2.3",
				GOOS:      "darwin",
				GOARCH:    "arm64",
				Counters: map[string]int64{
					"flag:b": 3,
				},
				// TODO: add support for stacks
				Stacks: map[string]int64{
					"panic": 2,
				},
			},
		},
		Config: "v0.0.1",
	},
	{
		Week:     "2999-01-01",
		LastWeek: "2998-01-01",
		X:        0.2,
		Programs: []*telemetry.ProgramReport{
			{
				Program:   "example.com/mod/pkg",
				Version:   "v1.2.3",
				GoVersion: "go1.2.3",
				GOOS:      "darwin",
				GOARCH:    "arm64",
				Counters: map[string]int64{
					"main":   1,
					"flag:a": 2,
					"flag:b": 3,
				},
				// TODO: add support for stacks
				Stacks: map[string]int64{
					"panic": 4,
				},
			},
			{
				Program:   "example.com/mod/pkg",
				Version:   "v2.3.4",
				GoVersion: "go1.19.0",
				GOOS:      "darwin",
				GOARCH:    "arm64",
				Counters: map[string]int64{
					"main":   1,
					"flag:a": 2,
					"flag:b": 3,
				},
				// TODO: add support for stacks
				Stacks: map[string]int64{
					"panic": 4,
				},
			},
		},
		Config: "v0.0.1",
	},
	{
		Week:     "2999-01-01",
		LastWeek: "2998-01-01",
		X:        0.3,
		Programs: []*telemetry.ProgramReport{
			{
				Program:   "example.com/mod/pkg",
				Version:   "v1.2.3",
				GoVersion: "go1.2.3",
				GOOS:      "linux",
				GOARCH:    "amd64",
				Counters: map[string]int64{
					"main":   4,
					"flag:a": 5,
					"flag:b": 6,
					"flag:c": 1,
				},
				// TODO: add support for stacks
				Stacks: map[string]int64{
					"panic": 7,
				},
			},
		},
		Config: "v0.0.1",
	},
}

func TestGroup(t *testing.T) {
	type args struct {
		reports []telemetry.Report
	}
	tests := []struct {
		name string
		args args
		want data
	}{
		{
			name: "single report",
			args: args{
				[]telemetry.Report{
					{
						Week:     "2999-01-01",
						LastWeek: "2998-01-01",
						X:        0.123456789,
						Programs: []*telemetry.ProgramReport{
							{
								Program:   "example.com/mod/pkg",
								Version:   "v1.2.3",
								GoVersion: "go1.2.3",
								GOOS:      "darwin",
								GOARCH:    "arm64",
								Counters: map[string]int64{
									"main":   1,
									"flag:a": 2,
									"flag:b": 3,
								},
								// TODO: add support for stacks
								Stacks: map[string]int64{
									"panic": 4,
								},
							},
						},
						Config: "v0.0.1",
					},
				},
			},
			want: data{
				weekName("2999-01-01"): {
					programName("example.com/mod/pkg"): {
						graphName("Version"): {
							bucketName("v1.2.3"): {
								reportID(0.1234567890): 1,
							},
						},
						graphName("GOOS"): {
							bucketName("darwin"): {
								reportID(0.1234567890): 1,
							},
						},
						graphName("GOARCH"): {
							bucketName("arm64"): {
								reportID(0.1234567890): 1,
							},
						},
						graphName("GoVersion"): {
							bucketName("go1.2.3"): {
								reportID(0.1234567890): 1,
							},
						},
						graphName("main"): {
							bucketName("main"): {
								reportID(0.1234567890): 1,
							},
						},
						graphName("flag"): {
							bucketName("a"): {
								reportID(0.1234567890): 2,
							},
							bucketName("b"): {
								reportID(0.1234567890): 3,
							},
						},
					},
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := group(tt.args.reports)
			if diff := cmp.Diff(tt.want, got); diff != "" {
				t.Errorf("nest() mismatch (-want +got):\n%s", diff)
			}
		})
	}
}

func TestPartition(t *testing.T) {
	normalVersion := func(b bucketName) bucketName {
		return bucketName(semver.MajorMinor(string(b)))
	}
	normalGoVersion := func(b bucketName) bucketName {
		return bucketName(goMajorMinor(string(b)))
	}
	exampleData := group(exampleReports)
	type args struct {
		program programName
		name    graphName
		buckets []bucketName
	}
	tests := []struct {
		name string
		data data
		args args
		opts partitionOptions
		want *chart
	}{
		{
			name: "major.minor.patch version counter",
			data: exampleData,
			args: args{
				program: "example.com/mod/pkg",
				name:    "Version",
				buckets: []bucketName{"v1.2.3", "v2.3.4", "v4.5.6"},
			},
			opts: partitionOptions{normalizeBucket: normalVersion},
			want: &chart{
				ID:   "charts:example.com/mod/pkg:Version",
				Name: "Version",
				Type: "partition",
				Data: []*datum{
					{Week: "2999-01-01", Key: "v1.2", Value: 2},
					{Week: "2999-01-01", Key: "v2.3", Value: 2},
					{Week: "2999-01-01", Key: "v4.5", Value: 0},
				},
			},
		},
		{
			name: "all nonempty versions",
			data: exampleData,
			args: args{
				program: "example.com/mod/pkg",
				name:    "Version",
				buckets: []bucketName{"v1.2.3", "v2.3.4", "v4.5.6"},
			},
			opts: partitionOptions{ignoreEmptyBuckets: true},
			want: &chart{
				ID:   "charts:example.com/mod/pkg:Version",
				Name: "Version",
				Type: "partition",
				Data: []*datum{
					{
						Week:  "2999-01-01",
						Key:   "v1.2.3",
						Value: 2,
					},
					{
						Week:  "2999-01-01",
						Key:   "v2.3.4",
						Value: 2,
					},
				},
			},
		},
		{
			name: "major.minor version counter should have same result as major.minor.patch",
			data: exampleData,
			args: args{
				program: "example.com/mod/pkg",
				name:    "Version",
				buckets: []bucketName{"v1.2.3", "v2.3.4"},
			},
			opts: partitionOptions{normalizeBucket: normalVersion},
			want: &chart{
				ID:   "charts:example.com/mod/pkg:Version",
				Name: "Version",
				Type: "partition",
				Data: []*datum{
					{
						Week:  "2999-01-01",
						Key:   "v1.2",
						Value: 2,
					},
					{
						Week:  "2999-01-01",
						Key:   "v2.3",
						Value: 2,
					},
				},
			},
		},
		{
			name: "duplicated counter should be ignored",
			data: exampleData,
			args: args{
				program: "example.com/mod/pkg",
				name:    "Version",
				buckets: []bucketName{"v1.2.3", "v2.3.4", "v1.2.3"},
			},
			opts: partitionOptions{normalizeBucket: normalVersion},
			want: &chart{
				ID:   "charts:example.com/mod/pkg:Version",
				Name: "Version",
				Type: "partition",
				Data: []*datum{
					{
						Week:  "2999-01-01",
						Key:   "v1.2",
						Value: 2,
					},
					{
						Week:  "2999-01-01",
						Key:   "v2.3",
						Value: 2,
					},
				},
			},
		},
		{
			name: "goos counter",
			data: exampleData,
			args: args{
				program: "example.com/mod/pkg",
				name:    "GOOS",
				buckets: []bucketName{"darwin", "linux"},
			},
			want: &chart{
				ID:   "charts:example.com/mod/pkg:GOOS",
				Name: "GOOS",
				Type: "partition",
				Data: []*datum{
					{
						Week:  "2999-01-01",
						Key:   "darwin",
						Value: 2,
					},
					{
						Week:  "2999-01-01",
						Key:   "linux",
						Value: 1,
					},
				},
			},
		},
		{
			name: "GoVersion counter",
			data: exampleData,
			args: args{
				program: "example.com/mod/pkg",
				name:    "GoVersion",
				buckets: []bucketName{"go1.2.3", "go2.3.4"},
			},
			opts: partitionOptions{normalizeBucket: normalGoVersion},
			want: &chart{
				ID:   "charts:example.com/mod/pkg:GoVersion",
				Name: "GoVersion",
				Type: "partition",
				Data: []*datum{
					{
						Week:  "2999-01-01",
						Key:   "go1.2",
						Value: 3,
					},
					{
						Week:  "2999-01-01",
						Key:   "go2.3",
						Value: 0,
					},
				},
			},
		},
		{
			name: "three days, multiple versions",
			data: data{
				"2999-01-01": {"example.com/mod/pkg": {"Version": {
					"v1.2.3": {0.1: 2},
					"v2.3.4": {0.1: 3},
				},
				}},
				"2999-01-04": {"example.com/mod/pkg": {"Version": {
					"v1.2.3": {0.3: 2},
					"v2.3.4": {0.4: 5},
				},
				}},
				"2999-01-05": {"example.com/mod/pkg": {"Version": {
					"v2.3.4": {0.5: 6},
				}}},
			},
			args: args{
				program: "example.com/mod/pkg",
				name:    "Version",
				buckets: []bucketName{"v1.2.3", "v2.3.4"},
			},
			opts: partitionOptions{normalizeBucket: normalVersion},
			want: &chart{
				ID:   "charts:example.com/mod/pkg:Version",
				Name: "Version",
				Type: "partition",
				Data: []*datum{
					{
						Week:  "2999-01-05",
						Key:   "v1.2",
						Value: 2,
					},
					{
						Week:  "2999-01-05",
						Key:   "v2.3",
						Value: 3,
					},
				},
			},
		},
		{
			name: "three days, multiple GOOS",
			data: data{
				"2999-01-01": {"example.com/mod/pkg": {"GOOS": {
					"darwin": {0.1: 2, 0.2: 2, 0.3: 2},
					"linux":  {0.1: 2, 0.2: 2},
				},
				}},
				"2999-01-02": {"example.com/mod/pkg": {"GOOS": {
					"darwin": {0.4: 2, 0.5: 2},
					"linux":  {0.6: 5},
				},
				}},
				"2999-01-03": {"example.com/mod/pkg": {"GOOS": {
					"darwin": {0.6: 3},
				},
				}},
			},
			args: args{
				program: "example.com/mod/pkg",
				name:    "GOOS",
				buckets: []bucketName{"darwin", "linux"},
			},
			want: &chart{
				ID:   "charts:example.com/mod/pkg:GOOS",
				Name: "GOOS",
				Type: "partition",
				Data: []*datum{
					{
						Week:  "2999-01-03",
						Key:   "darwin",
						Value: 6,
					},
					{
						Week:  "2999-01-03",
						Key:   "linux",
						Value: 3,
					},
				},
			},
		},
		{
			name: "two days data, missing GOOS in first day",
			data: data{
				"2999-01-01": {"example.com/mod/pkg": {"Version": {
					"v1.2": {0.1: 2},
				},
				}},
				"2999-01-02": {"example.com/mod/pkg": {"GOOS": {
					"darwin": {0.3: 2},
					"linux":  {0.3: 2},
				},
				}},
			},
			args: args{
				program: "example.com/mod/pkg",
				name:    "GOOS",
				buckets: []bucketName{"darwin", "linux"},
			},
			want: &chart{
				ID:   "charts:example.com/mod/pkg:GOOS",
				Name: "GOOS",
				Type: "partition",
				Data: []*datum{
					{
						Week:  "2999-01-02",
						Key:   "darwin",
						Value: 1,
					},
					{
						Week:  "2999-01-02",
						Key:   "linux",
						Value: 1,
					},
				},
			},
		},
		{
			name: "three days, missing version data all days",
			data: data{
				"2999-01-01": {"example.com/mod/pkg": {"GOOS": {
					"GOOS":        {0.1: 2},
					"GOOS:darwin": {0.1: 2},
				},
				}},
				"2999-01-02": {"example.com/mod/pkg": {"GOOS": {
					"GOOS":       {0.6: 5},
					"GOOS:linux": {0.6: 5},
				},
				}},
				"2999-01-03": {"example.com/mod/pkg": {"GOOS": {
					"GOOS":        {0.6: 3},
					"GOOS:darwin": {0.6: 3},
				},
				}},
			},
			args: args{
				program: "example.com/mod/pkg",
				name:    "Version",
				buckets: []bucketName{"v1.2.3", "v2.3.4"},
			},
			opts: partitionOptions{normalizeBucket: normalVersion},
			want: nil,
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			got := tc.data.partition(tc.args.program, tc.args.name, tc.args.buckets, tc.opts)
			if diff := cmp.Diff(tc.want, got); diff != "" {
				t.Errorf("partition() mismatch (-want +got):\n%s", diff)
			}
		})
	}
}

func TestCharts(t *testing.T) {
	exampleData := group(exampleReports)
	cfg := &config.Config{
		UploadConfig: &telemetry.UploadConfig{
			GOOS:       []string{"darwin"},
			GOARCH:     []string{"amd64"},
			GoVersion:  []string{"go1.2.3", "go1.19.0"},
			SampleRate: 1,
			Programs: []*telemetry.ProgramConfig{
				{
					Name:     "cmd/go",
					Versions: []string{"go1.2.3"},
					Counters: []telemetry.CounterConfig{{
						Name: "main",
					}},
				},
				{
					Name:     "cmd/compiler",
					Versions: []string{"go1.2.3"},
					Counters: []telemetry.CounterConfig{{
						Name: "count1",
					}},
				},
				{
					Name: "example.com/mod/pkg",
					// Exercise semver sorting. Notably v1.2.3 has data but is not
					// present.
					//
					// TODO(rfindley): in a follow-up CL, remove the MajMin collapsing of
					// Versions. It's actually really interesting to see detailed version
					// information.
					Versions: []string{"v2.3.4", "v2.3.4-pre.1", "v0.15.0"},
					Counters: []telemetry.CounterConfig{
						{Name: "count2"},
						{Name: "flag:{a,b,c}"},
					},
				},
			},
		},
	}
	want := &chartdata{
		DateRange: [2]string{"2999-01-01", "2999-01-01"},
		Programs: []*program{
			{
				ID:   "charts:cmd/go",
				Name: "cmd/go",
				Charts: []*chart{
					{
						ID:   "charts:cmd/go:GOOS",
						Name: "GOOS",
						Type: "partition",
						Data: []*datum{
							{Week: "2999-01-01", Key: "darwin", Value: 1},
						},
					},
					{
						ID:   "charts:cmd/go:GoVersion",
						Name: "GoVersion",
						Type: "partition",
						Data: []*datum{
							{Week: "2999-01-01", Key: "go1.2", Value: 1},
						},
					},
					{
						ID:   "charts:cmd/go:main",
						Name: "main",
						Type: "partition",
						Data: []*datum{
							{Week: "2999-01-01", Key: "main", Value: 1},
						},
					},
				},
			},
			{
				ID:   "charts:cmd/compiler",
				Name: "cmd/compiler",
			},
			{
				ID:   "charts:example.com/mod/pkg",
				Name: "example.com/mod/pkg",
				Charts: []*chart{
					{
						ID:   "charts:example.com/mod/pkg:Version",
						Name: "Version",
						Type: "partition",
						Data: []*datum{
							{Week: "2999-01-01", Key: "v2.3.4-pre.1", Value: 1},
							{Week: "2999-01-01", Key: "v2.3.4", Value: 2},
						},
					},
					{
						ID:   "charts:example.com/mod/pkg:GOOS",
						Name: "GOOS",
						Type: "partition",
						Data: []*datum{
							{Week: "2999-01-01", Key: "darwin", Value: 2},
						},
					},
					{
						ID:   "charts:example.com/mod/pkg:GOARCH",
						Name: "GOARCH",
						Type: "partition",
						Data: []*datum{
							{Week: "2999-01-01", Key: "amd64", Value: 1},
						},
					},
					{
						ID:   "charts:example.com/mod/pkg:GoVersion",
						Name: "GoVersion",
						Type: "partition",
						Data: []*datum{
							{Week: "2999-01-01", Key: "go1.2", Value: 3},
							{Week: "2999-01-01", Key: "go1.19", Value: 1},
						},
					},
					{
						ID:   "charts:example.com/mod/pkg:flag",
						Name: "flag",
						Type: "partition",
						Data: []*datum{
							{Week: "2999-01-01", Key: "a", Value: 3},
							{Week: "2999-01-01", Key: "b", Value: 3},
							{Week: "2999-01-01", Key: "c", Value: 1},
						},
					},
				},
			},
		},
		NumReports: 1,
	}
	got := charts(cfg, "2999-01-01", "2999-01-01", exampleData, []float64{0.12345})
	if diff := cmp.Diff(want, got); diff != "" {
		t.Errorf("charts = %+v\n, (-want +got): %v", got, diff)
	}
}

func TestWriteCount(t *testing.T) {
	type keyValue struct {
		week    weekName
		program programName
		chart   graphName
		bucket  bucketName
		x       reportID
		value   int64
	}
	testcases := []struct {
		name   string
		inputs []keyValue
		want   []keyValue
	}{
		{
			name: "program version counter should have value",
			inputs: []keyValue{
				{"2987-07-01", "golang.org/x/tools/gopls", "Version", "v0.15.3", 0.00009, 1},
			},
			want: []keyValue{
				{"2987-07-01", "golang.org/x/tools/gopls", "Version", "v0.15.3", 0.00009, 1},
			},
		},
		{
			name: "only one count with same prefix and counter",
			inputs: []keyValue{
				{"2987-06-30", "cmd/go", "go/invocations", "go/invocations", 0.86995, 84},
			},
			want: []keyValue{
				{"2987-06-30", "cmd/go", "go/invocations", "go/invocations", 0.86995, 84},
			},
		},
		{
			name: "overwrite values when calling multiple times",
			inputs: []keyValue{
				{"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "windows", 0.86018, 1},
				{"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "windows", 0.86018, 2},
				{"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "windows", 0.86018, 3},
			},
			want: []keyValue{
				{"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "windows", 0.86018, 3},
			},
		},
		{
			name: "multiple counters",
			inputs: []keyValue{
				{"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "windows", 0.86018, 2},
				{"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "linux", 0.86018, 4},
			},
			want: []keyValue{
				{"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "windows", 0.86018, 2},
				{"2987-06-30", "golang.org/x/tools/gopls", "GOOS", "linux", 0.86018, 4},
			},
		},
	}

	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			d := make(data)
			for _, input := range tc.inputs {
				d.writeCount(input.week, input.program, input.chart, input.bucket, input.x, input.value)
			}

			for _, want := range tc.want {
				got := d[want.week][want.program][want.chart][want.bucket][want.x]
				if want.value != got {
					t.Errorf("d[%q][%q][%q][%q][%v] = %v, want %v", want.week, want.program, want.chart, want.bucket, want.x, got, want.value)
				}
			}
		})
	}
}

func TestParseDateRange(t *testing.T) {
	testcases := []struct {
		name      string
		url       string
		wantStart time.Time
		wantEnd   time.Time
		wantErr   bool
	}{
		{
			name:      "regular key start & end input",
			url:       "http://localhost:8082/chart/?start=2024-06-10&end=2024-06-17",
			wantStart: time.Date(2024, 06, 10, 0, 0, 0, 0, time.UTC),
			wantEnd:   time.Date(2024, 06, 17, 0, 0, 0, 0, time.UTC),
		},
		{
			name:      "regular key date input",
			url:       "http://localhost:8082/chart/?date=2024-06-11",
			wantStart: time.Date(2024, 06, 11, 0, 0, 0, 0, time.UTC),
			wantEnd:   time.Date(2024, 06, 11, 0, 0, 0, 0, time.UTC),
		},
		{
			name:    "malformatted value for start",
			url:     "http://localhost:8082/chart/?start=2024-066-01&end=2024-06-17",
			wantErr: true,
		},
		{
			name:    "malformatted value for start",
			url:     "http://localhost:8082/chart/?start=2024-06-10&end=2024-06-179",
			wantErr: true,
		},
		{
			name:    "end is earlier than start",
			url:     "http://localhost:8082/chart/?start=2024-06-17&end=2024-06-10",
			wantErr: true,
		},
		{
			name:    "have only start but missing end",
			url:     "http://localhost:8082/chart/?start=2024-06-01",
			wantErr: true,
		},
		{
			name:    "key date and start used together",
			url:     "http://localhost:8082/chart/?start=2024-06-17&date=2024-06-19",
			wantErr: true,
		},
	}

	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			url, err := url.Parse(tc.url)
			if err != nil {
				t.Fatalf("failed to parse url %q: %v", url, err)
			}

			gotStart, gotEnd, err := parseDateRange(url)
			if tc.wantErr && err == nil {
				t.Errorf("parseDateRange %v should return error but return nil", tc.url)
			}
			if !tc.wantErr && err != nil {
				t.Errorf("parseDateRange %v should return nil but return error: %v", tc.url, err)
			}

			if !tc.wantErr {
				if !gotStart.Equal(tc.wantStart) || !gotEnd.Equal(tc.wantEnd) {
					t.Errorf("parseDateRange(%s) = (%s, %s), want (%s, %s)", tc.url, gotStart.Format(telemetry.DateOnly), gotEnd.Format(telemetry.DateOnly), tc.wantStart.Format(telemetry.DateOnly), tc.wantEnd.Format(telemetry.DateOnly))
				}
			}
		})
	}
}
