package specrunner

import (
	"fmt"
	"os"
	"os/signal"
	"sync"
	"syscall"

	"github.com/onsi/ginkgo/internal/spec_iterator"

	"github.com/onsi/ginkgo/config"
	"github.com/onsi/ginkgo/internal/leafnodes"
	"github.com/onsi/ginkgo/internal/spec"
	Writer "github.com/onsi/ginkgo/internal/writer"
	"github.com/onsi/ginkgo/reporters"
	"github.com/onsi/ginkgo/types"

	"time"
)

type SpecRunner struct {
	description     string
	beforeSuiteNode leafnodes.SuiteNode
	iterator        spec_iterator.SpecIterator
	afterSuiteNode  leafnodes.SuiteNode
	reporters       []reporters.Reporter
	startTime       time.Time
	suiteID         string
	runningSpec     *spec.Spec
	writer          Writer.WriterInterface
	config          config.GinkgoConfigType
	interrupted     bool
	processedSpecs  []*spec.Spec
	lock            *sync.Mutex
}

func New(description string, beforeSuiteNode leafnodes.SuiteNode, iterator spec_iterator.SpecIterator, afterSuiteNode leafnodes.SuiteNode, reporters []reporters.Reporter, writer Writer.WriterInterface, config config.GinkgoConfigType) *SpecRunner {
	return &SpecRunner{
		description:     description,
		beforeSuiteNode: beforeSuiteNode,
		iterator:        iterator,
		afterSuiteNode:  afterSuiteNode,
		reporters:       reporters,
		writer:          writer,
		config:          config,
		suiteID:         randomID(),
		lock:            &sync.Mutex{},
	}
}

func (runner *SpecRunner) Run() bool {
	if runner.config.DryRun {
		runner.performDryRun()
		return true
	}

	runner.reportSuiteWillBegin()
	go runner.registerForInterrupts()

	suitePassed := runner.runBeforeSuite()

	if suitePassed {
		suitePassed = runner.runSpecs()
	}

	runner.blockForeverIfInterrupted()

	suitePassed = runner.runAfterSuite() && suitePassed

	runner.reportSuiteDidEnd(suitePassed)

	return suitePassed
}

func (runner *SpecRunner) performDryRun() {
	runner.reportSuiteWillBegin()

	if runner.beforeSuiteNode != nil {
		summary := runner.beforeSuiteNode.Summary()
		summary.State = types.SpecStatePassed
		runner.reportBeforeSuite(summary)
	}

	for {
		spec, err := runner.iterator.Next()
		if err == spec_iterator.ErrClosed {
			break
		}
		if err != nil {
			fmt.Println("failed to iterate over tests:\n" + err.Error())
			break
		}

		runner.processedSpecs = append(runner.processedSpecs, spec)

		summary := spec.Summary(runner.suiteID)
		runner.reportSpecWillRun(summary)
		if summary.State == types.SpecStateInvalid {
			summary.State = types.SpecStatePassed
		}
		runner.reportSpecDidComplete(summary, false)
	}

	if runner.afterSuiteNode != nil {
		summary := runner.afterSuiteNode.Summary()
		summary.State = types.SpecStatePassed
		runner.reportAfterSuite(summary)
	}

	runner.reportSuiteDidEnd(true)
}

func (runner *SpecRunner) runBeforeSuite() bool {
	if runner.beforeSuiteNode == nil || runner.wasInterrupted() {
		return true
	}

	runner.writer.Truncate()
	conf := runner.config
	passed := runner.beforeSuiteNode.Run(conf.ParallelNode, conf.ParallelTotal, conf.SyncHost)
	if !passed {
		runner.writer.DumpOut()
	}
	runner.reportBeforeSuite(runner.beforeSuiteNode.Summary())
	return passed
}

func (runner *SpecRunner) runAfterSuite() bool {
	if runner.afterSuiteNode == nil {
		return true
	}

	runner.writer.Truncate()
	conf := runner.config
	passed := runner.afterSuiteNode.Run(conf.ParallelNode, conf.ParallelTotal, conf.SyncHost)
	if !passed {
		runner.writer.DumpOut()
	}
	runner.reportAfterSuite(runner.afterSuiteNode.Summary())
	return passed
}

