package main

import (
	"bytes"
	"compress/gzip"
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"testing"
	"time"

	"code.justin.tv/foundation/admin-panel-lambdas/internal/logparse"
	"code.justin.tv/foundation/admin-panel-lambdas/internal/mocks"
	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/cloudwatch"
	"github.com/stretchr/testify/suite"
)

type HandlerSuite struct {
	suite.Suite
	handler    *handler
	cloudwatch *mocks.CloudWatchAPI
	baseTime   time.Time
}

func (s *HandlerSuite) SetupTest() {
	s.cloudwatch = new(mocks.CloudWatchAPI)
	s.handler = &handler{
		Environment: s.Environment(),
		cloudwatch:  s.cloudwatch,
		namespace:   s.Namespace(),
	}

	s.handler.initSync.Do(func() {})
}

func (s *HandlerSuite) TearDownTest() {
	s.cloudwatch.AssertExpectations(s.T())
}

func (s HandlerSuite) Environment() string {
	return "testing"
}

func (s HandlerSuite) Namespace() string {
	return "my-namespace"
}

func (s HandlerSuite) RailsController() string {
	return "my-rails-controller"
}

func (s HandlerSuite) AdminPanelRequest() logparse.AdminPanelRequest {
	return logparse.AdminPanelRequest{
		BackendServiceCall: s.BackendServiceCall(),
		ResponseStatus:     200,
		RailsController:    s.RailsController(),
	}
}

func (s HandlerSuite) BackendServiceCall() logparse.BackendServiceCall {
	return logparse.BackendServiceCall{
		AccessDate: logparse.Time(s.baseTime),
		Latency:    logparse.Duration(s.Latency()),
		Method:     "test-method",
		URLHost:    s.URLHost(),
		URLPath:    s.URLPath(),
		URLScheme:  "test-url-scheme",
		URLQuery:   "test-url-query",
	}
}

func (s HandlerSuite) BackendServiceCallSuccess() logparse.BackendServiceCallSuccess {
	return logparse.BackendServiceCallSuccess{
		BackendServiceCall: s.BackendServiceCall(),
		ResponseStatus:     200,
	}
}

func (s HandlerSuite) BackendServiceCallError() logparse.BackendServiceCallError {
	return logparse.BackendServiceCallError{
		BackendServiceCall: s.BackendServiceCall(),
		ErrorMessage:       "error-message",
	}
}
func (s HandlerSuite) LatencySeconds() float64 {
	return 0.000005
}

func (s HandlerSuite) Latency() time.Duration {
	return time.Duration(s.LatencySeconds()*1000000) * time.Microsecond
}

func (s HandlerSuite) URLHost() string {
	return "test-url-host"
}

func (s HandlerSuite) URLPath() string {
	return "test-url-path"
}

func (s HandlerSuite) JSONMarshal(item interface{}) string {
	out, err := json.Marshal(item)
	s.Require().NoError(err)
	return string(out)
}

func (s HandlerSuite) GzippedBase64JSON(item interface{}) []byte {
	data, err := json.Marshal(item)
	s.Require().NoError(err)

	gzipOut := bytes.NewBuffer(nil)
	gzipW := gzip.NewWriter(gzipOut)
	_, err = gzipW.Write(data)
	s.Require().NoError(err)
	s.Require().NoError(gzipW.Flush())

	return []byte(base64.StdEncoding.EncodeToString(gzipOut.Bytes()))
}

func (s HandlerSuite) makeBaseDatum() *cloudwatch.MetricDatum {
	data := &cloudwatch.MetricDatum{}
	return data.
		SetStorageResolution(60).
		SetTimestamp(s.baseTime)
}

