// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package ecr

import (
	"encoding/base64"
	"errors"
	"fmt"
	"testing"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/service/ecr"
	"github.com/aws/copilot-cli/internal/pkg/aws/ecr/mocks"
	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/require"
)

func TestAuth(t *testing.T) {
	mockError := errors.New("error")

	mockUsername := "mockUsername"
	mockPassword := "mockPassword"

	encoded := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", mockUsername, mockPassword)))

	testCases := map[string]struct {
		mockECRClient func(m *mocks.Mockapi)

		wantedUsername string
		wantedPassword string
		wantErr        error
	}{
		"should return wrapped error given error returned from GetAuthorizationToken": {
			mockECRClient: func(m *mocks.Mockapi) {
				m.EXPECT().GetAuthorizationToken(gomock.Any()).Return(nil, mockError)
			},
			wantErr: fmt.Errorf("get ECR auth: %w", mockError),
		},
		"should return Auth data": {
			mockECRClient: func(m *mocks.Mockapi) {
				m.EXPECT().GetAuthorizationToken(gomock.Any()).Return(&ecr.GetAuthorizationTokenOutput{
					AuthorizationData: []*ecr.AuthorizationData{
						{
							AuthorizationToken: aws.String(encoded),
						},
					},
				}, nil)
			},
			wantedUsername: mockUsername,
			wantedPassword: mockPassword,
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			// GIVEN
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			mockECRAPI := mocks.NewMockapi(ctrl)
			tc.mockECRClient(mockECRAPI)

			client := ECR{
				mockECRAPI,
			}

			gotUsername, gotPassword, gotErr := client.Auth()

			require.Equal(t, tc.wantedUsername, gotUsername)
			require.Equal(t, tc.wantedPassword, gotPassword)
			require.Equal(t, tc.wantErr, gotErr)
		})

	}
}

func TestRepositoryURI(t *testing.T) {
	mockError := errors.New("error")

	mockRepoName := "mockRepoName"
	mockRepoURI := "mockRepoURI"

	testCases := map[string]struct {
		mockECRClient func(m *mocks.Mockapi)

		wantURI string
		wantErr error
	}{
		"should return wrapped error given error returned from DescribeRepositories": {
			mockECRClient: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeRepositories(gomock.Any()).Return(nil, mockError)
			},
			wantErr: fmt.Errorf("ecr describe repository %s: %w", mockRepoName, mockError),
		},
		"should return error given no repositories returned in list": {
			mockECRClient: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeRepositories(&ecr.DescribeRepositoriesInput{
					RepositoryNames: aws.StringSlice([]string{mockRepoName}),
				}).Return(&ecr.DescribeRepositoriesOutput{
					Repositories: []*ecr.Repository{},
				}, nil)
			},
			wantErr: errors.New("no repositories found"),
		},
		"should return repository URI": {
			mockECRClient: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeRepositories(&ecr.DescribeRepositoriesInput{
					RepositoryNames: aws.StringSlice([]string{mockRepoName}),
				}).Return(&ecr.DescribeRepositoriesOutput{
					Repositories: []*ecr.Repository{
						{
							RepositoryUri: aws.String(mockRepoURI),
						},
					},
				}, nil)
			},
			wantURI: mockRepoURI,
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			// GIVEN
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			mockECRAPI := mocks.NewMockapi(ctrl)
			tc.mockECRClient(mockECRAPI)

			client := ECR{
				mockECRAPI,
			}

			gotURI, gotErr := client.RepositoryURI(mockRepoName)

			require.Equal(t, tc.wantURI, gotURI)
			require.Equal(t, tc.wantErr, gotErr)
		})
	}
}

func TestURIFromARN(t *testing.T) {

	testCases := map[string]struct {
		givenARN  string
		wantedURI string
		wantErr   error
	}{
		"valid arn": {
			givenARN:  "arn:aws:ecr:us-west-2:0123456789:repository/myrepo",
			wantedURI: "0123456789.dkr.ecr.us-west-2.amazonaws.com/myrepo",
		},
		"valid arn in china partition": {
			givenARN:  "arn:aws-cn:ecr:cn-north-1:0123456789:repository/myrepo",
			wantedURI: "0123456789.dkr.ecr.cn-north-1.amazonaws.com.cn/myrepo",
		},
		"valid arn with namespace": {
			givenARN:  "arn:aws:ecr:us-west-2:0123456789:repository/myproject/myapp",
			wantedURI: "0123456789.dkr.ecr.us-west-2.amazonaws.com/myproject/myapp",
		},
		"separate region": {
			givenARN:  "arn:aws:ecr:us-east-1:0123456789:repository/myproject/myapp",
			wantedURI: "0123456789.dkr.ecr.us-east-1.amazonaws.com/myproject/myapp",
		},
		"invalid ARN": {
			givenARN: "myproject/myapp",
			wantErr:  fmt.Errorf("parsing repository ARN myproject/myapp: arn: invalid prefix"),
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			uri, err := URIFromARN(tc.givenARN)
			if tc.wantErr != nil {
				require.Error(t, err)
				require.EqualError(t, err, tc.wantErr.Error())
			} else {
				require.Equal(t, tc.wantedURI, uri)
			}
		})
	}
}