func (runner *SpecRunner) runSpecs() bool {
	suiteFailed := false
	skipRemainingSpecs := false
	for {
		spec, err := runner.iterator.Next()
		if err == spec_iterator.ErrClosed {
			break
		}
		if err != nil {
			fmt.Println("failed to iterate over tests:\n" + err.Error())
			suiteFailed = true
			break
		}

		runner.processedSpecs = append(runner.processedSpecs, spec)

		if runner.wasInterrupted() {
			break
		}
		if skipRemainingSpecs {
			spec.Skip()
		}

		if !spec.Skipped() && !spec.Pending() {
			if passed := runner.runSpec(spec); !passed {
				suiteFailed = true
			}
		} else if spec.Pending() && runner.config.FailOnPending {
			runner.reportSpecWillRun(spec.Summary(runner.suiteID))
			suiteFailed = true
			runner.reportSpecDidComplete(spec.Summary(runner.suiteID), spec.Failed())
		} else {
			runner.reportSpecWillRun(spec.Summary(runner.suiteID))
			runner.reportSpecDidComplete(spec.Summary(runner.suiteID), spec.Failed())
		}

		if spec.Failed() && runner.config.FailFast {
			skipRemainingSpecs = true
		}
	}

	return !suiteFailed
}

func (runner *SpecRunner) runSpec(spec *spec.Spec) (passed bool) {
	maxAttempts := 1
	if runner.config.FlakeAttempts > 0 {
		// uninitialized configs count as 1
		maxAttempts = runner.config.FlakeAttempts
	}

	for i := 0; i < maxAttempts; i++ {
		runner.reportSpecWillRun(spec.Summary(runner.suiteID))
		runner.runningSpec = spec
		spec.Run(runner.writer)
		runner.runningSpec = nil
		runner.reportSpecDidComplete(spec.Summary(runner.suiteID), spec.Failed())
		if !spec.Failed() {
			return true
		}
	}
	return false
}

func (runner *SpecRunner) CurrentSpecSummary() (*types.SpecSummary, bool) {
	if runner.runningSpec == nil {
		return nil, false
	}

	return runner.runningSpec.Summary(runner.suiteID), true
}

func (runner *SpecRunner) registerForInterrupts() {
	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt, syscall.SIGTERM)

	<-c
	signal.Stop(c)
	runner.markInterrupted()
	go runner.registerForHardInterrupts()
	runner.writer.DumpOutWithHeader(`
Received interrupt.  Emitting contents of GinkgoWriter...
---------------------------------------------------------
`)
	if runner.afterSuiteNode != nil {
		fmt.Fprint(os.Stderr, `
---------------------------------------------------------
Received interrupt.  Running AfterSuite...
^C again to terminate immediately
`)
		runner.runAfterSuite()
	}
	runner.reportSuiteDidEnd(false)
	os.Exit(1)
}

func (runner *SpecRunner) registerForHardInterrupts() {
	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt, syscall.SIGTERM)

	<-c
	fmt.Fprintln(os.Stderr, "\nReceived second interrupt.  Shutting down.")
	os.Exit(1)
}

func (runner *SpecRunner) blockForeverIfInterrupted() {
	runner.lock.Lock()
	interrupted := runner.interrupted
	runner.lock.Unlock()

	if interrupted {
		select {}
	}
}

func (runner *SpecRunner) markInterrupted() {
	runner.lock.Lock()
	defer runner.lock.Unlock()
	runner.interrupted = true
}

func (runner *SpecRunner) wasInterrupted() bool {
	runner.lock.Lock()
	defer runner.lock.Unlock()
	return runner.interrupted
}

func (runner *SpecRunner) reportSuiteWillBegin() {
	runner.startTime = time.Now()
	summary := runner.suiteWillBeginSummary()
	for _, reporter := range runner.reporters {
		reporter.SpecSuiteWillBegin(runner.config, summary)
	}
}

func (runner *SpecRunner) reportBeforeSuite(summary *types.SetupSummary) {
	for _, reporter := range runner.reporters {
		reporter.BeforeSuiteDidRun(summary)
	}
}

func (runner *SpecRunner) reportAfterSuite(summary *types.SetupSummary) {
	for _, reporter := range runner.reporters {
		reporter.AfterSuiteDidRun(summary)
	}
}

func (runner *SpecRunner) reportSpecWillRun(summary *types.SpecSummary) {
	runner.writer.Truncate()

	for _, reporter := range runner.reporters {
		reporter.SpecWillRun(summary)
	}
}

