// 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 (
	"bytes"
	"context"
	_ "embed"
	"flag"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	"golang.org/x/telemetry/godev/internal/config"
	tconfig "golang.org/x/telemetry/internal/config"
	"golang.org/x/telemetry/internal/telemetry"
	"golang.org/x/telemetry/internal/testenv"
)

func TestMain(m *testing.M) {
	if !canRunGoDevModuleTests() {
		return
	}
	os.Exit(m.Run())
}

// First class ports, https://go.dev/wiki/PortingPolicy, excluding 386 and arm.
var onSupportedPlatform = map[string]bool{
	"darwin/amd64":  true,
	"darwin/arm64":  true,
	"linux/amd64":   true,
	"linux/arm64":   true,
	"windows/amd64": true,
}

// canRunGoDevModuleTests returns whether the current test environment
// is suitable for golang.org/x/telemetry/godev module tests.
func canRunGoDevModuleTests() bool {
	// Even though telemetry.go.dev runs on linux,
	// we should be still able to debug and test telemetry.go.dev on
	// contributors' machines locally.
	if goosarch := runtime.GOOS + "/" + runtime.GOARCH; !onSupportedPlatform[goosarch] {
		return false
	}
	// Must be able to run 'go'.
	if err := testenv.HasGo(); err != nil {
		return false
	}

	// Our tests must run from the repository source, not from module cache.
	// Check golang.org/x/telemetry directory is accessible and has go.mod and config/config.json.
	output, err := exec.Command("go", "list", "-f", "{{.Dir}}", "golang.org/x/telemetry").Output()
	if err != nil {
		return false
	}
	xTelemetryDir := string(bytes.TrimSpace(output))
	if xTelemetryDir == "" {
		return false
	}
	if _, err := os.Stat(filepath.Join(xTelemetryDir, "go.mod")); err != nil {
		return false
	}
	// config/config.json is in the golang.org/x/telemetry/config module, so
	// this doesn't hold from e.g. GOMODCACHE.
	if _, err := os.Stat(filepath.Join(xTelemetryDir, "config", "config.json")); err != nil {
		return false
	}

	return true
}

// If telemetry_url is configured, TestPaths may be used as a basic push test.
var telemetryURL = flag.String("telemetry_url", "", "url of the telemetry instance to test")

func TestPaths(t *testing.T) {
	rootURL := *telemetryURL
	if rootURL == "" {
		ctx := context.Background()
		cfg := config.NewConfig()
		cfg.LocalStorage = t.TempDir()
		cfg.ProjectID = "" // defensive: don't use a real project ID for tests.

		// NewConfig assumes that the command is run from the repo root, but tests
		// run from their test directory. We should fix this, but for now just
		// fix up the config path.
		// TODO(rfindley): fix this.
		cfg.UploadConfig = filepath.Join("..", "..", "..", "config", "config.json")
		handler := newHandler(ctx, cfg)
		ts := httptest.NewServer(handler)
		defer ts.Close()
		rootURL = ts.URL
	}

	tests := []struct {
		method    string
		path      string
		body      string
		code      int
		fragments []string
	}{
		{"GET", "/", "", 200, []string{"Go Telemetry"}},
		{"GET", "/privacy", "", 200, []string{"Privacy Policy"}},
		{"GET", "/config", "", 200, []string{"Chart Config"}},
		{
			"POST",
			"/upload/2023-01-01/123.json",
			`{"Week":"2023-01-01","LastWeek":"2022-12-25","X":0.123,"Programs":null,"Config":"v0.0.0-20230822160736-17171dbf1d76"}`,
			200,
			nil, // the body returned by /upload doesn't matter
		},
	}

	for _, test := range tests {
		t.Run(test.method+" "+test.path, func(t *testing.T) {
			url := strings.TrimRight(rootURL, "/") + test.path
			r := strings.NewReader(test.body)
			req, err := http.NewRequest(test.method, url, r)
			if err != nil {
				t.Fatalf("NewRequest failed: %v", err)
			}
			resp, err := http.DefaultClient.Do(req)
			if err != nil {
				t.Fatalf("request failed: %v", err)
			}
			defer resp.Body.Close()

			if resp.StatusCode != test.code {
				t.Errorf("status code = %d, want %d", resp.StatusCode, test.code)
			}

			content, err := io.ReadAll(resp.Body)
			if err != nil {
				t.Fatalf("reading body: %v", err)
			}
			for _, fragment := range test.fragments {
				if !bytes.Contains(content, []byte(fragment)) {
					t.Errorf("missing fragment %q", fragment)
				}
			}
		})
	}
}

func TestValidate(t *testing.T) {
	cfg, err := tconfig.ReadConfig("testdata/config.json")
	if err != nil {
		t.Fatal(err)
	}
	tests := []struct {
		name    string
		report  *telemetry.Report
		wantErr bool
	}{
		{
			name:    "empty report",
			report:  &telemetry.Report{},
			wantErr: true,
		},
		{
			name: "valid report with no counters",
			report: &telemetry.Report{
				Week:     "2023-06-15",
				LastWeek: "",
				X:        0.1,
				Programs: []*telemetry.ProgramReport{},
				Config:   "v0.0.1-test",
			},
			wantErr: false,
		},
		{
			name: "valid report with counters",
			report: &telemetry.Report{
				Week:     "2023-06-15",
				LastWeek: "",
				X:        0.1,
				Programs: []*telemetry.ProgramReport{
					{
						Program:   "golang.org/x/tools/gopls",
						Version:   "v0.10.1",
						GoVersion: "go1.20.1",
						GOOS:      "linux",
						GOARCH:    "arm64",
						Counters: map[string]int64{
							"editor:vim": 100,
						},
					},
				},
				Config: "v0.0.1-test",
			},
		},
		{
			name: "valid report with a stack counter",
			report: &telemetry.Report{
				Week:     "2023-06-15",
				LastWeek: "",
				X:        1.0,
				Programs: []*telemetry.ProgramReport{
					{
						Program:   "golang.org/x/tools/gopls",
						Version:   "v0.10.1",
						GoVersion: "go1.20.1",
						GOOS:      "linux",
						GOARCH:    "arm64",
						Stacks: map[string]int64{
							"gopls/bug\ngolang.org/x/tools/gopls/internal/bug.report:35\ngolang.org/x/tools/gopls/internal/bug.Errorf:2\ngolang.org/x/tools/gopls/internal/lsp.(*Server).SignatureHelp:1\nruntime.goexit:0": 1,
						},
					},
				},
				Config: "v0.0.1-test",
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if err := validate(tt.report, cfg); (err != nil) != tt.wantErr {
				t.Errorf("validate() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}
