package projectmetadata

//go:generate mockgen -destination ../../mocks/mock_dynamodbiface/mock_dynamodbiface.go github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface DynamoDBAPI
//go:generate mockgen -destination ../../mocks/mock_client/mock_client.go github.com/aws/aws-sdk-go/aws/client ConfigProvider

import (
	"context"
	"log"
	"testing"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/dynamodb"
	"github.com/golang/mock/gomock"
	"github.com/golang/protobuf/proto"
	"github.com/pkg/errors"
	"google.golang.org/genproto/protobuf/field_mask"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"

	"code.justin.tv/dta/rockpaperscissors/internal/mocks/mock_dynamodbiface"
	"code.justin.tv/dta/rockpaperscissors/internal/testutil"
	pb "code.justin.tv/dta/rockpaperscissors/proto"
)

const (
	table = "TestTable"
)

var (
	ctx             = context.Background()
	projectMetadata = &pb.ProjectMetadata{
		ProjectId:   proto.String("project_id"),
		ProjectName: proto.String("project_name"),
		TeamName:    proto.String("team_name"),
	}
	attributeValue = map[string]*dynamodb.AttributeValue{
		"project_id":       {S: aws.String("project_id")},
		"project_name":     {S: aws.String("project_name")},
		"project_metadata": {},
		"team_name":        {S: aws.String("team_name")},
	}
	scanInput = &dynamodb.ScanInput{
		ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
			":true": {BOOL: aws.Bool(true)},
		},
		FilterExpression:     aws.String("archived <> :true"),
		ProjectionExpression: aws.String("project_id, project_name, team_name, org_name"),
		TableName:            aws.String(table),
	}
	scanInputIncludeArchived = &dynamodb.ScanInput{
		ProjectionExpression: aws.String("project_id, project_name, team_name, org_name"),
		TableName:            aws.String(table),
	}
	scanOutput = &dynamodb.ScanOutput{
		Count: aws.Int64(1),
		Items: []map[string]*dynamodb.AttributeValue{attributeValue},
	}
	getItemInput = &dynamodb.GetItemInput{
		TableName: aws.String(table),
		Key: map[string]*dynamodb.AttributeValue{
			"project_id": {S: aws.String("project_id")},
		},
		ProjectionExpression: aws.String("project_id, project_metadata"),
	}
	getItemOutput = &dynamodb.GetItemOutput{
		Item: attributeValue,
	}
	getItemOutputEmpty = &dynamodb.GetItemOutput{}
	putItemInput       = &dynamodb.PutItemInput{
		Item:      attributeValue,
		TableName: aws.String(table),
	}
	putItemOutput = &dynamodb.PutItemOutput{}
	errTest       = errors.New("OMG error")
)

func init() {
	// Stub our the "timeNow" function to return a constant time.
	now := time.Now()
	timeNow = func() time.Time { return now }

	projectMetadata.LastUpdateTimestamp = proto.Float64(convertTimestamp(now))

	projectMetadataBytes, err := proto.Marshal(projectMetadata)
	if err != nil {
		log.Fatal(err)
	}
	attributeValue["project_metadata"].B = projectMetadataBytes

	// Clear out the static data when running tests.
	StaticHeirarchy = nil
}

type mockGitHubFileGetter struct {
	FileContents []byte
	Err          error
}

func (c *mockGitHubFileGetter) GetFileFromGitHub(ctx context.Context, host, name, path string) ([]byte, error) {
	return c.FileContents, c.Err
}

func newServer(mockCtrl *gomock.Controller) (*Server, *mock_dynamodbiface.MockDynamoDBAPI) {
	mockDynamoDB := mock_dynamodbiface.NewMockDynamoDBAPI(mockCtrl)
	server := &Server{
		DynamoDBSvc:      mockDynamoDB,
		DynamoDBTable:    table,
		GitHubFileGetter: &mockGitHubFileGetter{},
	}
	return server, mockDynamoDB
}

func TestListProjects(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()
	server, mockDynamoDB := newServer(mockCtrl)

	mockDynamoDB.EXPECT().Scan(scanInput).Return(scanOutput, nil)

	req := &pb.ListProjectsRequest{}
	res, err := server.ListProjects(ctx, req)
	testutil.AssertNotNil(t, "ListProjects returned a response", res)
	testutil.AssertNil(t, "ListProjects returned a nil error", err)
	testutil.AssertNotNil(t, "ListProjects response has project list",
		res.GetProjects())
	project := res.GetProjects()[0]
	testutil.AssertEquals(t, "ListProjects response has correct ProjectId",
		project.GetProjectId(), "project_id")
	testutil.AssertEquals(t, "ListProjects response has correct ProjectName",
		project.GetProjectName(), "project_name")
}