func (runner *SpecRunner) reportSpecDidComplete(summary *types.SpecSummary, failed bool) {
	if failed && len(summary.CapturedOutput) == 0 {
		summary.CapturedOutput = string(runner.writer.Bytes())
	}
	for i := len(runner.reporters) - 1; i >= 1; i-- {
		runner.reporters[i].SpecDidComplete(summary)
	}

	if failed {
		runner.writer.DumpOut()
	}

	runner.reporters[0].SpecDidComplete(summary)
}

func (runner *SpecRunner) reportSuiteDidEnd(success bool) {
	summary := runner.suiteDidEndSummary(success)
	summary.RunTime = time.Since(runner.startTime)
	for _, reporter := range runner.reporters {
		reporter.SpecSuiteDidEnd(summary)
	}
}

func (runner *SpecRunner) countSpecsThatRanSatisfying(filter func(ex *spec.Spec) bool) (count int) {
	count = 0

	for _, spec := range runner.processedSpecs {
		if filter(spec) {
			count++
		}
	}

	return count
}

func (runner *SpecRunner) suiteDidEndSummary(success bool) *types.SuiteSummary {
	numberOfSpecsThatWillBeRun := runner.countSpecsThatRanSatisfying(func(ex *spec.Spec) bool {
		return !ex.Skipped() && !ex.Pending()
	})

	numberOfPendingSpecs := runner.countSpecsThatRanSatisfying(func(ex *spec.Spec) bool {
		return ex.Pending()
	})

	numberOfSkippedSpecs := runner.countSpecsThatRanSatisfying(func(ex *spec.Spec) bool {
		return ex.Skipped()
	})

	numberOfPassedSpecs := runner.countSpecsThatRanSatisfying(func(ex *spec.Spec) bool {
		return ex.Passed()
	})

	numberOfFlakedSpecs := runner.countSpecsThatRanSatisfying(func(ex *spec.Spec) bool {
		return ex.Flaked()
	})

	numberOfFailedSpecs := runner.countSpecsThatRanSatisfying(func(ex *spec.Spec) bool {
		return ex.Failed()
	})

	if runner.beforeSuiteNode != nil && !runner.beforeSuiteNode.Passed() && !runner.config.DryRun {
		var known bool
		numberOfSpecsThatWillBeRun, known = runner.iterator.NumberOfSpecsThatWillBeRunIfKnown()
		if !known {
			numberOfSpecsThatWillBeRun = runner.iterator.NumberOfSpecsPriorToIteration()
		}
		numberOfFailedSpecs = numberOfSpecsThatWillBeRun
	}

	return &types.SuiteSummary{
		SuiteDescription: runner.description,
		SuiteSucceeded:   success,
		SuiteID:          runner.suiteID,

		NumberOfSpecsBeforeParallelization: runner.iterator.NumberOfSpecsPriorToIteration(),
		NumberOfTotalSpecs:                 len(runner.processedSpecs),
		NumberOfSpecsThatWillBeRun:         numberOfSpecsThatWillBeRun,
		NumberOfPendingSpecs:               numberOfPendingSpecs,
		NumberOfSkippedSpecs:               numberOfSkippedSpecs,
		NumberOfPassedSpecs:                numberOfPassedSpecs,
		NumberOfFailedSpecs:                numberOfFailedSpecs,
		NumberOfFlakedSpecs:                numberOfFlakedSpecs,
	}
}

func (runner *SpecRunner) suiteWillBeginSummary() *types.SuiteSummary {
	numTotal, known := runner.iterator.NumberOfSpecsToProcessIfKnown()
	if !known {
		numTotal = -1
	}

	numToRun, known := runner.iterator.NumberOfSpecsThatWillBeRunIfKnown()
	if !known {
		numToRun = -1
	}

	return &types.SuiteSummary{
		SuiteDescription: runner.description,
		SuiteID:          runner.suiteID,

		NumberOfSpecsBeforeParallelization: runner.iterator.NumberOfSpecsPriorToIteration(),
		NumberOfTotalSpecs:                 numTotal,
		NumberOfSpecsThatWillBeRun:         numToRun,
		NumberOfPendingSpecs:               -1,
		NumberOfSkippedSpecs:               -1,
		NumberOfPassedSpecs:                -1,
		NumberOfFailedSpecs:                -1,
		NumberOfFlakedSpecs:                -1,
	}
}
