package main

import (
	"bytes"
	"context"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"testing"
	"time"
)

const (
	// CITimeoutMultiplier is the multiplier for all timeouts in the CI.
	CITimeoutMultiplier = 3
)

// Sleep pauses the current goroutine for at least the duration d.
func Sleep(d time.Duration) {
	d = applyCIMultiplier(d)
	time.Sleep(d)
}

func applyCIMultiplier(timeout time.Duration) time.Duration {
	ci := os.Getenv("CI")
	if ci == "" {
		return timeout
	}
	b, err := strconv.ParseBool(ci)
	if err != nil || !b {
		return timeout
	}
	return time.Duration(float64(timeout) * CITimeoutMultiplier)
}

func TestYaegiCmdCancel(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("skipping cancel test since windows has no os.Interrupt signal")
	}
	tmp := t.TempDir()
	yaegi := filepath.Join(tmp, "yaegi")

	args := []string{"build"}
	if raceDetectorSupported(runtime.GOOS, runtime.GOARCH) {
		args = append(args, "-race")
	}
	args = append(args, "-o", yaegi, ".")

	build := exec.Command("go", args...)

	out, err := build.CombinedOutput()
	if err != nil {
		t.Fatalf("failed to build yaegi command: %v: %s", err, out)
	}

	// Test src must be terminated by a single newline.
	tests := []string{
		"for {}\n",
		"select {}\n",
	}
	for _, src := range tests {
		cmd := exec.Command(yaegi)
		in, err := cmd.StdinPipe()
		if err != nil {
			t.Errorf("failed to get stdin pipe to yaegi command: %v", err)
		}
		var outBuf, errBuf bytes.Buffer
		cmd.Stdout = &outBuf
		cmd.Stderr = &errBuf

		// https://golang.org/doc/articles/race_detector.html#Options
		cmd.Env = []string{`GORACE="halt_on_error=1"`}

		err = cmd.Start()
		if err != nil {
			t.Fatalf("failed to start yaegi command: %v", err)
		}

		_, err = in.Write([]byte(src))
		if err != nil {
			t.Errorf("failed pipe test source to yaegi command: %v", err)
		}
		Sleep(500 * time.Millisecond)
		err = cmd.Process.Signal(os.Interrupt)
		if err != nil {
			t.Errorf("failed to send os.Interrupt to yaegi command: %v", err)
		}

		_, err = in.Write([]byte("1+1\n"))
		if err != nil {
			t.Errorf("failed to probe race: %v", err)
		}
		err = in.Close()
		if err != nil {
			t.Errorf("failed to close stdin pipe: %v", err)
		}

		err = cmd.Wait()
		if err != nil {
			if cmd.ProcessState.ExitCode() == 66 { // See race_detector.html article.
				t.Errorf("race detected running yaegi command canceling %q: %v", src, err)
				if testing.Verbose() {
					t.Log(&errBuf)
				}
			} else {
				t.Errorf("error running yaegi command for %q: %v", src, err)
			}
			continue
		}

		if strings.TrimSuffix(errBuf.String(), "\n") != context.Canceled.Error() {
			t.Errorf("unexpected error: %q", &errBuf)
		}
	}
}

func raceDetectorSupported(goos, goarch string) bool {
	if strings.Contains(os.Getenv("GOFLAGS"), "-buildmode=pie") {
		// The Go race detector is not compatible with position independent code (pie).
		// We read the conventional GOFLAGS env variable used for example on AlpineLinux
		// to build packages, as there is no way to get this information from the runtime.
		return false
	}
	switch goos {
	case "linux":
		return goarch == "amd64" || goarch == "ppc64le" || goarch == "arm64"
	case "darwin":
		return goarch == "amd64" || goarch == "arm64"
	case "freebsd", "netbsd", "openbsd", "windows":
		return goarch == "amd64"
	default:
		return false
	}
}
