package suite

import (
	"bytes"
	"context"
	"strconv"

	"code.justin.tv/eventbus/controlplane/infrastructure/routing"

	"code.justin.tv/eventbus/controlplane/e2e/internal/expected"

	"code.justin.tv/eventbus/controlplane/e2e/internal/e2eutil"
	"code.justin.tv/eventbus/controlplane/e2e/internal/resource"
	"code.justin.tv/eventbus/controlplane/infrastructure/validation"
	"code.justin.tv/eventbus/controlplane/internal/e2eaccounts"
	"code.justin.tv/eventbus/controlplane/internal/environment"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/aws/aws-sdk-go/service/sns"
	"github.com/pkg/errors"
)

// Unsubscribing an SQS queue from an SNS topic prevents resubscribing to that topic for 72
// hours, unless the unsubscribe is done from the subscribing account. This test verifies that
// we don't regress the ability to unsubscribe and resubscribe.

var _ Runner = &TopicValidationTestSuite{}

type TopicValidationTestSuite struct {
	*DefaultTestSuite // Use the default suite as ground work
}

func NewTopicValidationTestSuite(suiteName string) (Runner, error) {
	defaultTest, err := NewDefaultTestSuite(suiteName)
	if err != nil {
		return nil, err
	}
	return &TopicValidationTestSuite{
		DefaultTestSuite: defaultTest,
	}, nil
}

func (s *TopicValidationTestSuite) Test(ctx context.Context) {
	var err error
	s.Log(ctx, "Validating event stream existence and routing")

	s.Log(ctx, "Checking event stream with no configuration errors")
	err = s.checkTopicOk(ctx, s.EventDefinitions[0])
	if err != nil {
		s.Error(ctx, "validation failed expectations", err)
		return
	}

	s.Log(ctx, "Checking non-production event stream with deleted topic")
	err = s.checkTopicNonProductionDeleted(ctx, s.EventDefinitions[1])
	if err != nil {
		s.Error(ctx, "validation failed expectations", err)
		return
	}

	s.Log(ctx, "Checking production event stream with deleted topic")
	err = s.checkTopicProductionDeleted(ctx, s.EventDefinitions[2])
	if err != nil {
		s.Error(ctx, "validation failed expectations", err)
		return
	}

	s.Log(ctx, "Checking event stream with deleted s3 route object")
	err = s.checkTopicS3RouteDeleted(ctx, s.EventDefinitions[3])
	if err != nil {
		s.Error(ctx, "validation failed expectations", err)
		return
	}

	s.Log(ctx, "Checking event stream with malformed s3 route object")
	err = s.checkTopicS3RouteMalformed(ctx, s.EventDefinitions[4])
	if err != nil {
		s.Error(ctx, "validation failed expectations", err)
		return
	}

	s.Log(ctx, "Checking event stream with incorrect s3 route arn")
	err = s.checkTopicS3RouteMismatch(ctx, s.EventDefinitions[5])
	if err != nil {
		s.Error(ctx, "validation failed expectations", err)
		return
	}

}

func (s *TopicValidationTestSuite) checkTopicOk(ctx context.Context, eventDef *resource.EventDefinition) error {
	reports, err := e2eutil.Validate(ctx)
	if err != nil {
		return errors.Wrap(err, "validation routine failed")
	}

	validationFailures := validation.NotOk(reports)
	if validationFailures != nil && e2eutil.ErrorsFor(eventDef.EventType, validationFailures) {
		return errors.Wrap(err, "expected no errors for topic "+eventDef.EventType)
	}
	return nil
}

func (s *TopicValidationTestSuite) checkTopicNonProductionDeleted(ctx context.Context, eventDef *resource.EventDefinition) error {
	snsClient := sns.New(session.Must(session.NewSession()), e2eaccounts.AccountCredentials().MainConfig())

	_, err := snsClient.DeleteTopicWithContext(ctx, &sns.DeleteTopicInput{
		TopicArn: aws.String(expected.SNSTopicARN(e2eutil.JobID(ctx), eventDef.EventType, environment.Development)),
	})
	if err != nil {
		return errors.Wrap(err, "unable to delete topic")
	}

	reports, err := e2eutil.Validate(ctx)
	if err != nil {
		return errors.Wrap(err, "validation routine failed")
	}

	validationFailures := validation.NotOk(reports)
	if len(validationFailures) != 1 {
		return errors.New("expected 1 error, got " + strconv.Itoa(len(validationFailures)))
	} else if validationFailures[0].Status != validation.StatusWarn {
		return errors.New("expected WARN for missing non-prod topic, got " + string(validationFailures[0].Status))
	}

	return nil
}

