package stack

import (
	"bufio"
	"fmt"
	"os"
	"strings"

	"sigs.k8s.io/descheduler/tools/junitreport/pkg/api"
	"sigs.k8s.io/descheduler/tools/junitreport/pkg/builder"
	"sigs.k8s.io/descheduler/tools/junitreport/pkg/parser"
)

// NewParser returns a new parser that's capable of parsing Go unit test output
func NewParser(builder builder.TestSuitesBuilder, testParser TestDataParser, suiteParser TestSuiteDataParser, stream bool) parser.TestOutputParser {
	return &testOutputParser{
		builder:     builder,
		testParser:  testParser,
		suiteParser: suiteParser,
		stream:      stream,
	}
}

// testOutputParser uses a stack to parse test output. Critical assumptions that this parser makes are:
//   1 - packages may be nested but tests may not
//   2 - no package declarations will occur within the boundaries of a test
//   3 - all tests and packages are fully bounded by a start and result line
//   4 - if a package or test declaration occurs after the start of a package but before its result,
//       the sub-package's or member test's result line will occur before that of the parent package
//       i.e. any test or package overlap will necessarily mean that one package's lines are a superset
//       of any lines of tests or other packages overlapping with it
//   5 - any text in the input file that doesn't match the parser regex is necessarily the output of the
//       current test being built
type testOutputParser struct {
	builder builder.TestSuitesBuilder

	testParser  TestDataParser
	suiteParser TestSuiteDataParser

	stream bool
}

// Parse parses output syntax of a specific class, the assumptions of which are outlined in the struct definition.
// The specific boundary markers and metadata encodings are free to vary as long as regex can be build to extract them
// from test output.
func (p *testOutputParser) Parse(input *bufio.Scanner) (*api.TestSuites, error) {
	inProgress := NewTestSuiteStack()

	var currentTest *api.TestCase
	var currentResult api.TestResult
	var currentOutput []string
	var currentMessage string

	for input.Scan() {
		line := input.Text()
		isTestOutput := true

		if p.testParser.MarksBeginning(line) {
			currentTest = &api.TestCase{}
			currentResult = api.TestResultFail
			currentOutput = []string{}
			currentMessage = ""
		}

		if name, contained := p.testParser.ExtractName(line); contained {
			currentTest.Name = name
		}

		if result, contained := p.testParser.ExtractResult(line); contained {
			currentResult = result
		}

		if duration, contained := p.testParser.ExtractDuration(line); contained {
			if err := currentTest.SetDuration(duration); err != nil {
				return nil, err
			}
		}

		if message, contained := p.testParser.ExtractMessage(line); contained {
			currentMessage = message
		}

		if p.testParser.MarksCompletion(line) {
			currentOutput = append(currentOutput, line)
			// if we have finished the current test case, we finalize our current test, add it to the package
			// at the head of our in progress package stack, and clear our current test record.
			switch currentResult {
			case api.TestResultSkip:
				currentTest.MarkSkipped(currentMessage)
			case api.TestResultFail:
				output := strings.Join(currentOutput, "\n")
				currentTest.MarkFailed(currentMessage, output)
			}

			if inProgress.Peek() == nil {
				return nil, fmt.Errorf("found test case %q outside of a test suite", currentTest.Name)
			}

			inProgress.Peek().AddTestCase(currentTest)
			currentTest = &api.TestCase{}
		}

		if p.suiteParser.MarksBeginning(line) {
			// if we encounter the beginning of a suite, we create a new suite to be considered and
			// add it to the head of our in progress package stack
			inProgress.Push(&api.TestSuite{})
			isTestOutput = false
		}

		if name, contained := p.suiteParser.ExtractName(line); contained {
			inProgress.Peek().Name = name
			isTestOutput = false
		}

		if properties, contained := p.suiteParser.ExtractProperties(line); contained {
			for propertyName := range properties {
				inProgress.Peek().AddProperty(propertyName, properties[propertyName])
			}
			isTestOutput = false
		}

		if p.suiteParser.MarksCompletion(line) {
			if p.stream {
				fmt.Fprintln(os.Stdout, line)
			}

			// if we encounter the end of a suite, we remove the suite at the head of the in progress stack
			p.builder.AddSuite(inProgress.Pop())
			isTestOutput = false
		}

		// we want to associate every line other than those directly involved with test suites as output of a test case
		if isTestOutput {
			currentOutput = append(currentOutput, line)
		}
	}
	return p.builder.Build(), nil
}
