// Copyright 2021 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 (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	"golang.org/x/tools/internal/testenv"
)

func canRace() bool {
	_, err := exec.Command("go", "run", "-race", "./testdata/himom.go").CombinedOutput()
	return err == nil
}

// buildRunner builds the fuzz-runner executable, returning its path.
func buildRunner(t *testing.T) string {
	bindir := filepath.Join(t.TempDir(), "bin")
	err := os.Mkdir(bindir, os.ModePerm)
	if err != nil {
		t.Fatal(err)
	}
	binary := filepath.Join(bindir, "runner")
	if runtime.GOOS == "windows" {
		binary += ".exe"
	}
	cmd := exec.Command("go", "build", "-o", binary)
	if err := cmd.Run(); err != nil {
		t.Fatalf("Building fuzz-runner: %v", err)
	}
	return binary
}

// TestRunner builds the binary, then kicks off a collection of sub-tests that invoke it.
func TestRunner(t *testing.T) {
	testenv.NeedsTool(t, "go")
	if runtime.GOOS == "android" {
		t.Skipf("the dependencies are not available on android")
	}
	binaryPath := buildRunner(t)

	// Sub-tests using the binary built above.
	t.Run("Basic", func(t *testing.T) { testBasic(t, binaryPath) })
	t.Run("Race", func(t *testing.T) { testRace(t, binaryPath) })
	t.Run("Minimization1", func(t *testing.T) { testMinimization1(t, binaryPath) })
	t.Run("Minimization2", func(t *testing.T) { testMinimization2(t, binaryPath) })
}

func testBasic(t *testing.T, binaryPath string) {
	t.Parallel()
	args := []string{"-numit=1", "-numfcns=1", "-numpkgs=1", "-seed=103", "-cleancache=0"}
	c := exec.Command(binaryPath, args...)
	b, err := c.CombinedOutput()
	t.Logf("%s\n", b)
	if err != nil {
		t.Fatalf("error invoking fuzz-runner: %v", err)
	}
}

func testRace(t *testing.T, binaryPath string) {
	t.Parallel()
	// For this test to work, the current test platform has to support the
	// race detector. Check to see if that is the case by running a very
	// simple Go program through it.
	if !canRace() {
		t.Skip("current platform does not appear to support the race detector")
	}

	args := []string{"-v=1", "-numit=1", "-race", "-numfcns=3", "-numpkgs=3", "-seed=987", "-cleancache=0"}
	c := exec.Command(binaryPath, args...)
	b, err := c.CombinedOutput()
	t.Logf("%s\n", b)
	if err != nil {
		t.Fatalf("error invoking fuzz-runner: %v", err)
	}
}

func testMinimization1(t *testing.T, binaryPath string) {
	if binaryPath == "" {
		t.Skipf("No runner binary")
	}
	t.Parallel()
	// Fire off the runner passing it -emitbad=1, so that the generated code
	// contains illegal Go code (which will force the build to fail). Verify that
	// it does fail, that the error reflects the nature of the failure, and that
	// we can minimize the error down to a single package.
	args := []string{"-emitbad=1", "-badfcnidx=2", "-badpkgidx=2",
		"-forcetmpclean", "-cleancache=0",
		"-numit=1", "-numfcns=3", "-numpkgs=3", "-seed=909"}
	invocation := fmt.Sprintf("%s %v", binaryPath, args)
	c := exec.Command(binaryPath, args...)
	b, err := c.CombinedOutput()
	t.Logf("%s\n", b)
	if err == nil {
		t.Fatalf("unexpected pass of fuzz-runner (invocation %q): %v", invocation, err)
	}
	result := string(b)
	if !strings.Contains(result, "syntax error") {
		t.Fatalf("-emitbad=1 did not trigger syntax error (invocation %q): output: %s", invocation, result)
	}
	if !strings.Contains(result, "package minimization succeeded: found bad pkg 2") {
		t.Fatalf("failed to minimize package (invocation %q): output: %s", invocation, result)
	}
	if !strings.Contains(result, "function minimization succeeded: found bad fcn 2") {
		t.Fatalf("failed to minimize package (invocation %q): output: %s", invocation, result)
	}
}

func testMinimization2(t *testing.T, binaryPath string) {
	if binaryPath == "" {
		t.Skipf("No runner binary")
	}
	t.Parallel()
	// Fire off the runner passing it -emitbad=2, so that the
	// generated code forces a runtime error. Verify that it does
	// fail, and that the error is reflective.
	args := []string{"-emitbad=2", "-badfcnidx=1", "-badpkgidx=1",
		"-forcetmpclean", "-cleancache=0",
		"-numit=1", "-numfcns=3", "-numpkgs=3", "-seed=55909"}
	invocation := fmt.Sprintf("%s %v", binaryPath, args)
	c := exec.Command(binaryPath, args...)
	b, err := c.CombinedOutput()
	t.Logf("%s\n", b)
	if err == nil {
		t.Fatalf("unexpected pass of fuzz-runner (invocation %q): %v", invocation, err)
	}
	result := string(b)
	if !strings.Contains(result, "Error: fail") || !strings.Contains(result, "Checker1.Test1") {
		t.Fatalf("-emitbad=2 did not trigger runtime error (invocation %q): output: %s", invocation, result)
	}
	if !strings.Contains(result, "package minimization succeeded: found bad pkg 1") {
		t.Fatalf("failed to minimize package (invocation %q): output: %s", invocation, result)
	}
	if !strings.Contains(result, "function minimization succeeded: found bad fcn 1") {
		t.Fatalf("failed to minimize package (invocation %q): output: %s", invocation, result)
	}
}
