package suite

import (
	"context"
	"time"

	"code.justin.tv/eventbus/controlplane/e2e/internal/e2eutil"
	"code.justin.tv/eventbus/controlplane/e2e/internal/report"
	"code.justin.tv/eventbus/controlplane/e2e/internal/resource"
	"code.justin.tv/eventbus/controlplane/e2e/internal/seed"
)

var _ Runner = &DefaultTestSuite{}

// DefaultTestSuite performs the most sane actions on
type DefaultTestSuite struct {
	suiteName           string
	Services            []*resource.Service
	EventDefinitions    []*resource.EventDefinition
	Publications        []*resource.Publication
	SubscriptionTargets []*resource.SubscriptionTarget
	Subscriptions       []*resource.Subscription
	FeatureFlags        []*resource.FeatureFlag

	errors []report.Error
}

// NewDefaultTestSuite loads data for a given test name's seed,
// and establishes core (but overridable) actions for setup, test, and clean
func NewDefaultTestSuite(suiteName string) (*DefaultTestSuite, error) {

	data, err := seed.Fetch(suiteName)
	if err != nil {
		return nil, err
	}

	testSuite := &DefaultTestSuite{
		suiteName:           suiteName,
		Services:            data.Services,
		EventDefinitions:    data.EventDefinitions,
		Publications:        data.Publications,
		SubscriptionTargets: data.SubscriptionTargets,
		Subscriptions:       data.Subscriptions,
		FeatureFlags:        data.FeatureFlags,
	}

	// Inject the parent test suite in for bubbling up errors
	for _, s := range testSuite.Services {
		s.TestRunner = testSuite
	}
	for _, e := range testSuite.EventDefinitions {
		e.TestRunner = testSuite
	}
	for _, p := range testSuite.Publications {
		p.TestRunner = testSuite
	}
	for _, st := range testSuite.SubscriptionTargets {
		st.TestRunner = testSuite
	}
	for _, sub := range testSuite.Subscriptions {
		sub.TestRunner = testSuite
	}
	for _, ff := range testSuite.FeatureFlags {
		ff.TestRunner = testSuite
	}

	return testSuite, nil
}

// TestName returns the name of the test suite being run
func (s *DefaultTestSuite) TestName() string {
	return "DefaultTestSuite"
}

func (s *DefaultTestSuite) SuiteName() string {
	return s.suiteName
}

// Setup does the most sane thing for a given set of resources.
// It calls subresource Setup methods in a reasonable order
// followed by a call to converge
func (s *DefaultTestSuite) Setup(ctx context.Context) report.Error {
	ctx = e2eutil.AppendTestPath(ctx, s.TestName())
	var reportError report.Error
	for _, eventDefinition := range s.EventDefinitions {
		reportError = eventDefinition.Setup(ctx)
		if reportError != nil {
			return reportError
		}
	}
	for _, service := range s.Services {
		reportError = service.Setup(ctx)
		if reportError != nil {
			return reportError
		}
	}
	for _, publication := range s.Publications {
		reportError = publication.Setup(ctx)
		if reportError != nil {
			return reportError
		}
	}
	for _, st := range s.SubscriptionTargets {
		reportError = st.Setup(ctx)
		if reportError != nil {
			return reportError
		}
	}
	for _, sub := range s.Subscriptions {
		reportError = sub.Setup(ctx)
		if reportError != nil {
			return reportError
		}
	}
	for _, ff := range s.FeatureFlags {
		reportError = ff.Setup(ctx)
		if reportError != nil {
			return reportError
		}
	}

	s.Log(ctx, "Running controlplane converger (default suite)")
	err := e2eutil.Converge()
	if err != nil {
		return report.ErrorFromContext(ctx, "Error during converger process", err)
	}
	// Sleep for a few seconds to let AWS API operations bake,
	// to greatly increase determinism in E2E test outcomes.
	// JIRA: https://jira.twitch.com/browse/ASYNC-488
	time.Sleep(10 * time.Second)
	return nil
}

// Test does the most sane thing for a given set of resources.
// It calls subresource Test methods one at a time and aggregates errors
func (s *DefaultTestSuite) Test(ctx context.Context) {
	ctx = e2eutil.AppendTestPath(ctx, s.TestName())

	for _, eventDefinition := range s.EventDefinitions {
		eventDefinition.Test(ctx)
	}
	for _, service := range s.Services {
		service.Test(ctx)
	}
	for _, publication := range s.Publications {
		publication.Test(ctx)
	}
	for _, subscriptionTarget := range s.SubscriptionTargets {
		subscriptionTarget.Test(ctx)
	}
	for _, subscription := range s.Subscriptions {
		subscription.Test(ctx)
	}
	for _, featureFlag := range s.FeatureFlags {
		featureFlag.Test(ctx)
	}
}

func (s *DefaultTestSuite) Errors() []report.Error {
	return s.errors
}

func (s *DefaultTestSuite) Clean(ctx context.Context) []report.Error {
	// Clean should operate in reverse order from Setup!
	ctx = e2eutil.AppendTestPath(ctx, s.TestName())

	// Do a best effort of cleanup, collecting errors along the way to return
	cleanErrs := make([]report.Error, 0)
	for _, ff := range s.FeatureFlags {
		if errs := ff.Clean(ctx); errs != nil {
			cleanErrs = append(cleanErrs, errs...)
		}
	}
	for _, sub := range s.Subscriptions {
		if errs := sub.Clean(ctx); errs != nil {
			cleanErrs = append(cleanErrs, errs...)
		}
	}
	for _, st := range s.SubscriptionTargets {
		if errs := st.Clean(ctx); errs != nil {
			cleanErrs = append(cleanErrs, errs...)
		}
	}
	for _, service := range s.Services {
		if errs := service.Clean(ctx); errs != nil {
			cleanErrs = append(cleanErrs, errs...)
		}
	}
	for _, eventDefinition := range s.EventDefinitions {
		if errs := eventDefinition.Clean(ctx); errs != nil {
			cleanErrs = append(cleanErrs, errs...)
		}
	}
	for _, pub := range s.Publications {
		if errs := pub.Clean(ctx); errs != nil {
			cleanErrs = append(cleanErrs, errs...)
		}
	}
	// Truncate the DB
	s.Log(ctx, "Wiping controlplane DB data")
	if err := e2eutil.TruncateDB(); err != nil {
		cleanErrs = append(cleanErrs, report.ErrorFromContext(ctx, "Could not truncate database", err))
	}
	return cleanErrs
}

func (s *DefaultTestSuite) Error(ctx context.Context, msg string, err error) {
	e := report.ErrorFromContext(ctx, msg, err)
	s.errors = append(s.errors, e)
	report.LogError(e)
}

func (s *DefaultTestSuite) Log(ctx context.Context, msg string) {
	report.Log(report.MessageFromContext(ctx, msg))
}