func TestListProjectsIncludeArchived(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()
	server, mockDynamoDB := newServer(mockCtrl)

	mockDynamoDB.EXPECT().Scan(scanInputIncludeArchived).Return(scanOutput, nil)

	req := &pb.ListProjectsRequest{IncludeArchived: true}
	res, err := server.ListProjects(ctx, req)
	testutil.AssertNotNil(t, "ListProjects returned a response", res)
	testutil.AssertNil(t, "ListProjects returned a nil error", err)
}

func TestListProjectsDBError(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()
	server, mockDynamoDB := newServer(mockCtrl)

	mockDynamoDB.EXPECT().Scan(scanInputIncludeArchived).Return(nil, errTest)

	req := &pb.ListProjectsRequest{IncludeArchived: true}
	res, err := server.ListProjects(ctx, req)
	testutil.AssertNil(t, "ListProjects returned a nil response", res)
	testutil.AssertNotNil(t, "ListProjects returned an error", err)
	testutil.AssertEquals(t, "ListProjects returend an Internal error",
		grpc.Code(err), codes.Internal)
}

func TestGetMetadataNoProjectID(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()
	server, _ := newServer(mockCtrl)

	req := &pb.GetMetadataRequest{}
	res, err := server.GetMetadata(ctx, req)
	testutil.AssertNil(t, "GetMetadata returned a nil response", res)
	testutil.AssertNotNil(t, "GetMetadata returned an error", err)
	testutil.AssertEquals(t, "GetMetadata returned an InvalidArgument error",
		grpc.Code(err), codes.InvalidArgument)
}

func TestGetMetadata(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()
	server, mockDynamoDB := newServer(mockCtrl)

	mockDynamoDB.EXPECT().GetItem(getItemInput).Return(
		getItemOutput, nil)

	req := &pb.GetMetadataRequest{ProjectId: "project_id"}
	res, err := server.GetMetadata(ctx, req)
	testutil.AssertNotNil(t, "GetMetadata returned a response", res)
	testutil.AssertNil(t, "GetMetadata returned a nil error", err)
	testutil.AssertNotNil(t, "GetMetadata response has project", res.Project)
	testutil.AssertEquals(t, "GetMetadata response has correct project metadata",
		res.Project.String(), projectMetadata.String())
}

func TestGetMetadataDBError(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()
	server, mockDynamoDB := newServer(mockCtrl)

	mockDynamoDB.EXPECT().GetItem(getItemInput).Return(nil, errTest)

	req := &pb.GetMetadataRequest{ProjectId: "project_id"}
	res, err := server.GetMetadata(ctx, req)
	testutil.AssertNil(t, "GetMetadata returned a nil response", res)
	testutil.AssertNotNil(t, "GetMetadata returned an error", err)
	testutil.AssertEquals(t, "GetMetadata returned an Internal error",
		grpc.Code(err), codes.Internal)
}

func TestGetMetadataNotFound(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()
	server, mockDynamoDB := newServer(mockCtrl)

	mockDynamoDB.EXPECT().GetItem(getItemInput).Return(
		getItemOutputEmpty, nil)

	req := &pb.GetMetadataRequest{ProjectId: "project_id"}
	res, err := server.GetMetadata(ctx, req)
	testutil.AssertNil(t, "GetMetadata returned a nil response", res)
	testutil.AssertNotNil(t, "GetMetadata returned an error", err)
	testutil.AssertEquals(t, "GetMetadata returned a NotFound error",
		grpc.Code(err), codes.NotFound)
}

func TestGetMetadataBadProto(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()
	server, mockDynamoDB := newServer(mockCtrl)

	// Stick bad bytes into project_metadata to trigger an error.
	item := getItemOutput.Item
	projectMetadataBytes := item["project_metadata"].B
	item["project_metadata"].B = []byte("OMG bad proto")
	// Put the good ones back at the end of the test.
	defer func() {
		item["project_metadata"].B = projectMetadataBytes
	}()

	mockDynamoDB.EXPECT().GetItem(getItemInput).Return(
		getItemOutput, nil)

	req := &pb.GetMetadataRequest{ProjectId: "project_id"}
	res, err := server.GetMetadata(ctx, req)
	testutil.AssertNil(t, "GetMetadata returned a nil response", res)
	testutil.AssertNotNil(t, "GetMetadata returned an error", err)
	testutil.AssertEquals(t, "GetMetadata returned an Internal error",
		grpc.Code(err), codes.Internal)
}

func TestUpdateMetadata(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()
	server, mockDynamoDB := newServer(mockCtrl)

	mockDynamoDB.EXPECT().PutItem(putItemInput).Return(putItemOutput, nil)

	req := &pb.UpdateMetadataRequest{
		ProjectMetadata: projectMetadata,
	}
	res, err := server.UpdateMetadata(ctx, req)
	testutil.AssertNotNil(t, "UpdateMetadata returned a response", res)
	testutil.AssertNil(t, "UpdateMetadata returned a nil error", err)
}

