package resource

import (
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"

	"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"

	"code.justin.tv/eventbus/controlplane/e2e/internal/e2eutil"
	"code.justin.tv/eventbus/controlplane/e2e/internal/expected"
	"code.justin.tv/eventbus/controlplane/e2e/internal/httpserver"
	"code.justin.tv/eventbus/controlplane/e2e/internal/report"
	"code.justin.tv/eventbus/controlplane/e2e/internal/test"
	"code.justin.tv/eventbus/controlplane/internal/e2eaccounts"

	"code.justin.tv/eventbus/controlplane/infrastructure/rpc"
)

// Ensure this resource implements the interface at compile time
var _ test.Runner = &EventDefinition{}

type EventDefinition struct {
	test.TestRunner

	EventType        string             `json:"event_type"`
	AuthorizedFields []*AuthorizedField `json:"authorized_fields"`
	LDAPGroup        string             `json:"ldap_group"`
}

type AuthorizedField struct {
	MessageName string `json:"message_name"`
	FieldName   string `json:"field_name"`
}

func (e *EventDefinition) TestName() string {
	return fmt.Sprintf("EventDefinition{%s}", e.EventType)
}

func (e *EventDefinition) Setup(ctx context.Context) report.Error {
	authFields := []*rpc.AuthorizedField{}
	for _, authFieldDef := range e.AuthorizedFields {
		authFields = append(authFields, &rpc.AuthorizedField{
			MessageName: authFieldDef.MessageName,
			FieldName:   authFieldDef.FieldName,
		})
	}

	ldapGroup := "team-eventbus"
	if e.LDAPGroup != "" {
		ldapGroup = e.LDAPGroup
	}

	ctx = e2eutil.AppendTestPath(ctx, e.TestName())
	e.Log(ctx, "Registering event definition")
	httpClient := e2eutil.HTTPClientWithLDAP()
	eventClient := rpc.NewInfrastructureProtobufClient(expected.TwirpEndpoint, httpClient)
	_, err := eventClient.RegisterEventDefinitions(context.Background(), &rpc.RegisterEventDefinitionsReq{
		EventDefinitions: []*rpc.EventDefinition{
			{
				EventType:        e2eutil.Suffix(e.EventType, e2eutil.JobID(ctx)),
				AuthorizedFields: authFields,
				LdapGroup:        ldapGroup,
			},
		},
	})
	if err != nil {
		return report.ErrorFromContext(ctx, "Could not create event definition in database", err)
	}
	return nil
}

func (e *EventDefinition) Test(ctx context.Context) {
	ctx = e2eutil.AppendTestPath(ctx, e.TestName())
	e.testSNSTopic(ctx)
	e.testS3Object(ctx)
	e.testAPI(ctx)
}

func (e *EventDefinition) Clean(ctx context.Context) []report.Error {
	ctx = e2eutil.AppendTestPath(ctx, e.TestName())
	sess := session.Must(session.NewSession())
	snsClient := sns.New(sess, e2eaccounts.AccountCredentials().MainConfig())
	s3Client := s3.New(sess, e2eaccounts.AccountCredentials().MainConfig())
	var err error
	for _, environment := range expected.Environments { // TODO: break this into sub-resources for better contextual logging
		e.Log(ctx, "Deleting SNS topic and S3 object")
		// SNS Topic
		expectedARN := expected.SNSTopicARN(e2eutil.JobID(ctx), e.EventType, environment)
		_, delErr := snsClient.DeleteTopic(&sns.DeleteTopicInput{
			TopicArn: aws.String(expectedARN),
		})
		if delErr != nil {
			return reportErrorSlice(ctx, "Could not delete SNS topic", err)
		}
		// S3 Object
		_, delErr = s3Client.DeleteObject(&s3.DeleteObjectInput{
			Bucket: aws.String(expected.S3BucketName),
			Key:    aws.String(expected.S3ObjectKey(e2eutil.JobID(ctx), e.EventType, environment)),
		})
		if delErr != nil {
			return reportErrorSlice(ctx, "Could not delete S3 Object", err)
		}
	}
	return nil
}

