// 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 generator

import (
	"bytes"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"testing"

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

func mkGenState() *genstate {

	return &genstate{
		GenConfig: GenConfig{
			Tag:              "gen",
			OutDir:           "/tmp",
			NumTestPackages:  1,
			NumTestFunctions: 10,
		},
		ipref:       "foo/",
		derefFuncs:  make(map[string]string),
		assignFuncs: make(map[string]string),
		allocFuncs:  make(map[string]string),
		globVars:    make(map[string]string),
	}
}

func TestBasic(t *testing.T) {
	checkTunables(tunables)
	s := mkGenState()
	for i := range 1000 {
		s.wr = NewWrapRand(int64(i), RandCtlChecks|RandCtlPanic)
		fp := s.GenFunc(i, i)
		var buf bytes.Buffer
		var b *bytes.Buffer = &buf
		wr := NewWrapRand(int64(i), RandCtlChecks|RandCtlPanic)
		s.wr = wr
		s.emitCaller(fp, b, i)
		s.wr = NewWrapRand(int64(i), RandCtlChecks|RandCtlPanic)
		s.emitChecker(fp, b, i, true)
		wr.Check(s.wr)
	}
	if s.errs != 0 {
		t.Errorf("%d errors during Generate", s.errs)
	}
}

func TestMoreComplicated(t *testing.T) {
	saveit := tunables
	defer func() { tunables = saveit }()

	checkTunables(tunables)
	s := mkGenState()
	for i := range 10000 {
		s.wr = NewWrapRand(int64(i), RandCtlChecks|RandCtlPanic)
		fp := s.GenFunc(i, i)
		var buf bytes.Buffer
		var b *bytes.Buffer = &buf
		wr := NewWrapRand(int64(i), RandCtlChecks|RandCtlPanic)
		s.wr = wr
		s.emitCaller(fp, b, i)
		verb(1, "finished iter %d caller", i)
		s.wr = NewWrapRand(int64(i), RandCtlChecks|RandCtlPanic)
		s.emitChecker(fp, b, i, true)
		verb(1, "finished iter %d checker", i)
		wr.Check(s.wr)
		if s.errs != 0 {
			t.Errorf("%d errors during Generate iter %d", s.errs, i)
		}
	}
}

func TestIsBuildable(t *testing.T) {
	testenv.NeedsTool(t, "go")
	if runtime.GOOS == "android" {
		t.Skipf("the dependencies are not available on android")
	}

	td := t.TempDir()
	verb(1, "generating into temp dir %s", td)
	checkTunables(tunables)
	pack := filepath.Base(td)
	s := GenConfig{
		Tag:              "x",
		OutDir:           td,
		PkgPath:          pack,
		NumTestFunctions: 10,
		NumTestPackages:  10,
		MaxFail:          10,
		RandCtl:          RandCtlChecks | RandCtlPanic,
	}
	errs := Generate(s)
	if errs != 0 {
		t.Errorf("%d errors during Generate", errs)
	}

	verb(1, "building %s\n", td)

	cmd := exec.Command("go", "run", ".")
	cmd.Dir = td
	coutput, cerr := cmd.CombinedOutput()
	if cerr != nil {
		t.Errorf("go build command failed: %s\n", string(coutput))
	}
	verb(1, "output is: %s\n", string(coutput))
}

// TestExhaustive does a series of code generation runs, starting with
// (relatively) simple code and then getting progressively more
// complex (more params, deeper structs, turning on additional
// features such as address-taken vars and reflect testing). The
// intent here is mainly to insure that the tester still works if you
// turn things on and off, e.g. that each feature is separately
// controllable and not linked to other things.
func TestExhaustive(t *testing.T) {
	testenv.NeedsTool(t, "go")
	if runtime.GOOS == "android" {
		t.Skipf("the dependencies are not available on android")
	}

	if testing.Short() {
		t.Skip("skipping test in short mode.")
	}

	td := t.TempDir()
	verb(1, "generating into temp dir %s", td)

	scenarios := []struct {
		name     string
		adjuster func()
	}{
		{
			"minimal",
			func() {
				tunables.nParmRange = 3
				tunables.nReturnRange = 3
				tunables.structDepth = 1
				tunables.recurPerc = 0
				tunables.methodPerc = 0
				tunables.doReflectCall = false
				tunables.doDefer = false
				tunables.takeAddress = false
				tunables.doFuncCallValues = false
				tunables.doSkipCompare = false
				checkTunables(tunables)
			},
		},
		{
			"moreparms",
			func() {
				tunables.nParmRange = 15
				tunables.nReturnRange = 7
				tunables.structDepth = 3
				checkTunables(tunables)
			},
		},
		{
			"addrecur",
			func() {
				tunables.recurPerc = 20
				checkTunables(tunables)
			},
		},
		{
			"addmethod",
			func() {
				tunables.methodPerc = 25
				tunables.pointerMethodCallPerc = 30
				checkTunables(tunables)
			},
		},
		{
			"addtakeaddr",
			func() {
				tunables.takeAddress = true
				tunables.takenFraction = 20
				checkTunables(tunables)
			},
		},
		{
			"addreflect",
			func() {
				tunables.doReflectCall = true
				checkTunables(tunables)
			},
		},
		{
			"adddefer",
			func() {
				tunables.doDefer = true
				checkTunables(tunables)
			},
		},
		{
			"addfuncval",
			func() {
				tunables.doFuncCallValues = true
				checkTunables(tunables)
			},
		},
		{
			"addfuncval",
			func() {
				tunables.doSkipCompare = true
				checkTunables(tunables)
			},
		},
	}

	// Loop over scenarios and make sure each one works properly.
	for i, s := range scenarios {
		t.Logf("running %s\n", s.name)
		s.adjuster()
		os.RemoveAll(td)
		pack := filepath.Base(td)
		c := GenConfig{
			Tag:              "x",
			OutDir:           td,
			PkgPath:          pack,
			NumTestFunctions: 10,
			NumTestPackages:  10,
			Seed:             int64(i + 9),
			MaxFail:          10,
			RandCtl:          RandCtlChecks | RandCtlPanic,
		}
		errs := Generate(c)
		if errs != 0 {
			t.Errorf("%d errors during scenarios %q Generate", errs, s.name)
		}
		cmd := exec.Command("go", "run", ".")
		cmd.Dir = td
		coutput, cerr := cmd.CombinedOutput()
		if cerr != nil {
			t.Fatalf("run failed for scenario %q:  %s\n", s.name, string(coutput))
		}
		verb(1, "output is: %s\n", string(coutput))
	}
}

func TestEmitBadBuildFailure(t *testing.T) {
	testenv.NeedsTool(t, "go")
	if runtime.GOOS == "android" {
		t.Skipf("the dependencies are not available on android")
	}

	td := t.TempDir()
	verb(1, "generating into temp dir %s", td)

	checkTunables(tunables)
	pack := filepath.Base(td)
	s := GenConfig{
		Tag:              "x",
		OutDir:           td,
		PkgPath:          pack,
		NumTestFunctions: 10,
		NumTestPackages:  10,
		MaxFail:          10,
		RandCtl:          RandCtlChecks | RandCtlPanic,
		EmitBad:          1,
	}
	errs := Generate(s)
	if errs != 0 {
		t.Errorf("%d errors during Generate", errs)
	}

	cmd := exec.Command("go", "build", ".")
	cmd.Dir = td
	coutput, cerr := cmd.CombinedOutput()
	if cerr == nil {
		t.Errorf("go build command passed, expected failure. output: %s\n", string(coutput))
	}
}

func TestEmitBadRunFailure(t *testing.T) {
	testenv.NeedsTool(t, "go")
	if runtime.GOOS == "android" {
		t.Skipf("the dependencies are not available on android")
	}

	td := t.TempDir()
	verb(1, "generating into temp dir %s", td)

	checkTunables(tunables)
	pack := filepath.Base(td)
	s := GenConfig{
		Tag:              "x",
		OutDir:           td,
		PkgPath:          pack,
		NumTestFunctions: 10,
		NumTestPackages:  10,
		MaxFail:          10,
		RandCtl:          RandCtlChecks | RandCtlPanic,
		EmitBad:          2,
	}
	errs := Generate(s)
	if errs != 0 {
		t.Errorf("%d errors during Generate", errs)
	}

	// build
	cmd := exec.Command("go", "build", ".")
	cmd.Dir = td
	coutput, cerr := cmd.CombinedOutput()
	if cerr != nil {
		t.Fatalf("build failed: %s\n", string(coutput))
	}

	// run
	cmd = exec.Command("./" + pack)
	cmd.Dir = td
	coutput, cerr = cmd.CombinedOutput()
	if cerr == nil {
		t.Fatalf("run passed, expected failure -- run output: %s", string(coutput))
	}
}
