// Copyright 2016 Attic Labs, Inc. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0

package runner

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	"github.com/stretchr/testify/suite"
)

const (
	boilerplate = `
from __future__ import print_function
import os, sys

%s
`
	buildFileBasename = "build.py"
)

func TestSerialRunnerTestSuite(t *testing.T) {
	suite.Run(t, &SerialRunnerTestSuite{})
}

type SerialRunnerTestSuite struct {
	suite.Suite
	dir   string
	index int
}

func (suite *SerialRunnerTestSuite) SetupTest() {
	var err error
	suite.dir, err = ioutil.TempDir(os.TempDir(), "")
	suite.NoError(err)
}

func (suite *SerialRunnerTestSuite) TearDownTest() {
	os.Remove(suite.dir)
}

func (suite *SerialRunnerTestSuite) TestForceRunInDir() {
	scriptPath := filepath.Join(suite.dir, buildFileBasename)
	suite.makeTestBuildFile(scriptPath, []string{"print(os.getcwd(), file=sys.stdout)"})

	old := os.Stdout // keep backup of the real stdout
	r, w, err := os.Pipe()
	suite.NoError(err)
	os.Stdout = w
	defer func() { os.Stdout = old }()
	defer r.Close()

	outC := make(chan string)
	// copy the output in a separate goroutine so printing can't block indefinitely
	go func() {
		buf := &bytes.Buffer{}
		io.Copy(buf, r)
		outC <- buf.String()
	}()

	ForceRunInDir(suite.dir, nil, "python", scriptPath)

	w.Close()
	out := strings.TrimSpace(<-outC)
	actualSuiteDir, err := filepath.EvalSymlinks(suite.dir)
	suite.NoError(err)
	suite.Equal(actualSuiteDir, out)
}

func (suite *SerialRunnerTestSuite) TestRunInDir() {
	scriptPath := filepath.Join(suite.dir, buildFileBasename)
	suite.makeTestBuildFile(scriptPath, []string{
		"print(os.getcwd(), file=sys.stdout)",
		"print('error', file=sys.stderr)",
	})

	stdout := &bytes.Buffer{}
	stderr := &bytes.Buffer{}
	RunInDir(stdout, stderr, suite.dir, "python", scriptPath)
	actualSuiteDir, err := filepath.EvalSymlinks(suite.dir)
	suite.NoError(err)
	suite.Equal(actualSuiteDir, strings.TrimSpace(string(stdout.Bytes())))
	suite.Equal("error", strings.TrimSpace(string(stderr.Bytes())))
}

func (suite *SerialRunnerTestSuite) TestEnvVars() {
	makeEnvVarPrintBuildFile := func(path, varname string) {
		fmtStatement := fmt.Sprintf(`print(os.environ['%s'], file=sys.stdout)`, varname)
		suite.makeTestBuildFile(path, []string{fmtStatement})
	}

	type testCase struct {
		path, varname, expected string
	}
	env := Env{
		"PATH":                os.Getenv("PATH"),
		"GOPATH":              os.Getenv("GOPATH"),
		"NOMS_CHECKOUT_PATH":  "/where/noms/is",
		"ATTIC_CHECKOUT_PATH": "/where/attic/is",
	}
	tests := []testCase{}
	for n, v := range env {
		tc := testCase{suite.uniqueBuildFile(), n, v}
		makeEnvVarPrintBuildFile(tc.path, tc.varname)
		tests = append(tests, tc)
	}
	gorootTestCase := testCase{suite.uniqueBuildFile(), "GOROOT", runtime.GOROOT()}
	makeEnvVarPrintBuildFile(gorootTestCase.path, gorootTestCase.varname)
	tests = append(tests, gorootTestCase)

	log := &bytes.Buffer{}
	if suite.True(Serial(log, log, env, suite.dir, buildFileBasename), "Serial() should have succeeded! logs:\n%s", string(log.Bytes())) {
		logText := string(log.Bytes())
		for _, tc := range tests {
			suite.Contains(logText, tc.expected)
		}
	}
}

func (suite *SerialRunnerTestSuite) TestFailure() {
	type testCase struct {
		path, expected string
	}
	tests := []testCase{
		{suite.uniqueBuildFile(), "Scoobaz"},
		{suite.uniqueBuildFile(), "at the disco"},
	}
	goodOne := testCase{suite.uniqueBuildFile(), "All's well"}

	suite.makeTestBuildFile(tests[0].path, []string{"Scoobaz() # Won't compile."})
	suite.makeTestBuildFile(tests[1].path, []string{`assert(False, "at the disco") # Won't run.`})
	suite.makeTestBuildFile(goodOne.path, []string{fmt.Sprintf(`print "%s"`, goodOne.expected)})

	log := &bytes.Buffer{}
	suite.False(Serial(log, log, Env{}, suite.dir, buildFileBasename))
	logText := string(log.Bytes())
	suite.Contains(logText, tests[0].expected)
	suite.Contains(logText, tests[1].expected)
}

func (suite *SerialRunnerTestSuite) uniqueBuildFile() string {
	suite.index++
	return filepath.Join(suite.dir, fmt.Sprintf("%d", suite.index), buildFileBasename)
}

func (suite *SerialRunnerTestSuite) makeTestBuildFile(path string, statements []string) {
	buf := &bytes.Buffer{}
	fmt.Fprintf(buf, boilerplate, strings.Join(statements, "\n"))
	err := os.MkdirAll(filepath.Dir(path), 0777)
	suite.NoError(err)
	err = ioutil.WriteFile(path, buf.Bytes(), 0755)
	suite.NoError(err)
}
