package interp_test

import (
	"go/build"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"

	"github.com/traefik/yaegi/interp"
	"github.com/traefik/yaegi/stdlib"
	"github.com/traefik/yaegi/stdlib/unsafe"
)

func TestInterpConsistencyBuild(t *testing.T) {
	if testing.Short() {
		t.Skip("short mode")
	}
	dir := filepath.Join("..", "_test", "tmp")
	if _, err := os.Stat(dir); os.IsNotExist(err) {
		if err := os.Mkdir(dir, 0700); err != nil {
			t.Fatal(err)
		}
	}

	baseDir := filepath.Join("..", "_test")
	files, err := ioutil.ReadDir(baseDir)
	if err != nil {
		t.Fatal(err)
	}

	for _, file := range files {
		if filepath.Ext(file.Name()) != ".go" ||
			file.Name() == "assign11.go" || // expect error
			file.Name() == "assign12.go" || // expect error
			file.Name() == "assign15.go" || // expect error
			file.Name() == "bad0.go" || // expect error
			file.Name() == "const9.go" || // expect error
			file.Name() == "export1.go" || // non-main package
			file.Name() == "export0.go" || // non-main package
			file.Name() == "for7.go" || // expect error
			file.Name() == "fun21.go" || // expect error
			file.Name() == "fun22.go" || // expect error
			file.Name() == "fun23.go" || // expect error
			file.Name() == "fun24.go" || // expect error
			file.Name() == "fun25.go" || // expect error
			file.Name() == "if2.go" || // expect error
			file.Name() == "import6.go" || // expect error
			file.Name() == "init1.go" || // expect error
			file.Name() == "io0.go" || // use random number
			file.Name() == "op1.go" || // expect error
			file.Name() == "op7.go" || // expect error
			file.Name() == "op9.go" || // expect error
			file.Name() == "bltn0.go" || // expect error
			file.Name() == "method16.go" || // private struct field
			file.Name() == "switch8.go" || // expect error
			file.Name() == "switch9.go" || // expect error
			file.Name() == "switch13.go" || // expect error
			file.Name() == "switch19.go" || // expect error
			file.Name() == "time0.go" || // display time (similar to random number)
			file.Name() == "factor.go" || // bench
			file.Name() == "fib.go" || // bench

			file.Name() == "type5.go" || // used to illustrate a limitation with no workaround, related to the fact that the reflect package does not allow the creation of named types
			file.Name() == "type6.go" || // used to illustrate a limitation with no workaround, related to the fact that the reflect package does not allow the creation of named types

			file.Name() == "redeclaration0.go" || // expect error
			file.Name() == "redeclaration1.go" || // expect error
			file.Name() == "redeclaration2.go" || // expect error
			file.Name() == "redeclaration3.go" || // expect error
			file.Name() == "redeclaration4.go" || // expect error
			file.Name() == "redeclaration5.go" || // expect error
			file.Name() == "redeclaration-global0.go" || // expect error
			file.Name() == "redeclaration-global1.go" || // expect error
			file.Name() == "redeclaration-global2.go" || // expect error
			file.Name() == "redeclaration-global3.go" || // expect error
			file.Name() == "redeclaration-global4.go" || // expect error
			file.Name() == "redeclaration-global5.go" || // expect error
			file.Name() == "redeclaration-global6.go" || // expect error
			file.Name() == "redeclaration-global7.go" || // expect error
			file.Name() == "pkgname0.go" || // has deps
			file.Name() == "pkgname1.go" || // expect error
			file.Name() == "pkgname2.go" || // has deps
			file.Name() == "ipp_as_key.go" || // has deps
			file.Name() == "restricted0.go" || // expect error
			file.Name() == "restricted1.go" || // expect error
			file.Name() == "restricted2.go" || // expect error
			file.Name() == "restricted3.go" || // expect error
			file.Name() == "server6.go" || // syntax parsing
			file.Name() == "server5.go" || // syntax parsing
			file.Name() == "server4.go" || // syntax parsing
			file.Name() == "server3.go" || // syntax parsing
			file.Name() == "server2.go" || // syntax parsing
			file.Name() == "server1a.go" || // syntax parsing
			file.Name() == "server1.go" || // syntax parsing
			file.Name() == "server0.go" || // syntax parsing
			file.Name() == "server.go" || // syntax parsing
			file.Name() == "range9.go" { // expect error
			continue
		}

		file := file
		t.Run(file.Name(), func(t *testing.T) {
			filePath := filepath.Join(baseDir, file.Name())

			// catch stdout
			backupStdout := os.Stdout
			defer func() {
				os.Stdout = backupStdout
			}()
			r, w, _ := os.Pipe()
			os.Stdout = w

			i := interp.New(interp.Options{GoPath: build.Default.GOPATH})
			i.Use(stdlib.Symbols)
			i.Use(interp.Symbols)
			i.Use(unsafe.Symbols)

			_, err = i.EvalPath(filePath)
			if err != nil {
				t.Fatal(err)
			}

			// read stdout
			if err = w.Close(); err != nil {
				t.Fatal(err)
			}
			outInterp, err := ioutil.ReadAll(r)
			if err != nil {
				t.Fatal(err)
			}

			// restore Stdout
			os.Stdout = backupStdout

			bin := filepath.Join(dir, strings.TrimSuffix(file.Name(), ".go"))

			cmdBuild := exec.Command("go", "build", "-tags=dummy", "-o", bin, filePath)
			outBuild, err := cmdBuild.CombinedOutput()
			if err != nil {
				t.Log(string(outBuild))
				t.Fatal(err)
			}

			cmd := exec.Command(bin)
			outRun, err := cmd.CombinedOutput()
			if err != nil {
				t.Log(string(outRun))
				t.Fatal(err)
			}

			if string(outInterp) != string(outRun) {
				t.Errorf("\nGot: %q,\n want: %q", string(outInterp), string(outRun))
			}
		})
	}
}

