// Copyright 2022 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 integration

import (
	"maps"
	"strings"
	"testing"
	"time"

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

type runConfig struct {
	editor  fake.EditorConfig
	sandbox fake.SandboxConfig
	// modes determines the execution modes for the test.
	//
	// If nil (unset), the runner falls back to its DefaultModes.
	// If non-nil but empty (*modes == NoMode), the test runs in 0 modes (effectively skipping it).
	// If non-nil and non-empty, the test runs only in the specified modes.
	modes         *Mode
	noLogsOnError bool
	writeGoSum    []string
}

func defaultConfig() runConfig {
	return runConfig{
		editor: fake.EditorConfig{
			Settings: map[string]any{
				// Shorten the diagnostic delay to speed up test execution (else we'd add
				// the default delay to each assertion about diagnostics)
				"diagnosticsDelay": "10ms",
			},
		},
	}
}

// A RunOption augments the behavior of the test runner.
type RunOption interface {
	set(*runConfig)
}

type optionSetter func(*runConfig)

func (f optionSetter) set(opts *runConfig) {
	f(opts)
}

// ProxyFiles configures a file proxy using the given txtar-encoded string.
func ProxyFiles(txt string) RunOption {
	return optionSetter(func(opts *runConfig) {
		opts.sandbox.ProxyFiles = fake.UnpackTxt(txt)
	})
}

// CacheFiles configures a module cache using the given txtar-encoded string.
func CacheFiles(txt string) RunOption {
	return optionSetter(func(opts *runConfig) {
		opts.sandbox.CacheFiles = fake.UnpackTxt(txt)
	})
}

// WriteGoSum causes the environment to write a go.sum file for the requested
// relative directories (via `go list -mod=mod`), before starting gopls.
//
// Useful for tests that use ProxyFiles, but don't care about crafting the
// go.sum content.
func WriteGoSum(dirs ...string) RunOption {
	return optionSetter(func(opts *runConfig) {
		opts.writeGoSum = dirs
	})
}

// Modes configures the execution modes that the test should run in.
//
// By default, modes are configured by the test runner. If this option is set,
// it overrides the set of default modes and the test runs in exactly these
// modes.
func Modes(modes Mode) RunOption {
	return optionSetter(func(opts *runConfig) {
		if opts.modes != nil {
			panic("modes set more than once")
		}
		opts.modes = &modes
	})
}

// NoLogsOnError turns off dumping the LSP logs on test failures.
func NoLogsOnError() RunOption {
	return optionSetter(func(opts *runConfig) {
		opts.noLogsOnError = true
	})
}

// WindowsLineEndings configures the editor to use windows line endings.
func WindowsLineEndings() RunOption {
	return optionSetter(func(opts *runConfig) {
		opts.editor.WindowsLineEndings = true
	})
}

// ClientName sets the LSP client name.
func ClientName(name string) RunOption {
	return optionSetter(func(opts *runConfig) {
		opts.editor.ClientName = name
	})
}

// CapabilitiesJSON sets the capabalities json.
func CapabilitiesJSON(capabilities []byte) RunOption {
	return optionSetter(func(opts *runConfig) {
		opts.editor.CapabilitiesJSON = capabilities
	})
}

// Settings sets user-provided configuration for the LSP server.
//
// As a special case, the env setting must not be provided via Settings: use
// EnvVars instead.
type Settings map[string]any

func (s Settings) set(opts *runConfig) {
	if opts.editor.Settings == nil {
		opts.editor.Settings = make(map[string]any)
	}
	maps.Copy(opts.editor.Settings, s)
}

// WorkspaceFolders configures the workdir-relative workspace folders or uri
// to send to the LSP server. By default the editor sends a single workspace folder
// corresponding to the workdir root. To explicitly configure no workspace
// folders, use WorkspaceFolders with no arguments.
func WorkspaceFolders(relFolders ...string) RunOption {
	if len(relFolders) == 0 {
		// Use an empty non-nil slice to signal explicitly no folders.
		relFolders = []string{}
	}

	return optionSetter(func(opts *runConfig) {
		opts.editor.WorkspaceFolders = relFolders
	})
}

// NoDefaultWorkspaceFiles is used to specify whether the fake editor
// should give a default workspace folder to the LSP server.
// When it's true, the editor will pass original WorkspaceFolders to the LSP server.
func NoDefaultWorkspaceFiles() RunOption {
	return optionSetter(func(opts *runConfig) {
		opts.editor.NoDefaultWorkspaceFiles = true
	})
}

// RootPath configures the roo path which will be converted to rootUri and sent to the LSP server.
func RootPath(relpath string) RunOption {
	return optionSetter(func(opts *runConfig) {
		opts.editor.RelRootPath = relpath
	})
}

// FolderSettings defines per-folder workspace settings, keyed by relative path
// to the folder.
//
// Use in conjunction with WorkspaceFolders to have different settings for
// different folders.
type FolderSettings map[string]Settings

func (fs FolderSettings) set(opts *runConfig) {
	// Re-use the Settings type, for symmetry, but translate back into maps for
	// the editor config.
	folders := make(map[string]map[string]any)
	for k, v := range fs {
		folders[k] = v
	}
	opts.editor.FolderSettings = folders
}

// EnvVars sets environment variables for the LSP session. When applying these
// variables to the session, the special string $SANDBOX_WORKDIR is replaced by
// the absolute path to the sandbox working directory.
type EnvVars map[string]string

func (e EnvVars) set(opts *runConfig) {
	if opts.editor.Env == nil {
		opts.editor.Env = make(map[string]string)
	}
	maps.Copy(opts.editor.Env, e)
}

// FakeGoPackagesDriver configures gopls to run with a fake GOPACKAGESDRIVER
// environment variable.
func FakeGoPackagesDriver(t *testing.T) RunOption {
	env := drivertest.Env(t)
	vars := make(EnvVars)
	for _, e := range env {
		kv := strings.SplitN(e, "=", 2)
		vars[kv[0]] = kv[1]
	}
	return vars
}

// InGOPATH configures the workspace working directory to be GOPATH, rather
// than a separate working directory for use with modules.
func InGOPATH() RunOption {
	return optionSetter(func(opts *runConfig) {
		opts.sandbox.InGoPath = true
	})
}

// MessageResponder configures the editor to respond to
// window/showMessageRequest messages using the provided function.
func MessageResponder(f func(*protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error)) RunOption {
	return optionSetter(func(opts *runConfig) {
		opts.editor.MessageResponder = f
	})
}

// DelayMessages can be used to fuzz message delivery delays for the purpose of
// reproducing test flakes.
//
// (Even though this option may be unused, keep it around to aid in debugging
// future flakes.)
func DelayMessages(upto time.Duration) RunOption {
	return optionSetter(func(opts *runConfig) {
		opts.editor.MaxMessageDelay = upto
	})
}
