package builder_test

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

	"github.com/stretchr/testify/require"

	"github.com/cucumber/godog"
	"github.com/cucumber/godog/internal/builder"
)

func InitializeScenario(ctx *godog.ScenarioContext) {}

func Test_GodogBuild(t *testing.T) {
	t.Run("WithSourceNotInGoPath", testWithSourceNotInGoPath)
	t.Run("WithoutSourceNotInGoPath", testWithoutSourceNotInGoPath)
	t.Run("WithoutTestSourceNotInGoPath", testWithoutTestSourceNotInGoPath)
	t.Run("WithinGopath", testWithinGopath)
	t.Run("WithVendoredGodogWithoutModule", testWithVendoredGodogWithoutModule)
	t.Run("WithVendoredGodogAndMod", testWithVendoredGodogAndMod)
	t.Run("WithIncorrectProjectStructure", testWithIncorrectProjectStructure)

	t.Run("WithModule", func(t *testing.T) {
		t.Run("OutsideGopathAndHavingOnlyFeature", testOutsideGopathAndHavingOnlyFeature)
		t.Run("OutsideGopath", testOutsideGopath)
		t.Run("OutsideGopathWithXTest", testOutsideGopathWithXTest)
		t.Run("InsideGopath", testInsideGopath)
	})
}

var builderFeatureFile = `Feature: eat godogs
  In order to be happy
  As a hungry gopher
  I need to be able to eat godogs

  Scenario: Eat 5 out of 12
    Given there are 12 godogs
    When I eat 5
    Then there should be 7 remaining
`

var builderTestFile = `package godogs

import (
	"fmt"

	"github.com/cucumber/godog"
)

func thereAreGodogs(available int) error {
	Godogs = available
	return nil
}

func iEat(num int) error {
	if Godogs < num {
		return fmt.Errorf("you cannot eat %d godogs, there are %d available", num, Godogs)
	}
	Godogs -= num
	return nil
}

func thereShouldBeRemaining(remaining int) error {
	if Godogs != remaining {
		return fmt.Errorf("expected %d godogs to be remaining, but there is %d", remaining, Godogs)
	}
	return nil
}


func InitializeScenario(ctx *godog.ScenarioContext) {
	ctx.Step("^there are (\\d+) godogs$", thereAreGodogs)
	ctx.Step("^I eat (\\d+)$", iEat)
	ctx.Step("^there should be (\\d+) remaining$", thereShouldBeRemaining)

	ctx.BeforeScenario(func(*godog.Scenario) {
		Godogs = 0 // clean the state before every scenario
	})
}
`

var builderXTestFile = `package godogs_test

import (
	"fmt"

	"github.com/cucumber/godog"

	"godogs"
)

func thereAreGodogs(available int) error {
	godogs.Godogs = available
	return nil
}

func iEat(num int) error {
	if godogs.Godogs < num {
		return fmt.Errorf("you cannot eat %d godogs, there are %d available", num, godogs.Godogs)
	}
	godogs.Godogs -= num
	return nil
}

func thereShouldBeRemaining(remaining int) error {
	if godogs.Godogs != remaining {
		return fmt.Errorf("expected %d godogs to be remaining, but there is %d", remaining, godogs.Godogs)
	}
	return nil
}

func InitializeScenario(ctx *godog.ScenarioContext) {
	ctx.Step("^there are (\\d+) godogs$", thereAreGodogs)
	ctx.Step("^I eat (\\d+)$", iEat)
	ctx.Step("^there should be (\\d+) remaining$", thereShouldBeRemaining)

	ctx.BeforeScenario(func(*godog.Scenario) {
		godogs.Godogs = 0 // clean the state before every scenario
	})
}
`

var builderMainCodeFile = `package godogs

// Godogs available to eat
var Godogs int

func main() {
}
`

var emptyBuilderTestFile = `package godogs

import "github.com/cucumber/godog"
 
func InitializeScenario(ctx *godog.ScenarioContext) {}
`

var builderModFile = `module godogs`

func buildTestPackage(dir string, files map[string]string) error {
	if err := os.MkdirAll(dir, 0755); err != nil {
		return err
	}

	for name, content := range files {
		if err := ioutil.WriteFile(filepath.Join(dir, name), []byte(content), 0644); err != nil {
			return err
		}
	}

	return nil
}

func buildTestCommand(t *testing.T, wd, featureFile string) *exec.Cmd {
	testBin := filepath.Join(wd, "godog.test")
	testBin, err := filepath.Abs(testBin)
	require.Nil(t, err)

	if build.Default.GOOS == "windows" {
		testBin += ".exe"
	}

	err = builder.Build(testBin)
	require.Nil(t, err)

	featureFilePath := filepath.Join(wd, featureFile)
	return exec.Command(testBin, featureFilePath)
}

func envVarsWithoutGopath() []string {
	var env []string

	for _, def := range os.Environ() {
		if strings.Index(def, "GOPATH=") == 0 {
			continue
		}

		env = append(env, def)
	}

	return env
}