func (s *TopicValidationTestSuite) checkTopicProductionDeleted(ctx context.Context, eventDef *resource.EventDefinition) error {
	snsClient := sns.New(session.Must(session.NewSession()), e2eaccounts.AccountCredentials().MainConfig())

	_, err := snsClient.DeleteTopicWithContext(ctx, &sns.DeleteTopicInput{
		TopicArn: aws.String(expected.SNSTopicARN(e2eutil.JobID(ctx), eventDef.EventType, environment.Production)),
	})
	if err != nil {
		return errors.Wrap(err, "unable to delete topic")
	}

	reports, err := e2eutil.Validate(ctx)
	if err != nil {
		return errors.Wrap(err, "validation routine failed")
	}

	validationFailures := e2eutil.ReportsFor(eventDef.EventType, validation.NotOk(reports))
	if len(validationFailures) != 1 {
		return errors.New("expected 1 error, got " + strconv.Itoa(len(validationFailures)))
	} else if validationFailures[0].Status != validation.StatusError {
		return errors.New("expected WARN for missing non-prod topic, got " + string(validationFailures[0].Status))
	}

	return nil
}

func (s *TopicValidationTestSuite) checkTopicS3RouteDeleted(ctx context.Context, eventDef *resource.EventDefinition) error {
	s3Client := s3.New(session.Must(session.NewSession()), e2eaccounts.AccountCredentials().MainConfig())

	_, err := s3Client.DeleteObjectWithContext(ctx, &s3.DeleteObjectInput{
		Bucket: aws.String(routing.S3BucketName(environment.Environment())),
		Key:    aws.String(expected.S3ObjectKey(e2eutil.JobID(ctx), eventDef.EventType, environment.Staging)),
	})
	if err != nil {
		return errors.Wrap(err, "could not delete s3 route object")
	}

	reports, err := e2eutil.Validate(ctx)
	if err != nil {
		return errors.Wrap(err, "validation routine failed")
	}

	validationFailures := e2eutil.ReportsFor(eventDef.EventType, validation.NotOk(reports))
	if len(validationFailures) != 1 {
		return errors.New("expected 1 error, got " + strconv.Itoa(len(validationFailures)))
	} else if validationFailures[0].Status != validation.StatusWarn {
		return errors.New("expected WARN for missing s3 object, got " + string(validationFailures[0].Status))
	}

	return nil
}

func (s *TopicValidationTestSuite) checkTopicS3RouteMalformed(ctx context.Context, eventDef *resource.EventDefinition) error {
	s3Client := s3.New(session.Must(session.NewSession()), e2eaccounts.AccountCredentials().MainConfig())

	_, err := s3Client.PutObjectWithContext(ctx, &s3.PutObjectInput{
		Bucket: aws.String(routing.S3BucketName(environment.Environment())),
		Key:    aws.String(expected.S3ObjectKey(e2eutil.JobID(ctx), eventDef.EventType, environment.Staging)),
		Body:   bytes.NewReader([]byte("this isnt valid JSON")),
	})
	if err != nil {
		return errors.Wrap(err, "could not delete s3 route object")
	}

	reports, err := e2eutil.Validate(ctx)
	if err != nil {
		return errors.Wrap(err, "validation routine failed")
	}

	validationFailures := e2eutil.ReportsFor(eventDef.EventType, validation.NotOk(reports))
	if len(validationFailures) != 1 {
		return errors.New("expected 1 error, got " + strconv.Itoa(len(validationFailures)))
	} else if validationFailures[0].Status != validation.StatusWarn {
		return errors.New("expected WARN for missing s3 object, got " + string(validationFailures[0].Status))
	}

	return nil
}

func (s *TopicValidationTestSuite) checkTopicS3RouteMismatch(ctx context.Context, eventDef *resource.EventDefinition) error {
	s3Client := s3.New(session.Must(session.NewSession()), e2eaccounts.AccountCredentials().MainConfig())

	_, err := s3Client.PutObjectWithContext(ctx, &s3.PutObjectInput{
		Bucket: aws.String(routing.S3BucketName(environment.Environment())),
		Key:    aws.String(expected.S3ObjectKey(e2eutil.JobID(ctx), eventDef.EventType, environment.Staging)),
		Body:   bytes.NewReader([]byte("{\"arn\":\"incorrectARN\"}")),
	})
	if err != nil {
		return errors.Wrap(err, "could not delete s3 route object")
	}

	reports, err := e2eutil.Validate(ctx)
	if err != nil {
		return errors.Wrap(err, "validation routine failed")
	}

	validationFailures := e2eutil.ReportsFor(eventDef.EventType, validation.NotOk(reports))
	if len(validationFailures) != 1 {
		return errors.New("expected 1 error, got " + strconv.Itoa(len(validationFailures)))
	} else if validationFailures[0].Status != validation.StatusWarn {
		return errors.New("expected WARN for missing s3 object, got " + string(validationFailures[0].Status))
	}

	return nil
}

func (s *TopicValidationTestSuite) TestName() string {
	return "TopicValidationTestSuite"
}
