package k6

import (
	"crypto/x509"
	"fmt"
	"log/slog"
	"time"

	"github.com/sirupsen/logrus"
	"go.k6.io/k6/errext"
	"go.k6.io/k6/errext/exitcodes"
	"go.k6.io/k6/js"
	"go.k6.io/k6/js/modules"
	"go.k6.io/k6/lib"
	"go.k6.io/k6/lib/executor"
	"go.k6.io/k6/lib/fsext"
	"go.k6.io/k6/lib/types"
	"go.k6.io/k6/loader"
	"go.k6.io/k6/metrics"
)

// loadedTest contains all of data, details and dependencies of a loaded
// k6 test, but without any config consolidation.
type loadedTest struct {
	source         *loader.SourceData
	fs             fsext.Fs
	fileSystems    map[string]fsext.Fs
	preInitState   *lib.TestPreInitState
	initRunner     lib.Runner // TODO: rename to something more appropriate
	moduleResolver *modules.ModuleResolver

	logger       *slog.Logger
	loggerCompat *logrus.Logger
}

func (lt *loadedTest) initializeFirstRunner() error {
	testPath := lt.source.URL.String()
	logger := lt.logger.With("test_path", testPath)

	logger.Debug("Trying to load as a JS test...")
	runner, err := js.New(lt.preInitState, lt.source, lt.fileSystems)
	// TODO: should we use common.UnwrapGojaInterruptedError() here?
	if err != nil {
		return fmt.Errorf("could not load JS test '%s': %w", testPath, err)
	}
	lt.initRunner = runner
	lt.moduleResolver = runner.Bundle.ModuleResolver
	return nil
}

func (lt *loadedTest) consolidateDeriveAndValidateConfig() (*loadedAndConfiguredTest, error) {
	lt.logger.Debug("Consolidating config layers...")

	config := lib.Options{}
	config = config.Apply(lt.initRunner.GetOptions())

	if config.SystemTags == nil {
		config.SystemTags = &metrics.DefaultSystemTagSet
	}
	if config.SummaryTrendStats == nil {
		config.SummaryTrendStats = lib.DefaultSummaryTrendStats
	}
	defDNS := types.DefaultDNSConfig()
	if !config.DNS.TTL.Valid {
		config.DNS.TTL = defDNS.TTL
	}
	if !config.DNS.Select.Valid {
		config.DNS.Select = defDNS.Select
	}
	if !config.DNS.Policy.Valid {
		config.DNS.Policy = defDNS.Policy
	}
	if !config.SetupTimeout.Valid {
		config.SetupTimeout.Duration = types.Duration(60 * time.Second)
	}
	if !config.TeardownTimeout.Valid {
		config.TeardownTimeout.Duration = types.Duration(60 * time.Second)
	}

	lt.logger.Debug("Parsing thresholds and validating config...")
	// Parse the thresholds, only if the --no-threshold flag is not set.
	// If parsing the threshold expressions failed, consider it as an
	// invalid configuration error.
	if !lt.preInitState.RuntimeOptions.NoThresholds.Bool {
		for metricName, thresholdsDefinition := range config.Thresholds {
			err := thresholdsDefinition.Parse()
			if err != nil {
				return nil, errext.WithExitCodeIfNone(err, exitcodes.InvalidConfig)
			}

			err = thresholdsDefinition.Validate(metricName, lt.preInitState.Registry)
			if err != nil {
				return nil, errext.WithExitCodeIfNone(err, exitcodes.InvalidConfig)
			}
		}
	}

	config, err := executor.DeriveScenariosFromShortcuts(config, lt.loggerCompat)
	if err == nil {
		errors := config.Validate()
		// FIXME: should show them all.
		if len(errors) > 0 {
			err = errors[0]
		}
	}
	if err != nil {
		return nil, err
	}

	return &loadedAndConfiguredTest{
		loadedTest: lt,
		config:     config,
	}, nil
}

// loadedAndConfiguredTest contains the whole loadedTest, as well as the
// consolidated test config and the full test run state.
type loadedAndConfiguredTest struct {
	*loadedTest
	config lib.Options
}

// loadSystemCertPool attempts to load system certificates.
func loadSystemCertPool(logger logrus.FieldLogger) {
	if _, err := x509.SystemCertPool(); err != nil {
		logger.WithError(err).Warning("Unable to load system cert pool")
	}
}

func (lct *loadedAndConfiguredTest) buildTestRunState(
	configToReinject lib.Options,
) (*lib.TestRunState, error) {
	// This might be the full derived or just the consodlidated options
	if err := lct.initRunner.SetOptions(configToReinject); err != nil {
		return nil, err
	}

	// it pre-loads system certificates to avoid doing it on the first TLS request.
	// This is done async to avoid blocking the rest of the loading process as it will not stop if it fails.
	go loadSystemCertPool(lct.preInitState.Logger)

	return &lib.TestRunState{
		TestPreInitState: lct.preInitState,
		Runner:           lct.initRunner,
		Options:          lct.config,
		RunTags:          lct.preInitState.Registry.RootTagSet().WithTagsFromMap(configToReinject.RunTags),
	}, nil
}