func TestInterpErrorConsistency(t *testing.T) {
	testCases := []struct {
		fileName       string
		expectedInterp string
		expectedExec   string
	}{
		{
			fileName:       "assign11.go",
			expectedInterp: "6:2: assignment mismatch: 3 variables but fmt.Println returns 2 values",
			expectedExec:   "6:10: assignment mismatch: 3 variables but fmt.Println returns 2 values",
		},
		{
			fileName:       "assign12.go",
			expectedInterp: "6:2: assignment mismatch: 3 variables but fmt.Println returns 2 values",
			expectedExec:   "6:10: assignment mismatch: 3 variables but fmt.Println returns 2 values",
		},
		{
			fileName:       "bad0.go",
			expectedInterp: "1:1: expected 'package', found println",
			expectedExec:   "1:1: expected 'package', found println",
		},
		{
			fileName:       "const9.go",
			expectedInterp: "5:2: constant definition loop",
			expectedExec:   "5:2: constant definition loop",
		},
		{
			fileName:       "if2.go",
			expectedInterp: "7:5: non-bool used as if condition",
			expectedExec:   "7:2: non-bool i % 1000000 (type int) used as if condition",
		},
		{
			fileName:       "for7.go",
			expectedInterp: "4:14: non-bool used as for condition",
			expectedExec:   "4:2: non-bool i (type int) used as for condition",
		},
		{
			fileName:       "fun21.go",
			expectedInterp: "4:2: not enough arguments to return",
			expectedExec:   "4:2: not enough arguments to return",
		},
		{
			fileName:       "fun22.go",
			expectedInterp: "6:2: not enough arguments in call to time.Date",
			expectedExec:   "6:11: not enough arguments in call to time.Date",
		},
		{
			fileName:       "fun23.go",
			expectedInterp: "3:17: too many arguments to return",
			expectedExec:   "3:17: too many arguments to return",
		},
		{
			fileName:       "op1.go",
			expectedInterp: "5:2: invalid operation: mismatched types int and float64",
			expectedExec:   "5:4: constant 1.3 truncated to integer",
		},
		{
			fileName:       "bltn0.go",
			expectedInterp: "4:7: use of builtin println not in function call",
		},
		{
			fileName:       "import6.go",
			expectedInterp: "import cycle not allowed",
			expectedExec:   "import cycle not allowed",
		},
		{
			fileName:       "switch8.go",
			expectedInterp: "5:2: fallthrough statement out of place",
			expectedExec:   "5:2: fallthrough statement out of place",
		},
		{
			fileName:       "switch9.go",
			expectedInterp: "9:3: cannot fallthrough in type switch",
			expectedExec:   "9:3: cannot fallthrough in type switch",
		},
		{
			fileName:       "switch13.go",
			expectedInterp: "9:2: i is not a type",
			expectedExec:   "9:2: i (type interface {}) is not a type",
		},
		{
			fileName:       "switch19.go",
			expectedInterp: "37:2: duplicate case Bir in type switch",
			expectedExec:   "37:2: duplicate case Bir in type switch",
		},
	}

	for _, test := range testCases {
		t.Run(test.fileName, func(t *testing.T) {
			if len(test.expectedInterp) == 0 && len(test.expectedExec) == 0 {
				t.Fatal("at least expectedInterp must be define")
			}

			filePath := filepath.Join("..", "_test", test.fileName)

			i := interp.New(interp.Options{GoPath: build.Default.GOPATH})
			i.Use(stdlib.Symbols)

			_, errEval := i.EvalPath(filePath)
			if errEval == nil {
				t.Fatal("An error is expected but got none.")
			}

			if !strings.Contains(errEval.Error(), test.expectedInterp) {
				t.Errorf("got %q, want: %q", errEval.Error(), test.expectedInterp)
			}

			cmd := exec.Command("go", "run", filePath)
			outRun, errExec := cmd.CombinedOutput()
			if errExec == nil {
				t.Log(string(outRun))
				t.Fatal("An error is expected but got none.")
			}

			if len(test.expectedExec) == 0 && !strings.Contains(string(outRun), test.expectedInterp) {
				t.Errorf("got %q, want: %q", string(outRun), test.expectedInterp)
			} else if !strings.Contains(string(outRun), test.expectedExec) {
				t.Errorf("got %q, want: %q", string(outRun), test.expectedExec)
			}
		})
	}
}