func TestListImages(t *testing.T) {
	mockRepoName := "mockRepoName"
	mockError := errors.New("mockError")
	mockDigest := "mockDigest"
	mockNextToken := "next"

	tests := map[string]struct {
		mockECRClient func(m *mocks.Mockapi)

		wantImages []Image
		wantError  error
	}{
		"should wrap error returned by ECR DescribeImages": {
			mockECRClient: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeImages(gomock.Any()).Return(nil, mockError)
			},
			wantImages: nil,
			wantError:  fmt.Errorf("ecr repo %s describe images: %w", mockRepoName, mockError),
		},
		"should return Image list": {
			mockECRClient: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeImages(gomock.Any()).Return(&ecr.DescribeImagesOutput{
					ImageDetails: []*ecr.ImageDetail{
						{
							ImageDigest: aws.String(mockDigest),
						},
					},
				}, nil)
			},
			wantImages: []Image{{Digest: mockDigest}},
			wantError:  nil,
		},
		"should return all images when paginated": {
			mockECRClient: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeImages(&ecr.DescribeImagesInput{
					RepositoryName: aws.String(mockRepoName),
				}).Return(&ecr.DescribeImagesOutput{
					ImageDetails: []*ecr.ImageDetail{
						{
							ImageDigest: aws.String(mockDigest),
						},
					},
					NextToken: &mockNextToken,
				}, nil)
				m.EXPECT().DescribeImages(&ecr.DescribeImagesInput{
					RepositoryName: aws.String(mockRepoName),
					NextToken:      &mockNextToken,
				}).Return(&ecr.DescribeImagesOutput{
					ImageDetails: []*ecr.ImageDetail{
						{
							ImageDigest: aws.String(mockDigest),
						},
					},
				}, nil)
			},
			wantImages: []Image{{Digest: mockDigest}, {Digest: mockDigest}},
			wantError:  nil,
		},
	}

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			// GIVEN
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			mockECRAPI := mocks.NewMockapi(ctrl)
			tc.mockECRClient(mockECRAPI)

			client := ECR{
				mockECRAPI,
			}

			gotImages, gotError := client.ListImages(mockRepoName)

			require.ElementsMatch(t, tc.wantImages, gotImages)
			require.Equal(t, tc.wantError, gotError)
		})
	}
}

