package audit

import (
	"context"
	"encoding/json"
	"errors"
	"testing"

	"code.justin.tv/foundation/history-admin/internal/mocks"
	"github.com/aws/aws-sdk-go/service/dynamodb"
	"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/suite"
)

type ClientSuite struct {
	suite.Suite

	client *Client
	mockDB *mocks.DynamoDBAPI
}

func (s *ClientSuite) SetupTest() {
	s.mockDB = new(mocks.DynamoDBAPI)
	s.client = &Client{
		db:        s.mockDB,
		tableName: "mock",
	}
	s.client.initOnce.Do(func() {})
}

func (s *ClientSuite) TearDownTest() {
	s.mockDB.AssertExpectations(s.T())
}

type ScanAuditsSuite struct {
	ClientSuite
}

func (s *ScanAuditsSuite) testAudit() *Audit {
	return &Audit{
		UUID:   "my-uuid",
		Action: "my-action",
	}
}

func (s *ScanAuditsSuite) testAuditJSONEncoded() []byte {
	jsonAudit, err := json.Marshal(s.testAudit())
	s.Require().NoError(err)
	return jsonAudit
}

func (s *ScanAuditsSuite) testAuditDynamoMarshalled() map[string]*dynamodb.AttributeValue {
	dynamoAudit, err := dynamodbattribute.MarshalMap(struct {
		Body string `dynamodbav:"body"`
	}{string(s.testAuditJSONEncoded())})
	s.Require().NoError(err)
	return dynamoAudit
}

func (s *ScanAuditsSuite) TestLastItem() {
	s.mockDB.On(
		"ScanPagesWithContext", mock.Anything, mock.Anything, mock.Anything,
	).Run(func(args mock.Arguments) {
		// mock to return a single item in a scan with no additional items
		handler := args[2].(func(*dynamodb.ScanOutput, bool) bool)
		s.Assert().False(handler(&dynamodb.ScanOutput{
			Items: []map[string]*dynamodb.AttributeValue{
				s.testAuditDynamoMarshalled(),
			},
		}, true), "should NOT request to continue")
	}).Return(nil).Once()

	calls := make([]*Audit, 0)
	s.Require().NoError(s.client.ScanAudits(
		context.Background(),
		func(a *Audit) bool {
			calls = append(calls, a)
			return true
		},
	))
	s.Assert().Equal([]*Audit{s.testAudit()}, calls)
}

func (s *ScanAuditsSuite) TestNotLastItem() {
	s.mockDB.On(
		"ScanPagesWithContext", mock.Anything, mock.Anything, mock.Anything,
	).Run(func(args mock.Arguments) {
		// mock to return a single item in a scan with no additional items
		handler := args[2].(func(*dynamodb.ScanOutput, bool) bool)
		s.Assert().True(handler(&dynamodb.ScanOutput{
			Items: []map[string]*dynamodb.AttributeValue{
				s.testAuditDynamoMarshalled(),
			},
		}, false), "should request to continue")
	}).Return(nil).Once()

	calls := make([]*Audit, 0)
	s.Require().NoError(s.client.ScanAudits(
		context.Background(),
		func(a *Audit) bool {
			calls = append(calls, a)
			return true
		},
	))
	s.Assert().Equal([]*Audit{s.testAudit()}, calls)
}

func (s *ScanAuditsSuite) TestStop() {
	s.mockDB.On(
		"ScanPagesWithContext", mock.Anything, mock.Anything, mock.Anything,
	).Run(func(args mock.Arguments) {
		// mock to return a single item in a scan with no additional items
		handler := args[2].(func(*dynamodb.ScanOutput, bool) bool)
		s.Assert().False(handler(&dynamodb.ScanOutput{
			Items: []map[string]*dynamodb.AttributeValue{
				s.testAuditDynamoMarshalled(),
			},
		}, false), "should stop because handler requested to stop")
	}).Return(nil).Once()

	calls := make([]*Audit, 0)
	s.Require().NoError(s.client.ScanAudits(
		context.Background(),
		func(a *Audit) bool {
			calls = append(calls, a)
			return false
		},
	))
	s.Assert().Equal([]*Audit{s.testAudit()}, calls)
}

func (s *ScanAuditsSuite) TestDynamoError() {
	dynamoErr := errors.New("my-dynamo-error")
	s.mockDB.On(
		"ScanPagesWithContext", mock.Anything, mock.Anything, mock.Anything,
	).Return(dynamoErr).Once()

	s.Assert().Equal(dynamoErr, s.client.ScanAudits(
		context.Background(),
		func(a *Audit) bool {
			return true
		},
	))
}

type DeleteAuditsSuite struct {
	ClientSuite
}

func (s *DeleteAuditsSuite) TestSingleBatch() {
	done := make(chan interface{})

	s.mockDB.On(
		"BatchWriteItemWithContext", mock.Anything, mock.Anything,
	).Run(func(args mock.Arguments) {
		input := args.Get(1).(*dynamodb.BatchWriteItemInput)
		s.Assert().Len(input.RequestItems[s.client.tableName], 3)
		done <- nil
	}).Return(&dynamodb.BatchWriteItemOutput{}, nil).Once()

	s.Assert().NoError(s.client.DeleteAudits(
		context.Background(),
		&arrayIDStream{IDs: []string{"a", "b", "c"}},
	))

	<-done
}

func TestClient(t *testing.T) {
	suite.Run(t, &ScanAuditsSuite{})
	suite.Run(t, &DeleteAuditsSuite{})
}
