package internal_integration_test

import (
	"context"
	"fmt"
	"io"
	"reflect"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	"github.com/onsi/ginkgo/v2/internal"
	"github.com/onsi/ginkgo/v2/internal/global"
	"github.com/onsi/ginkgo/v2/internal/interrupt_handler"
	"github.com/onsi/ginkgo/v2/internal/parallel_support"
	. "github.com/onsi/ginkgo/v2/internal/test_helpers"
	"github.com/onsi/ginkgo/v2/types"
	. "github.com/onsi/gomega"
	"github.com/onsi/gomega/format"
)

func TestSuiteTests(t *testing.T) {
	interrupt_handler.ABORT_POLLING_INTERVAL = 200 * time.Millisecond
	format.TruncatedDiff = false
	RegisterFailHandler(Fail)
	suiteConfig, _ := GinkgoConfiguration()
	suiteConfig.GracePeriod = time.Second
	RunSpecs(t, "Suite Integration Tests", suiteConfig)
}

var conf types.SuiteConfig
var failer *internal.Failer
var writer *internal.Writer
var reporter *FakeReporter
var rt *RunTracker
var cl types.CodeLocation
var interruptHandler *FakeInterruptHandler
var outputInterceptor *FakeOutputInterceptor

var server parallel_support.Server
var client parallel_support.Client
var exitChannels map[int]chan any

var triggerProgressSignal func()

func progressSignalRegistrar(handler func()) context.CancelFunc {
	triggerProgressSignal = handler
	return func() { triggerProgressSignal = nil }
}

func noopProgressSignalRegistrar(_ func()) context.CancelFunc {
	return func() {}
}

var _ = BeforeEach(func() {
	conf = types.SuiteConfig{}
	failer = internal.NewFailer()
	writer = internal.NewWriter(io.Discard)
	writer.SetMode(internal.WriterModeBufferOnly)
	reporter = NewFakeReporter()
	rt = NewRunTracker()
	cl = types.NewCodeLocation(0)
	interruptHandler = NewFakeInterruptHandler()
	outputInterceptor = NewFakeOutputInterceptor()

	conf.ParallelTotal = 1
	conf.ParallelProcess = 1
	conf.RandomSeed = 17
	conf.GracePeriod = 30 * time.Second

	server, client, exitChannels = nil, nil, nil
})

/* Helpers to set up and run test fixtures using the Ginkgo DSL */
func WithSuite(suite *internal.Suite, callback func()) {
	originalSuite, originalFailer := global.Suite, global.Failer
	global.Suite = suite
	global.Failer = failer
	callback()
	global.Suite = originalSuite
	global.Failer = originalFailer
}

func SetUpForParallel(parallelTotal int) {
	conf.ParallelTotal = parallelTotal
	server, client, exitChannels = SetUpServerAndClient(conf.ParallelTotal)

	conf.ParallelHost = server.Address()
}

func RunFixture(description string, callback func(), aroundNodes ...types.AroundNodeDecorator) (bool, bool) {
	suite := internal.NewSuite()
	var success, hasProgrammaticFocus bool
	WithSuite(suite, func() {
		callback()
		Ω(suite.BuildTree()).Should(Succeed())
		success, hasProgrammaticFocus = suite.Run(description, Label("TopLevelLabel"), SemVerConstraints{}, ComponentSemVerConstraints{}, aroundNodes, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, progressSignalRegistrar, conf)
	})
	return success, hasProgrammaticFocus
}

/*
You should call SetUpForParallel() first, then call RunFixtureInParallel()

this is, at best, an approximation.  There are some dsl objects that can be called within a running node (e.g. DeferCleanup) that will not work with RunFixtureInParallel as they will attach to the actual internal_integration suite as opposed to the simulated fixture

moreover the FakeInterruptHandler is not used - instead a real interrupt handler is created and configured with the client generated by SetUpForParallel.  this is to facilitate the testing of cross-process aborts, which was the primary motivator for this method.

also a noopProgressSignalRegistrar is used to avoid an annoying data race
*/
func RunFixtureInParallel(description string, callback func(proc int)) bool {
	suites := make([]*internal.Suite, conf.ParallelTotal)
	finished := make(chan bool, conf.ParallelTotal)
	for proc := 1; proc <= conf.ParallelTotal; proc++ {
		suites[proc-1] = internal.NewSuite()
		WithSuite(suites[proc-1], func() {
			callback(proc)
			Ω(suites[proc-1].BuildTree()).Should(Succeed())
		})
	}
	for proc := 1; proc <= conf.ParallelTotal; proc++ {
		proc := proc
		c := conf //make a copy
		c.ParallelProcess = proc
		exit := exitChannels[proc]
		suite := suites[proc-1]
		go func() {
			interruptHandler := interrupt_handler.NewInterruptHandler(client)
			defer interruptHandler.Stop()

			success, _ := suite.Run(fmt.Sprintf("%s - %d", description, proc), Label("TopLevelLabel"), SemVerConstraints{}, ComponentSemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, noopProgressSignalRegistrar, c)
			close(exit)
			finished <- success
		}()
	}
	success := true
	for proc := 1; proc <= conf.ParallelTotal; proc++ {
		success = (<-finished) && success
	}

	return success
}

func F(options ...any) {
	location := cl
	message := "fail"
	for _, option := range options {
		if reflect.TypeOf(option).Kind() == reflect.String {
			message = option.(string)
		} else if reflect.TypeOf(option) == reflect.TypeOf(cl) {
			location = option.(types.CodeLocation)
		}
	}

	failer.Fail(message, location)
	panic("panic to simulate how ginkgo's Fail works")
}

func Abort(options ...any) {
	location := cl
	message := "abort"
	for _, option := range options {
		if reflect.TypeOf(option).Kind() == reflect.String {
			message = option.(string)
		} else if reflect.TypeOf(option) == reflect.TypeOf(cl) {
			location = option.(types.CodeLocation)
		}
	}

	failer.AbortSuite(message, location)
	panic("panic to simulate how ginkgo's AbortSuite works")
}

func FixtureSkip(options ...any) {
	location := cl
	message := "skip"
	for _, option := range options {
		if reflect.TypeOf(option).Kind() == reflect.String {
			message = option.(string)
		} else if reflect.TypeOf(option) == reflect.TypeOf(cl) {
			location = option.(types.CodeLocation)
		}
	}

	failer.Skip(message, location)
	panic("panic to simulate how ginkgo's Skip works")
}

func HaveHighlightedStackLine(cl types.CodeLocation) OmegaMatcher {
	return ContainElement(WithTransform(func(fc types.FunctionCall) types.CodeLocation {
		if fc.Highlight {
			return types.CodeLocation{
				FileName:   fc.Filename,
				LineNumber: int(fc.Line),
			}
		}
		return types.CodeLocation{}
	}, Equal(cl)))
}

func clLine(offset int) types.CodeLocation {
	cl := cl
	cl.LineNumber += offset
	return cl
}
