package parser

import (
	"fmt"
	"regexp"
	"sort"
	"strconv"
	"strings"

	"github.com/smartystreets/goconvey/web/server/contract"
)

var (
	testNamePattern = regexp.MustCompile("^=== RUN:? +(.+)$")
)

func ParsePackageResults(result *contract.PackageResult, rawOutput string) {
	newOutputParser(result, rawOutput).parse()
}

type outputParser struct {
	raw    string
	lines  []string
	result *contract.PackageResult
	tests  []*contract.TestResult

	// place holders for loops
	line    string
	test    *contract.TestResult
	testMap map[string]*contract.TestResult
}

func newOutputParser(result *contract.PackageResult, rawOutput string) *outputParser {
	self := new(outputParser)
	self.raw = strings.TrimSpace(rawOutput)
	self.lines = strings.Split(self.raw, "\n")
	self.result = result
	self.tests = []*contract.TestResult{}
	self.testMap = make(map[string]*contract.TestResult)
	return self
}

func (self *outputParser) parse() {
	self.separateTestFunctionsAndMetadata()
	self.parseEachTestFunction()
}

func (self *outputParser) separateTestFunctionsAndMetadata() {
	for _, self.line = range self.lines {
		if self.processNonTestOutput() {
			break
		}
		// Hack for results from ginkgo tests
		lines := strings.Split(self.line, " --- ")
		if len(lines) == 2 && len(strings.TrimSpace(lines[0])) > 0 && strings.HasPrefix(lines[1], "PASS") {
			self.line = lines[0]
			self.processTestOutput()
			self.line = "--- " + lines[1]
			self.processTestOutput()
			continue
		}
		self.processTestOutput()
	}
}
func (self *outputParser) processNonTestOutput() bool {
	if noGoFiles(self.line) {
		self.recordFinalOutcome(contract.NoGoFiles)

	} else if buildFailed(self.line) {
		self.recordFinalOutcome(contract.BuildFailure)

	} else if noTestFiles(self.line) {
		self.recordFinalOutcome(contract.NoTestFiles)

	} else if noTestFunctions(self.line) {
		self.recordFinalOutcome(contract.NoTestFunctions)

	} else {
		return false
	}
	return true
}

func (self *outputParser) recordFinalOutcome(outcome string) {
	self.result.Outcome = outcome
	self.result.BuildOutput = strings.Join(self.lines, "\n")
}

func (self *outputParser) processTestOutput() {
	self.line = strings.TrimSpace(self.line)
	if isNewTest(self.line) {
		self.registerTestFunction()

	} else if isTestResult(self.line) {
		self.recordTestMetadata()

	} else if isPackageReport(self.line) {
		self.recordPackageMetadata()

	} else {
		self.saveLineForParsingLater()

	}
}

func (self *outputParser) registerTestFunction() {
	testNameReg := testNamePattern.FindStringSubmatch(self.line)
	if len(testNameReg) < 2 { // Test-related lines that aren't about a new test
		return
	}
	self.test = contract.NewTestResult(testNameReg[1])
	self.tests = append(self.tests, self.test)
	self.testMap[self.test.TestName] = self.test
}
func (self *outputParser) recordTestMetadata() {
	testName := strings.Split(self.line, " ")[2]
	if test, ok := self.testMap[testName]; ok {
		self.test = test
		self.test.Passed = !strings.HasPrefix(self.line, "--- FAIL: ")
		self.test.Skipped = strings.HasPrefix(self.line, "--- SKIP: ")
		self.test.Elapsed = parseTestFunctionDuration(self.line)
	}
}
func (self *outputParser) recordPackageMetadata() {
	if packageFailed(self.line) {
		self.recordTestingOutcome(contract.Failed)

	} else if packagePassed(self.line) {
		self.recordTestingOutcome(contract.Passed)

	} else if isCoverageSummary(self.line) {
		self.recordCoverageSummary(self.line)
	}
}
func (self *outputParser) recordTestingOutcome(outcome string) {
	self.result.Outcome = outcome
	fields := strings.Split(self.line, "\t")
	self.result.PackageName = strings.TrimSpace(fields[1])
	self.result.Elapsed = parseDurationInSeconds(fields[2], 3)
}
func (self *outputParser) recordCoverageSummary(summary string) {
	start := len("coverage: ")
	end := strings.Index(summary, "%")
	value := summary[start:end]
	parsed, err := strconv.ParseFloat(value, 64)
	if err != nil {
		self.result.Coverage = -1
	} else {
		self.result.Coverage = parsed
	}
}
func (self *outputParser) saveLineForParsingLater() {
	self.line = strings.TrimLeft(self.line, "\t")
	if self.test == nil {
		fmt.Println("Potential error parsing output of", self.result.PackageName, "; couldn't handle this stray line:", self.line)
		return
	}
	self.test.RawLines = append(self.test.RawLines, self.line)
}

// TestResults is a collection of TestResults that implements sort.Interface.
type TestResults []contract.TestResult

func (r TestResults) Len() int {
	return len(r)
}

// Less compares TestResults on TestName
func (r TestResults) Less(i, j int) bool {
	return r[i].TestName < r[j].TestName
}

func (r TestResults) Swap(i, j int) {
	r[i], r[j] = r[j], r[i]
}

func (self *outputParser) parseEachTestFunction() {
	for _, self.test = range self.tests {
		self.test = parseTestOutput(self.test)
		if self.test.Error != "" {
			self.result.Outcome = contract.Panicked
		}
		self.test.RawLines = []string{}
		self.result.TestResults = append(self.result.TestResults, *self.test)
	}
	sort.Sort(TestResults(self.result.TestResults))
}
