// Copyright 2025 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 bench

import (
	"context"
	"fmt"
	"go/token"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"

	. "golang.org/x/tools/gopls/internal/test/integration"
	"golang.org/x/tools/gopls/internal/test/integration/fake"
	"golang.org/x/tools/internal/modindex"
)

// experiments show the new code about 15 times faster than the old,
// and the old code sometimes fails to find the completion
func BenchmarkLocalModcache(b *testing.B) {
	budgets := []string{"0s", "100ms", "200ms", "500ms", "1s", "5s"}
	sources := []string{"gopls", "goimports"}
	for _, budget := range budgets {
		b.Run(fmt.Sprintf("budget=%s", budget), func(b *testing.B) {
			for _, source := range sources {
				b.Run(fmt.Sprintf("source=%s", source), func(b *testing.B) {
					runModcacheCompletion(b, budget, source)
				})
			}
		})
	}
}

func runModcacheCompletion(b *testing.B, budget, source string) {
	// First set up the program to be edited
	gomod := `
module mod.com

go 1.21
`
	pat := `
package main
var _ = %s.%s
`
	pkg, name, modcache := findSym(b)
	name, _, _ = strings.Cut(name, " ")
	mainfile := fmt.Sprintf(pat, pkg, name)
	// Second, create the Env and start gopls
	dir := getTempDir()
	if err := os.Mkdir(dir, 0750); err != nil {
		if !os.IsExist(err) {
			b.Fatal(err)
		}
	}
	defer os.RemoveAll(dir) // is this right? needed?
	if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(gomod), 0644); err != nil {
		b.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainfile), 0644); err != nil {
		b.Fatal(err)
	}
	ts, err := newGoplsConnector(nil)
	if err != nil {
		b.Fatal(err)
	}
	// PJW: put better EditorConfig here
	envvars := map[string]string{
		"GOMODCACHE": modcache,
		//"GOPATH":     sandbox.GOPATH(), // do we need a GOPATH?
	}
	fc := fake.EditorConfig{
		Env: envvars,
		Settings: map[string]any{
			"completeUnimported": true,
			"completionBudget":   budget, // "0s", "100ms"
			"importsSource":      source, // "gopls" or "goimports"
		},
	}
	sandbox, editor, awaiter, err := connectEditor(dir, fc, ts)
	if err != nil {
		b.Fatal(err)
	}
	defer sandbox.Close()
	defer editor.Close(context.Background())
	if err := awaiter.Await(context.Background(), InitialWorkspaceLoad); err != nil {
		b.Fatal(err)
	}
	env := &Env{
		TB:      b,
		Ctx:     context.Background(),
		Editor:  editor,
		Sandbox: sandbox,
		Awaiter: awaiter,
	}
	// Check that completion works as expected
	env.CreateBuffer("main.go", mainfile)
	env.AfterChange()
	if false { // warm up? or not?
		loc := env.RegexpSearch("main.go", name)
		completions := env.Completion(loc)
		if len(completions.Items) == 0 {
			b.Fatal("no completions")
		}
	}

	// run benchmark
	for b.Loop() {
		loc := env.RegexpSearch("main.go", name)
		env.Completion(loc)
	}
}

// find some symbol in the module cache
func findSym(t testing.TB) (pkg, name, gomodcache string) {
	initForTest(t)
	cmd := exec.Command("go", "env", "GOMODCACHE")
	out, err := cmd.Output()
	if err != nil {
		t.Fatal(err)
	}
	modcache := strings.TrimSpace(string(out))
	ix, err := modindex.ReadIndex(modcache)
	if err != nil {
		t.Fatal(err)
	}
	if ix == nil {
		t.Fatal("no index")
	}
	if len(ix.Entries) == 0 {
		t.Fatal("no entries")
	}
	nth := 100 // or something
	for _, e := range ix.Entries {
		if token.IsExported(e.PkgName) || strings.HasPrefix(e.PkgName, "_") {
			continue // weird stuff in module cache
		}

		for _, nm := range e.Names {
			nth--
			if nth == 0 {
				return e.PkgName, nm, modcache
			}
		}
	}
	t.Fatalf("index doesn't have enough usable names, need another %d", nth)
	return "", "", modcache
}

// Set IndexDir, avoiding the special case for tests,
func initForTest(t testing.TB) {
	dir, err := os.UserCacheDir()
	if err != nil {
		t.Fatalf("os.UserCacheDir: %v", err)
	}
	dir = filepath.Join(dir, "go", "imports")
	modindex.IndexDir = dir
}