func TestUpdateMetadataMissingField(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()
	server, _ := newServer(mockCtrl)

	req := &pb.UpdateMetadataRequest{}
	res, err := server.UpdateMetadata(ctx, req)
	testutil.AssertNil(t, "UpdateMetadata returned a nil response", res)
	testutil.AssertNotNil(t, "UpdateMetadata returned an error", err)
	testutil.AssertEquals(t, "UpdateMetadata returned an InvalidArgument error",
		grpc.Code(err), codes.InvalidArgument)
}

func TestUpdateMetadataDBError(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()
	server, mockDynamoDB := newServer(mockCtrl)

	mockDynamoDB.EXPECT().PutItem(putItemInput).Return(nil, errTest)

	req := &pb.UpdateMetadataRequest{
		ProjectMetadata: projectMetadata,
	}
	res, err := server.UpdateMetadata(ctx, req)
	testutil.AssertNil(t, "UpdateMetadata returned a nil response", res)
	testutil.AssertNotNil(t, "UpdateMetadata returned an error", err)
	testutil.AssertEquals(t, "UpdateMetadata returned Internal error",
		grpc.Code(err), codes.Internal)
}

func TestUpdateMetadataMasked(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()
	server, mockDynamoDB := newServer(mockCtrl)

	mockDynamoDB.EXPECT().GetItem(getItemInput).Return(getItemOutput, nil)

	expectedProjectMetadata := proto.Clone(projectMetadata).(*pb.ProjectMetadata)
	expectedProjectMetadata.ProjectId = proto.String("new_project_id")
	expectedProjectMetadata.TeamName = proto.String("new_team_name")
	expectedProjectMetadataBytes, err := proto.Marshal(expectedProjectMetadata)
	if err != nil {
		log.Fatal(err)
	}
	expectedPutItemInput := &dynamodb.PutItemInput{
		Item: map[string]*dynamodb.AttributeValue{
			"project_id":       {S: aws.String("new_project_id")},
			"project_name":     {S: aws.String("project_name")},
			"project_metadata": {B: expectedProjectMetadataBytes},
			"team_name":        {S: aws.String("new_team_name")},
		},
		TableName: aws.String(table),
	}
	mockDynamoDB.EXPECT().PutItem(expectedPutItemInput).Return(putItemOutput, nil)

	expectedDeleteItemInput := &dynamodb.DeleteItemInput{
		Key: map[string]*dynamodb.AttributeValue{
			"project_id": {S: aws.String("project_id")},
		},
		TableName: aws.String(table),
	}
	mockDynamoDB.EXPECT().DeleteItem(expectedDeleteItemInput).Return(&dynamodb.DeleteItemOutput{}, nil)

	req := &pb.UpdateMetadataRequest{
		ProjectId: "project_id",
		ProjectMetadata: &pb.ProjectMetadata{
			ProjectId: proto.String("new_project_id"),
			TeamName:  proto.String("new_team_name"),
		},
		Mask: &field_mask.FieldMask{
			Paths: []string{"project_id", "team_name"},
		},
	}
	res, err := server.UpdateMetadata(ctx, req)
	testutil.AssertNotNil(t, "UpdateMetadata returned a response", res)
	testutil.AssertNil(t, "UpdateMetadata returned a nil error", err)
}

func makeIngestBleuprintRequest(path, host, name *string) *pb.IngestBlueprintRequest {
	return &pb.IngestBlueprintRequest{
		Source: &pb.SourceFilePath{
			Path: path,
			Repository: &pb.SourceRepository{
				Repository: &pb.SourceRepository_GithubRepository{
					GithubRepository: &pb.GitHubRepository{
						Host: host,
						Name: name,
					},
				},
			},
		},
	}
}

func testIngestBlueprintValidationError(t *testing.T, path, host, name *string) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()
	server, _ := newServer(mockCtrl)
	res, err := server.IngestBlueprint(
		ctx, makeIngestBleuprintRequest(path, host, name))
	testutil.AssertNil(t, "IngestBlueprint returned a nil response", res)
	testutil.AssertNotNil(t, "IngestBlueprint returned an error", err)
	testutil.AssertEquals(t, "IngestBlueprint returned InvalidArgument error",
		grpc.Code(err), codes.InvalidArgument)
}

func TestIngestBlueprintNoPathError(t *testing.T) {
	testIngestBlueprintValidationError(
		t, nil, proto.String("host"), proto.String("name"))
}

func TestIngestBlueprintNoRepoHostError(t *testing.T) {
	testIngestBlueprintValidationError(
		t, proto.String("path"), proto.String(""), proto.String("name"))
}

