// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
// Revel Framework source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.

package main

import (
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/revel/cmd/harness"
	"github.com/revel/modules/testrunner/app/controllers"
	"github.com/revel/revel"
)

var cmdTest = &Command{
	UsageLine: "test [import path] [run mode] [suite.method]",
	Short:     "run all tests from the command-line",
	Long: `
Run all tests for the Revel app named by the given import path.

For example, to run the booking sample application's tests:

    revel test github.com/revel/examples/booking dev

The run mode is used to select which set of app.conf configuration should
apply and may be used to determine logic in the application itself.

Run mode defaults to "dev".

You can run a specific suite (and function) by specifying a third parameter.
For example, to run all of UserTest:

    revel test outspoken test UserTest

or one of UserTest's methods:

    revel test outspoken test UserTest.Test1
`,
}

func init() {
	cmdTest.Run = testApp
}

func testApp(args []string) {
	var err error
	if len(args) == 0 {
		errorf("No import path given.\nRun 'revel help test' for usage.\n")
	}

	mode := DefaultRunMode
	if len(args) >= 2 {
		mode = args[1]
	}

	// Find and parse app.conf
	revel.Init(mode, args[0], "")

	// Ensure that the testrunner is loaded in this mode.
	checkTestRunner()

	// Create a directory to hold the test result files.
	resultPath := filepath.Join(revel.BasePath, "test-results")
	if err = os.RemoveAll(resultPath); err != nil {
		errorf("Failed to remove test result directory %s: %s", resultPath, err)
	}
	if err = os.Mkdir(resultPath, 0777); err != nil {
		errorf("Failed to create test result directory %s: %s", resultPath, err)
	}

	// Direct all the output into a file in the test-results directory.
	file, err := os.OpenFile(filepath.Join(resultPath, "app.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		errorf("Failed to create test result log file: %s", err)
	}

	app, reverr := harness.Build()
	if reverr != nil {
		errorf("Error building: %s", reverr)
	}
	cmd := app.Cmd()
	cmd.Stderr = io.MultiWriter(cmd.Stderr, file)
	cmd.Stdout = io.MultiWriter(cmd.Stderr, file)

	// Start the app...
	if err := cmd.Start(); err != nil {
		errorf("%s", err)
	}
	defer cmd.Kill()
	revel.INFO.Printf("Testing %s (%s) in %s mode\n", revel.AppName, revel.ImportPath, mode)

	var httpAddr = revel.HTTPAddr
	if httpAddr == "" {
		httpAddr = "127.0.0.1"
	}

	var httpProto = "http"
	if revel.HTTPSsl {
		httpProto = "https"
	}

	// Get a list of tests
	var baseURL = fmt.Sprintf("%s://%s:%d", httpProto, httpAddr, revel.HTTPPort)
	testSuites, _ := getTestsList(baseURL)

	// If a specific TestSuite[.Method] is specified, only run that suite/test
	if len(args) == 3 {
		testSuites = filterTestSuites(testSuites, args[2])
	}
	testSuiteCount := len(*testSuites)
	fmt.Printf("\n%d test suite%s to run.\n", testSuiteCount, pluralize(testSuiteCount, "", "s"))
	fmt.Println()

	// Run each suite.
	failedResults, overallSuccess := runTestSuites(baseURL, resultPath, testSuites)

	fmt.Println()
	if overallSuccess {
		writeResultFile(resultPath, "result.passed", "passed")
		fmt.Println("All Tests Passed.")
	} else {
		for _, failedResult := range *failedResults {
			fmt.Printf("Failures:\n")
			for _, result := range failedResult.Results {
				if !result.Passed {
					fmt.Printf("%s.%s\n", failedResult.Name, result.Name)
					fmt.Printf("%s\n\n", result.ErrorSummary)
				}
			}
		}
		writeResultFile(resultPath, "result.failed", "failed")
		errorf("Some tests failed.  See file://%s for results.", resultPath)
	}
}

func writeResultFile(resultPath, name, content string) {
	if err := ioutil.WriteFile(filepath.Join(resultPath, name), []byte(content), 0666); err != nil {
		errorf("Failed to write result file %s: %s", filepath.Join(resultPath, name), err)
	}
}

func pluralize(num int, singular, plural string) string {
	if num == 1 {
		return singular
	}
	return plural
}

// Filters test suites and individual tests to match
// the parsed command line parameter
func filterTestSuites(suites *[]controllers.TestSuiteDesc, suiteArgument string) *[]controllers.TestSuiteDesc {
	var suiteName, testName string
	argArray := strings.Split(suiteArgument, ".")
	suiteName = argArray[0]
	if suiteName == "" {
		return suites
	}
	if len(argArray) == 2 {
		testName = argArray[1]
	}
	for _, suite := range *suites {
		if suite.Name != suiteName {
			continue
		}
		if testName == "" {
			return &[]controllers.TestSuiteDesc{suite}
		}
		// Only run a particular test in a suite
		for _, test := range suite.Tests {
			if test.Name != testName {
				continue
			}
			return &[]controllers.TestSuiteDesc{
				{
					Name:  suite.Name,
					Tests: []controllers.TestDesc{test},
				},
			}
		}
		errorf("Couldn't find test %s in suite %s", testName, suiteName)
	}
	errorf("Couldn't find test suite %s", suiteName)
	return nil
}

func checkTestRunner() {
	testRunnerFound := false
	for _, module := range revel.Modules {
		if module.ImportPath == revel.Config.StringDefault("module.testrunner", "github.com/revel/modules/testrunner") {
			testRunnerFound = true
			break
		}
	}

	if !testRunnerFound {
		errorf(`Error: The testrunner module is not running.

You can add it to a run mode configuration with the following line:

	module.testrunner = github.com/revel/modules/testrunner

`)
	}
}

// Get a list of tests from server.
// Since this is the first request to the server, retry/sleep a couple times
// in case it hasn't finished starting up yet.
func getTestsList(baseURL string) (*[]controllers.TestSuiteDesc, error) {
	var (
		err        error
		resp       *http.Response
		testSuites []controllers.TestSuiteDesc
	)
	for i := 0; ; i++ {
		if resp, err = http.Get(baseURL + "/@tests.list"); err == nil {
			if resp.StatusCode == http.StatusOK {
				break
			}
		}
		if i < 3 {
			time.Sleep(3 * time.Second)
			continue
		}
		if err != nil {
			errorf("Failed to request test list: %s", err)
		} else {
			errorf("Failed to request test list: non-200 response")
		}
	}
	defer func() {
		_ = resp.Body.Close()
	}()

	err = json.NewDecoder(resp.Body).Decode(&testSuites)

	return &testSuites, err
}

func runTestSuites(baseURL, resultPath string, testSuites *[]controllers.TestSuiteDesc) (*[]controllers.TestSuiteResult, bool) {
	// Load the result template, which we execute for each suite.
	module, _ := revel.ModuleByName("testrunner")
	TemplateLoader := revel.NewTemplateLoader([]string{filepath.Join(module.Path, "app", "views")})
	if err := TemplateLoader.Refresh(); err != nil {
		errorf("Failed to compile templates: %s", err)
	}
	resultTemplate, err := TemplateLoader.Template("TestRunner/SuiteResult.html")
	if err != nil {
		errorf("Failed to load suite result template: %s", err)
	}

	var (
		overallSuccess = true
		failedResults  []controllers.TestSuiteResult
	)
	for _, suite := range *testSuites {
		// Print the name of the suite we're running.
		name := suite.Name
		if len(name) > 22 {
			name = name[:19] + "..."
		}
		fmt.Printf("%-22s", name)

		// Run every test.
		startTime := time.Now()
		suiteResult := controllers.TestSuiteResult{Name: suite.Name, Passed: true}
		for _, test := range suite.Tests {
			testURL := baseURL + "/@tests/" + suite.Name + "/" + test.Name
			resp, err := http.Get(testURL)
			if err != nil {
				errorf("Failed to fetch test result at url %s: %s", testURL, err)
			}
			defer func() {
				_ = resp.Body.Close()
			}()

			var testResult controllers.TestResult
			err = json.NewDecoder(resp.Body).Decode(&testResult)
			if err == nil && !testResult.Passed {
				suiteResult.Passed = false
			}
			suiteResult.Results = append(suiteResult.Results, testResult)
		}
		overallSuccess = overallSuccess && suiteResult.Passed

		// Print result.  (Just PASSED or FAILED, and the time taken)
		suiteResultStr, suiteAlert := "PASSED", ""
		if !suiteResult.Passed {
			suiteResultStr, suiteAlert = "FAILED", "!"
			failedResults = append(failedResults, suiteResult)
		}
		fmt.Printf("%8s%3s%6ds\n", suiteResultStr, suiteAlert, int(time.Since(startTime).Seconds()))
		// Create the result HTML file.
		suiteResultFilename := filepath.Join(resultPath,
			fmt.Sprintf("%s.%s.html", suite.Name, strings.ToLower(suiteResultStr)))
		suiteResultFile, err := os.Create(suiteResultFilename)
		if err != nil {
			errorf("Failed to create result file %s: %s", suiteResultFilename, err)
		}
		if err = resultTemplate.Render(suiteResultFile, suiteResult); err != nil {
			errorf("Failed to render result template: %s", err)
		}
	}

	return &failedResults, overallSuccess
}