// Ensures existence of SNS topics (one per environment) for the EventDefinition
func (e *EventDefinition) testSNSTopic(ctx context.Context) {
	ctx = e2eutil.AppendTestPath(ctx, "SNSTopic")
	sess := session.Must(session.NewSession())
	snsClient := sns.New(sess, e2eaccounts.AccountCredentials().MainConfig())
	for _, environment := range expected.Environments {
		expectedARN := expected.SNSTopicARN(e2eutil.JobID(ctx), e.EventType, environment)
		e.Log(ctx, "Checking existence of SNS topic for event stream")
		_, err := snsClient.GetTopicAttributes(&sns.GetTopicAttributesInput{
			TopicArn: aws.String(expectedARN),
		})
		if err != nil {
			e.Error(ctx, "Could not find SNS topic", err)
		}
	}
}

type expectedS3ObjectStructure struct {
	ARN string `json:"arn"`
}

// Ensures existence of S3 object with proper contents (one per environment) for a given EventDefinition
func (e *EventDefinition) testS3Object(ctx context.Context) {
	ctx = e2eutil.AppendTestPath(ctx, "S3Object")
	sess := session.Must(session.NewSession())
	s3Client := s3.New(sess, e2eaccounts.AccountCredentials().MainConfig())
	for _, environment := range expected.Environments {
		e.Log(ctx, "Checking existence of S3 bucket object for event stream")
		expectedARN := expected.SNSTopicARN(e2eutil.JobID(ctx), e.EventType, environment)
		expectedObject := expected.S3ObjectKey(e2eutil.JobID(ctx), e.EventType, environment)
		output, err := s3Client.GetObject(&s3.GetObjectInput{
			Bucket: aws.String(expected.S3BucketName),
			Key:    aws.String(expectedObject),
		})
		if err != nil {
			e.Error(ctx, "Could not get S3 object", err)
			continue
		}
		// now marshal contents into expected structure and verify content
		var content = &expectedS3ObjectStructure{}
		b, err := ioutil.ReadAll(output.Body)
		if err != nil {
			e.Error(ctx, "Could not read S3 object contents", err)
			continue
		}
		err = json.Unmarshal(b, content)
		if err != nil {
			e.Error(ctx, "Could not unmarshal S3 object contents", err)
			continue
		}
		if content.ARN != expectedARN {
			e.Error(ctx, "Invalid S3 object contents", fmt.Errorf("incorrect ARN in S3 (expected=%s, actual=%s)", expectedARN, content.ARN))
		}

	}
}

func (e *EventDefinition) testAPI(ctx context.Context) {
	ctx = e2eutil.AppendTestPath(ctx, "AuthorizedFields")
	// Hit API to ensure authorized fields were created for each environment
	for _, env := range expected.Environments {
		eventStream, err := httpserver.EventStream(e2eutil.JobID(ctx), e.EventType, env)
		if err != nil {
			e.Error(ctx, "EventStream not found when checking for authorized fields", err)
			continue
		}
		for _, expectedAuthField := range e.AuthorizedFields {
			var found bool
			for _, actualAuthField := range eventStream.AuthorizedFields {
				if expectedAuthField.MessageName == actualAuthField.MessageName && expectedAuthField.FieldName == actualAuthField.FieldName {
					found = true
					break
				}
			}
			if !found {
				errMsg := fmt.Sprintf("Missing expected authorized field (%s,%s)", expectedAuthField.MessageName, expectedAuthField.FieldName)
				e.Error(ctx, errMsg, errors.New("missing authorized field"))
			}
		}
		expectedLDAPGroup := "team-eventbus"
		if e.LDAPGroup != "" {
			expectedLDAPGroup = e.LDAPGroup
		}
		if eventStream.LdapGroup != expectedLDAPGroup {
			e.Error(ctx, fmt.Sprintf("EventStream (%s,%s) missing expected LDAP group '%s'; found '%s'", e.EventType, env, expectedLDAPGroup, eventStream.LdapGroup), nil)
			continue
		}
	}

}