func TestIngestBlueprintNoRepoNameError(t *testing.T) {
	testIngestBlueprintValidationError(
		t, proto.String("path"), proto.String("host"), nil)
}

func TestIngestBlueprintGitHubErr(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()
	server, _ := newServer(mockCtrl)
	server.GitHubFileGetter = &mockGitHubFileGetter{
		Err: errors.New("An error"),
	}
	res, err := server.IngestBlueprint(ctx, makeIngestBleuprintRequest(
		proto.String("path"), proto.String("host"), proto.String("name")))
	testutil.AssertNil(t, "IngestBlueprint returned a nil response", res)
	testutil.AssertNotNil(t, "IngestBlueprint returned an error", err)
	testutil.AssertEquals(t, "IngestBlueprint returned Internal error",
		grpc.Code(err), codes.Internal)
}

func TestIngestBlueprint(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()
	server, mockDynamoDB := newServer(mockCtrl)
	server.GitHubFileGetter = &mockGitHubFileGetter{
		FileContents: []byte(
			"project_id: \"project_id\" project_name: \"project_name\" team_name: \"team_name\""),
	}

	req := makeIngestBleuprintRequest(
		proto.String("path"), proto.String("host"), proto.String("name"))

	updatedProjectMetadata := proto.Clone(projectMetadata).(*pb.ProjectMetadata)
	updatedProjectMetadata.BlueprintLocation = req.Source
	projectMetadataBytes, err := proto.Marshal(updatedProjectMetadata)
	if err != nil {
		log.Fatal(err)
	}
	attributeValue := map[string]*dynamodb.AttributeValue{
		"project_id":       {S: aws.String("project_id")},
		"project_name":     {S: aws.String("project_name")},
		"project_metadata": {B: projectMetadataBytes},
		"team_name":        {S: aws.String("team_name")},
	}
	putItemInput := &dynamodb.PutItemInput{
		Item:      attributeValue,
		TableName: aws.String(table),
	}

	mockDynamoDB.EXPECT().PutItem(putItemInput).Return(putItemOutput, nil)

	res, err := server.IngestBlueprint(ctx, req)
	testutil.AssertNotNil(t, "IngestBlueprint returned a response", res)
	testutil.AssertNil(t, "IngestBlueprint returned a nil error", err)
}

func TestListTeams(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()
	server, mockDynamoDB := newServer(mockCtrl)

	mockDynamoDB.EXPECT().Scan(scanInput).Return(scanOutput, nil)

	res, err := server.ListTeams(ctx, &pb.ListTeamsRequest{})
	testutil.AssertNotNil(t, "ListTeams returned a response", res)
	testutil.AssertNil(t, "ListTeams returned a nil error", err)
	testutil.AssertEquals(t, "ListTeams response has one TeamEntry",
		len(res.GetTeams()), 1)
	teamEntry := res.GetTeams()[0]
	testutil.AssertEquals(t, "ListTeams response has correct TeamName",
		teamEntry.TeamName, "team_name")
	testutil.AssertEquals(t, "ListTeams response has one project",
		len(teamEntry.GetProjects()), 1)
	project := teamEntry.GetProjects()[0]
	testutil.AssertEquals(t, "ListTeams response has correct ProjectId",
		project.GetProjectId(), "project_id")
	testutil.AssertEquals(t, "ListTeams response has correct ProjectName",
		project.GetProjectName(), "project_name")
	testutil.AssertEquals(t, "ListTeams response has correct TeamName",
		project.GetTeamName(), "team_name")
}

func TestGetTeam(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()
	server, mockDynamoDB := newServer(mockCtrl)

	scanInput := &dynamodb.ScanInput{
		ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
			":team_name": {S: aws.String("team_name")},
		},
		FilterExpression:     aws.String("team_name = :team_name"),
		ProjectionExpression: aws.String("project_id, project_name, team_name, org_name"),
		TableName:            aws.String(table),
	}
	mockDynamoDB.EXPECT().Scan(scanInput).Return(scanOutput, nil)

	res, err := server.GetTeam(ctx, &pb.GetTeamRequest{
		TeamName: "team_name",
	})
	testutil.AssertNotNil(t, "GetTeam returned a response", res)
	testutil.AssertNil(t, "GetTeam returned a nil error", err)
	testutil.AssertEquals(t, "GetTeam response has one project",
		len(res.GetProjects()), 1)
	project := res.GetProjects()[0]
	testutil.AssertEquals(t, "GetTeam response has correct ProjectId",
		project.GetProjectId(), "project_id")
	testutil.AssertEquals(t, "GetTeam response has correct ProjectName",
		project.GetProjectName(), "project_name")
	testutil.AssertEquals(t, "GetTeam response has correct TeamName",
		project.GetTeamName(), "team_name")
}