func (s HandlerSuite) TestAdminPanelRequest() {
	ctx := context.Background()

	s.cloudwatch.
		On("PutMetricDataWithContext", ctx, &cloudwatch.PutMetricDataInput{
			MetricData: []*cloudwatch.MetricDatum{
				s.makeBaseDatum().
					SetDimensions([]*cloudwatch.Dimension{
						{Name: aws.String("Environment"), Value: aws.String(s.Environment())},
						{Name: aws.String("Host"), Value: aws.String(s.URLHost())},
						{Name: aws.String("RailsController"), Value: aws.String(s.RailsController())},
					}).
					SetMetricName("HTTPCode_Backend_2XX").
					SetUnit("Count").
					SetValue(1),
				s.makeBaseDatum().
					SetDimensions([]*cloudwatch.Dimension{
						{Name: aws.String("Environment"), Value: aws.String(s.Environment())},
						{Name: aws.String("Host"), Value: aws.String(s.URLHost())},
						{Name: aws.String("RailsController"), Value: aws.String(s.RailsController())},
					}).
					SetMetricName("Latency").
					SetUnit("Seconds").
					SetValue(s.LatencySeconds()),
				s.makeBaseDatum().
					SetDimensions([]*cloudwatch.Dimension{
						{Name: aws.String("Environment"), Value: aws.String(s.Environment())},
						{Name: aws.String("Host"), Value: aws.String(s.URLHost())},
						{Name: aws.String("Path"), Value: aws.String(s.URLPath())},
					}).
					SetMetricName("HTTPCode_Backend_2XX").
					SetUnit("Count").
					SetValue(1),
				s.makeBaseDatum().
					SetDimensions([]*cloudwatch.Dimension{
						{Name: aws.String("Environment"), Value: aws.String(s.Environment())},
						{Name: aws.String("Host"), Value: aws.String(s.URLHost())},
						{Name: aws.String("Path"), Value: aws.String(s.URLPath())},
					}).
					SetMetricName("Latency").
					SetUnit("Seconds").
					SetValue(s.LatencySeconds()),
			},
			Namespace: aws.String(fmt.Sprintf("%s/ap-endpoints", s.handler.namespace)),
		}).
		Return(nil, nil)

	s.Require().NoError(s.handler.Handle(ctx, events.CloudwatchLogsEvent{
		AWSLogs: events.CloudwatchLogsRawData{
			Data: string(s.GzippedBase64JSON(events.CloudwatchLogsData{
				LogEvents: []events.CloudwatchLogsLogEvent{
					{
						Message: s.JSONMarshal(struct {
							logparse.AdminPanelRequest
							Type string `json:"_type"`
						}{s.AdminPanelRequest(), "admin_panel_request"}),
					},
				},
			})),
		},
	}))
}

func (s HandlerSuite) TestHandleBackendServiceCallSuccess() {
	ctx := context.Background()

	s.cloudwatch.
		On("PutMetricDataWithContext", ctx, &cloudwatch.PutMetricDataInput{
			MetricData: []*cloudwatch.MetricDatum{
				s.makeBaseDatum().
					SetDimensions([]*cloudwatch.Dimension{
						{Name: aws.String("Environment"), Value: aws.String(s.Environment())},
						{Name: aws.String("Host"), Value: aws.String(s.URLHost())},
					}).
					SetMetricName("HTTPCode_Backend_2XX").
					SetUnit("Count").
					SetValue(1),
				s.makeBaseDatum().
					SetDimensions([]*cloudwatch.Dimension{
						{Name: aws.String("Environment"), Value: aws.String(s.Environment())},
						{Name: aws.String("Host"), Value: aws.String(s.URLHost())},
					}).
					SetMetricName("Latency").
					SetUnit("Seconds").
					SetValue(s.LatencySeconds()),
				s.makeBaseDatum().
					SetDimensions([]*cloudwatch.Dimension{
						{Name: aws.String("Environment"), Value: aws.String(s.Environment())},
						{Name: aws.String("Host"), Value: aws.String(s.URLHost())},
						{Name: aws.String("Path"), Value: aws.String(s.URLPath())},
					}).
					SetMetricName("HTTPCode_Backend_2XX").
					SetUnit("Count").
					SetValue(1),
				s.makeBaseDatum().
					SetDimensions([]*cloudwatch.Dimension{
						{Name: aws.String("Environment"), Value: aws.String(s.Environment())},
						{Name: aws.String("Host"), Value: aws.String(s.URLHost())},
						{Name: aws.String("Path"), Value: aws.String(s.URLPath())},
					}).
					SetMetricName("Latency").
					SetUnit("Seconds").
					SetValue(s.LatencySeconds()),
			},
			Namespace: aws.String(fmt.Sprintf("%s/backend-success", s.handler.namespace)),
		}).
		Return(nil, nil)

	s.Require().NoError(s.handler.Handle(ctx, events.CloudwatchLogsEvent{
		AWSLogs: events.CloudwatchLogsRawData{
			Data: string(s.GzippedBase64JSON(events.CloudwatchLogsData{
				LogEvents: []events.CloudwatchLogsLogEvent{
					{
						Message: s.JSONMarshal(struct {
							logparse.BackendServiceCallSuccess
							Type string `json:"_type"`
						}{s.BackendServiceCallSuccess(), "backend_service_call_success"}),
					},
				},
			})),
		},
	}))
}