func testWithSourceNotInGoPath(t *testing.T) {
	dir := filepath.Join(os.TempDir(), t.Name(), "godogs")
	files := map[string]string{
		"godogs.feature": builderFeatureFile,
		"godogs.go":      builderMainCodeFile,
		"godogs_test.go": builderTestFile,
		"go.mod":         builderModFile,
	}

	err := buildTestPackage(dir, files)
	defer os.RemoveAll(dir)
	require.Nil(t, err)

	prevDir, err := os.Getwd()
	require.Nil(t, err)

	err = os.Chdir(dir)
	require.Nil(t, err)
	defer os.Chdir(prevDir)

	testCmd := buildTestCommand(t, "", "godogs.feature")
	testCmd.Env = os.Environ()

	var stdout, stderr bytes.Buffer
	testCmd.Stdout = &stdout
	testCmd.Stderr = &stderr

	err = testCmd.Run()
	require.Nil(t, err, "stdout:\n%s\nstderr:\n%s", stdout.String(), stderr.String())
}

func testWithoutSourceNotInGoPath(t *testing.T) {
	builderTC := builderTestCase{}

	builderTC.dir = filepath.Join(os.TempDir(), t.Name(), "godogs")
	builderTC.files = map[string]string{
		"godogs.feature": builderFeatureFile,
		"go.mod":         builderModFile,
	}

	builderTC.run(t)
}

func testWithoutTestSourceNotInGoPath(t *testing.T) {
	builderTC := builderTestCase{}

	builderTC.dir = filepath.Join(os.TempDir(), t.Name(), "godogs")
	builderTC.files = map[string]string{
		"godogs.feature": builderFeatureFile,
		"godogs.go":      builderMainCodeFile,
		"go.mod":         builderModFile,
	}

	builderTC.run(t)
}

func testWithIncorrectProjectStructure(t *testing.T) {
	dir := filepath.Join(os.TempDir(), t.Name(), "godogs")
	files := map[string]string{
		"godogs.go": emptyBuilderTestFile,
		"go.mod":    builderModFile,
	}

	err := buildTestPackage(dir, files)
	defer os.RemoveAll(dir)
	require.Nil(t, err)

	prevDir, err := os.Getwd()
	require.Nil(t, err)
	err = os.Chdir(dir)
	require.Nil(t, err)
	defer os.Chdir(prevDir)

	testBin, err := filepath.Abs(filepath.Join(dir, "godog.test"))
	require.Nil(t, err)

	if build.Default.GOOS == "windows" {
		testBin += ".exe"
	}

	// call the builder - we should get an error
	err = builder.Build(testBin)
	// check that we even got an error at all
	require.NotNil(t, err)
	// now check the details of the error message
	require.EqualError(t, err, "incorrect project structure: no test files found")
}

func testWithinGopath(t *testing.T) {
	builderTC := builderTestCase{}

	gopath := filepath.Join(os.TempDir(), t.Name(), "_gp")
	defer os.RemoveAll(gopath)

	builderTC.dir = filepath.Join(gopath, "src", "godogs")
	builderTC.files = map[string]string{
		"godogs.feature": builderFeatureFile,
		"godogs.go":      builderMainCodeFile,
		"godogs_test.go": builderTestFile,
		"go.mod":         builderModFile,
	}

	pkg := filepath.Join(gopath, "src", "github.com", "cucumber")
	err := os.MkdirAll(pkg, 0755)
	require.Nil(t, err)

	wd, err := os.Getwd()
	require.Nil(t, err)

	// symlink godog package
	err = os.Symlink(wd, filepath.Join(pkg, "godog"))
	require.Nil(t, err)

	builderTC.testCmdEnv = []string{"GOPATH=" + gopath}
	builderTC.run(t)
}

func testWithVendoredGodogWithoutModule(t *testing.T) {
	builderTC := builderTestCase{}

	gopath := filepath.Join(os.TempDir(), t.Name(), "_gp")
	defer os.RemoveAll(gopath)

	builderTC.dir = filepath.Join(gopath, "src", "godogs")
	builderTC.files = map[string]string{
		"godogs.feature": builderFeatureFile,
	}

	pkg := filepath.Join(builderTC.dir, "vendor", "github.com", "cucumber")
	err := os.MkdirAll(pkg, 0755)
	require.Nil(t, err)

	wd, err := os.Getwd()
	require.Nil(t, err)

	// symlink godog package
	err = os.Symlink(wd, filepath.Join(pkg, "godog"))
	require.Nil(t, err)

	builderTC.testCmdEnv = append(envVarsWithoutGopath(), "GOPATH="+gopath)
	builderTC.run(t)
}

type builderTestCase struct {
	dir        string
	files      map[string]string
	goModCmds  []*exec.Cmd
	testCmdEnv []string
}

func (bt builderTestCase) run(t *testing.T) {
	err := buildTestPackage(bt.dir, bt.files)
	defer os.RemoveAll(bt.dir)
	require.Nil(t, err)

	for _, c := range bt.goModCmds {
		c.Dir = bt.dir
		out, err := c.CombinedOutput()
		require.Nil(t, err, "%s", string(out))
	}

	testCmd := buildTestCommand(t, bt.dir, "godogs.feature")
	testCmd.Env = append(os.Environ(), bt.testCmdEnv...)

	var stdout, stderr bytes.Buffer
	testCmd.Stdout = &stdout
	testCmd.Stderr = &stderr

	err = testCmd.Run()
	require.Nil(t, err, "stdout:\n%s\nstderr:\n%s", stdout.String(), stderr.String())
}