func TestDeleteImages(t *testing.T) {
	mockRepoName := "mockRepoName"
	mockError := errors.New("mockError")
	mockDigest := "mockDigest"
	mockImages := []Image{
		{
			Digest: mockDigest,
		},
	}
	mockFailCode := "400"
	mockFailReason := "some reason"
	// with only one image identifier
	var imageIdentifiers []*ecr.ImageIdentifier
	for _, image := range mockImages {
		imageIdentifiers = append(imageIdentifiers, image.imageIdentifier())
	}
	var mockBatchImages []Image
	for ii := 0; ii < batchDeleteLimit+1; ii++ {
		mockBatchImages = append(mockBatchImages, mockImages[0])
	}
	// with a batch limit number of image identifiers
	var batchImageIdentifiers []*ecr.ImageIdentifier
	for ii := 0; ii < batchDeleteLimit; ii++ {
		batchImageIdentifiers = append(batchImageIdentifiers, mockImages[0].imageIdentifier())
	}

	tests := map[string]struct {
		images        []Image
		mockECRClient func(m *mocks.Mockapi)

		wantError error
	}{
		"should not return error if input Image list is empty": {
			images:        nil,
			mockECRClient: func(m *mocks.Mockapi) {},
			wantError:     nil,
		},
		"should wrap error return from BatchDeleteImage": {
			images: mockImages,
			mockECRClient: func(m *mocks.Mockapi) {
				m.EXPECT().BatchDeleteImage(gomock.Any()).Return(nil, mockError)
			},
			wantError: fmt.Errorf("ecr repo %s batch delete image: %w", mockRepoName, mockError),
		},
		"should return nil if call to BatchDeleteImage successful": {
			images: mockImages,
			mockECRClient: func(m *mocks.Mockapi) {
				m.EXPECT().BatchDeleteImage(&ecr.BatchDeleteImageInput{
					RepositoryName: aws.String(mockRepoName),
					ImageIds:       imageIdentifiers,
				}).Return(&ecr.BatchDeleteImageOutput{}, nil)
			},
			wantError: nil,
		},
		fmt.Sprintf("should be able to batch delete more than %d images", batchDeleteLimit): {
			images: mockBatchImages,
			mockECRClient: func(m *mocks.Mockapi) {
				m.EXPECT().BatchDeleteImage(&ecr.BatchDeleteImageInput{
					RepositoryName: aws.String(mockRepoName),
					ImageIds:       batchImageIdentifiers,
				}).Return(&ecr.BatchDeleteImageOutput{}, nil).Times(1)
				m.EXPECT().BatchDeleteImage(&ecr.BatchDeleteImageInput{
					RepositoryName: aws.String(mockRepoName),
					ImageIds:       imageIdentifiers,
				}).Return(&ecr.BatchDeleteImageOutput{}, nil).Times(1)
			},
			wantError: nil,
		},
		"warns if fail to delete some images": {
			images: mockImages,
			mockECRClient: func(m *mocks.Mockapi) {
				m.EXPECT().BatchDeleteImage(&ecr.BatchDeleteImageInput{
					RepositoryName: aws.String(mockRepoName),
					ImageIds:       imageIdentifiers,
				}).Return(&ecr.BatchDeleteImageOutput{
					Failures: []*ecr.ImageFailure{
						{
							FailureCode:   &mockFailCode,
							FailureReason: &mockFailReason,
							ImageId:       imageIdentifiers[0],
						},
					},
				}, nil)
			},
			wantError: nil,
		},
	}

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			// GIVEN
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			mockECRAPI := mocks.NewMockapi(ctrl)
			tc.mockECRClient(mockECRAPI)

			client := ECR{
				mockECRAPI,
			}

			got := client.DeleteImages(tc.images, mockRepoName)

			require.Equal(t, tc.wantError, got)
		})
	}
}

func TestClearRepository(t *testing.T) {
	mockRepoName := "mockRepoName"
	mockAwsError := awserr.New("someErrorCode", "some error", nil)
	mockError := errors.New("some error")
	mockRepoNotFoundError := awserr.New("RepositoryNotFoundException", "some error", nil)
	mockDigest := "mockDigest"
	mockImageID := ecr.ImageIdentifier{
		ImageDigest: aws.String(mockDigest),
	}

	tests := map[string]struct {
		mockECRClient func(m *mocks.Mockapi)

		wantError error
	}{
		"should clear repo if exists": {
			mockECRClient: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeImages(&ecr.DescribeImagesInput{
					RepositoryName: aws.String(mockRepoName),
				}).Return(&ecr.DescribeImagesOutput{
					ImageDetails: []*ecr.ImageDetail{
						{
							ImageDigest: aws.String(mockDigest),
						},
					},
				}, nil)
				m.EXPECT().BatchDeleteImage(&ecr.BatchDeleteImageInput{
					RepositoryName: aws.String(mockRepoName),
					ImageIds:       []*ecr.ImageIdentifier{&mockImageID},
				}).Return(&ecr.BatchDeleteImageOutput{}, nil)
			},
			wantError: nil,
		},
		"returns nil if repo not exists": {
			mockECRClient: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeImages(&ecr.DescribeImagesInput{
					RepositoryName: aws.String(mockRepoName),
				}).Return(nil, mockRepoNotFoundError)
			},
			wantError: nil,
		},
		"returns error if fail to check repo existence": {
			mockECRClient: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeImages(&ecr.DescribeImagesInput{
					RepositoryName: aws.String(mockRepoName),
				}).Return(nil, mockAwsError)
			},
			wantError: fmt.Errorf("ecr repo mockRepoName describe images: %w", mockAwsError),
		},
		"returns error if fail to check repo existence because of non-awserr error type": {
			mockECRClient: func(m *mocks.Mockapi) {
				m.EXPECT().DescribeImages(&ecr.DescribeImagesInput{
					RepositoryName: aws.String(mockRepoName),
				}).Return(nil, mockError)
			},
			wantError: fmt.Errorf("ecr repo mockRepoName describe images: %w", mockError),
		},
	}

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			// GIVEN
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			mockECRAPI := mocks.NewMockapi(ctrl)
			tc.mockECRClient(mockECRAPI)

			client := ECR{
				mockECRAPI,
			}

			gotError := client.ClearRepository(mockRepoName)

			require.Equal(t, tc.wantError, gotError)
		})
	}
}