func (s HandlerSuite) TestHandleBackendServiceCallError() {
	ctx := context.Background()

	s.cloudwatch.
		On("PutMetricDataWithContext", ctx, &cloudwatch.PutMetricDataInput{
			MetricData: []*cloudwatch.MetricDatum{
				s.makeBaseDatum().
					SetDimensions([]*cloudwatch.Dimension{
						{Name: aws.String("Environment"), Value: aws.String(s.Environment())},
						{Name: aws.String("Host"), Value: aws.String(s.URLHost())},
					}).
					SetMetricName("BackendConnectionErrors").
					SetUnit("Count").
					SetValue(1),
				s.makeBaseDatum().
					SetDimensions([]*cloudwatch.Dimension{
						{Name: aws.String("Environment"), Value: aws.String(s.Environment())},
						{Name: aws.String("Host"), Value: aws.String(s.URLHost())},
					}).
					SetMetricName("error-message").
					SetUnit("Count").
					SetValue(1),
				s.makeBaseDatum().
					SetDimensions([]*cloudwatch.Dimension{
						{Name: aws.String("Environment"), Value: aws.String(s.Environment())},
						{Name: aws.String("Host"), Value: aws.String(s.URLHost())},
						{Name: aws.String("Path"), Value: aws.String(s.URLPath())},
					}).
					SetMetricName("BackendConnectionErrors").
					SetUnit("Count").
					SetValue(1),
				s.makeBaseDatum().
					SetDimensions([]*cloudwatch.Dimension{
						{Name: aws.String("Environment"), Value: aws.String(s.Environment())},
						{Name: aws.String("Host"), Value: aws.String(s.URLHost())},
						{Name: aws.String("Path"), Value: aws.String(s.URLPath())},
					}).
					SetMetricName("error-message").
					SetUnit("Count").
					SetValue(1),
			},
			Namespace: aws.String(fmt.Sprintf("%s/backend-error", s.handler.namespace)),
		}).
		Return(nil, nil)

	s.Require().NoError(s.handler.Handle(ctx, events.CloudwatchLogsEvent{
		AWSLogs: events.CloudwatchLogsRawData{
			Data: string(s.GzippedBase64JSON(events.CloudwatchLogsData{
				LogEvents: []events.CloudwatchLogsLogEvent{
					{
						Message: s.JSONMarshal(struct {
							logparse.BackendServiceCallError
							Type string `json:"_type"`
						}{s.BackendServiceCallError(), "backend_service_call_error"}),
					},
				},
			})),
		},
	}))
}

func TestHandler(t *testing.T) {
	suite.Run(t, &HandlerSuite{})
}
