package godog

import (
	"fmt"
	"io"
	"os"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"sync"

	"github.com/cucumber/godog/colors"
)

const (
	exitSuccess int = iota
	exitFailure
	exitOptionError
)

type initializer func(*Suite)

type runner struct {
	randomSeed            int64
	stopOnFailure, strict bool
	features              []*feature
	fmt                   Formatter
	initializer           initializer
}

func (r *runner) concurrent(rate int, formatterFn func() Formatter) (failed bool) {
	var useFmtCopy bool
	var copyLock sync.Mutex

	// special mode for concurrent-formatter
	if _, ok := r.fmt.(ConcurrentFormatter); ok {
		useFmtCopy = true
	}

	queue := make(chan int, rate)
	for i, ft := range r.features {
		queue <- i // reserve space in queue
		ft := *ft

		go func(fail *bool, feat *feature) {
			var fmtCopy Formatter
			defer func() {
				<-queue // free a space in queue
			}()
			if r.stopOnFailure && *fail {
				return
			}
			suite := &Suite{
				fmt:           r.fmt,
				randomSeed:    r.randomSeed,
				stopOnFailure: r.stopOnFailure,
				strict:        r.strict,
				features:      []*feature{feat},
			}
			if useFmtCopy {
				fmtCopy = formatterFn()
				suite.fmt = fmtCopy

				concurrentDestFmt, dOk := fmtCopy.(ConcurrentFormatter)
				concurrentSourceFmt, sOk := r.fmt.(ConcurrentFormatter)

				if dOk && sOk {
					concurrentDestFmt.Sync(concurrentSourceFmt)
				}
			} else {
				suite.fmt = r.fmt
			}

			r.initializer(suite)
			suite.run()
			if suite.failed {
				*fail = true
			}
			if useFmtCopy {
				copyLock.Lock()

				concurrentDestFmt, dOk := r.fmt.(ConcurrentFormatter)
				concurrentSourceFmt, sOk := fmtCopy.(ConcurrentFormatter)

				if dOk && sOk {
					concurrentDestFmt.Copy(concurrentSourceFmt)
				} else if !dOk {
					panic("cant cast dest formatter to progress-typed")
				} else if !sOk {
					panic("cant cast source formatter to progress-typed")
				}

				copyLock.Unlock()
			}
		}(&failed, &ft)
	}
	// wait until last are processed
	for i := 0; i < rate; i++ {
		queue <- i
	}
	close(queue)

	// print summary
	r.fmt.Summary()
	return
}

func (r *runner) run() bool {
	suite := &Suite{
		fmt:           r.fmt,
		randomSeed:    r.randomSeed,
		stopOnFailure: r.stopOnFailure,
		strict:        r.strict,
		features:      r.features,
	}
	r.initializer(suite)
	suite.run()

	r.fmt.Summary()
	return suite.failed
}

// RunWithOptions is same as Run function, except
// it uses Options provided in order to run the
// test suite without parsing flags
//
// This method is useful in case if you run
// godog in for example TestMain function together
// with go tests
//
// The exit codes may vary from:
//  0 - success
//  1 - failed
//  2 - command line usage error
//  128 - or higher, os signal related error exit codes
//
// If there are flag related errors they will
// be directed to os.Stderr
func RunWithOptions(suite string, contextInitializer func(suite *Suite), opt Options) int {
	var output io.Writer = os.Stdout
	if nil != opt.Output {
		output = opt.Output
	}

	if opt.NoColors {
		output = colors.Uncolored(output)
	} else {
		output = colors.Colored(output)
	}

	if opt.ShowStepDefinitions {
		s := &Suite{}
		contextInitializer(s)
		s.printStepDefinitions(output)
		return exitOptionError
	}

	if len(opt.Paths) == 0 {
		inf, err := os.Stat("features")
		if err == nil && inf.IsDir() {
			opt.Paths = []string{"features"}
		}
	}

	if opt.Concurrency > 1 && !supportsConcurrency(opt.Format) {
		fmt.Fprintln(os.Stderr, fmt.Errorf("format \"%s\" does not support concurrent execution", opt.Format))
		return exitOptionError
	}
	formatter := FindFmt(opt.Format)
	if nil == formatter {
		var names []string
		for name := range AvailableFormatters() {
			names = append(names, name)
		}
		fmt.Fprintln(os.Stderr, fmt.Errorf(
			`unregistered formatter name: "%s", use one of: %s`,
			opt.Format,
			strings.Join(names, ", "),
		))
		return exitOptionError
	}

	features, err := parseFeatures(opt.Tags, opt.Paths)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		return exitOptionError
	}

	// user may have specified -1 option to create random seed
	randomize := opt.Randomize
	if randomize == -1 {
		randomize = makeRandomSeed()
	}

	r := runner{
		fmt:           formatter(suite, output),
		initializer:   contextInitializer,
		features:      features,
		randomSeed:    randomize,
		stopOnFailure: opt.StopOnFailure,
		strict:        opt.Strict,
	}

	// store chosen seed in environment, so it could be seen in formatter summary report
	os.Setenv("GODOG_SEED", strconv.FormatInt(r.randomSeed, 10))
	// determine tested package
	_, filename, _, _ := runtime.Caller(1)
	os.Setenv("GODOG_TESTED_PACKAGE", runsFromPackage(filename))

	var failed bool
	if opt.Concurrency > 1 {
		failed = r.concurrent(opt.Concurrency, func() Formatter { return formatter(suite, output) })
	} else {
		failed = r.run()
	}

	// @TODO: should prevent from having these
	os.Setenv("GODOG_SEED", "")
	os.Setenv("GODOG_TESTED_PACKAGE", "")
	if failed && opt.Format != "events" {
		return exitFailure
	}
	return exitSuccess
}

func runsFromPackage(fp string) string {
	dir := filepath.Dir(fp)
	for _, gp := range gopaths {
		gp = filepath.Join(gp, "src")
		if strings.Index(dir, gp) == 0 {
			return strings.TrimLeft(strings.Replace(dir, gp, "", 1), string(filepath.Separator))
		}
	}
	return dir
}

// Run creates and runs the feature suite.
// Reads all configuration options from flags.
// uses contextInitializer to register contexts
//
// the concurrency option allows runner to
// initialize a number of suites to be run
// separately. Only progress formatter
// is supported when concurrency level is
// higher than 1
//
// contextInitializer must be able to register
// the step definitions and event handlers.
//
// The exit codes may vary from:
//  0 - success
//  1 - failed
//  2 - command line usage error
//  128 - or higher, os signal related error exit codes
//
// If there are flag related errors they will
// be directed to os.Stderr
func Run(suite string, contextInitializer func(suite *Suite)) int {
	var opt Options
	opt.Output = colors.Colored(os.Stdout)
	flagSet := FlagSet(&opt)
	if err := flagSet.Parse(os.Args[1:]); err != nil {
		fmt.Fprintln(os.Stderr, err)
		return exitOptionError
	}

	opt.Paths = flagSet.Args()

	return RunWithOptions(suite, contextInitializer, opt)
}

func supportsConcurrency(format string) bool {
	switch format {
	case "progress", "junit":
		return true
	case "events", "pretty", "cucumber":
		return false
	default:
		return true // enables concurrent custom formatters to work
	}
}
