package scm_provider

import (
	"errors"
	"sort"
	"testing"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/codecommit"
	codecommittypes "github.com/aws/aws-sdk-go-v2/service/codecommit/types"
	"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
	rgsatypes "github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi/types"
	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"

	"github.com/argoproj/argo-cd/v3/applicationset/services/scm_provider/mocks"
	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
)

type awsCodeCommitTestRepository struct {
	name                     string
	id                       string
	arn                      string
	accountId                string
	defaultBranch            string
	expectedCloneURL         string
	getRepositoryError       error
	getRepositoryNilMetadata bool
	valid                    bool
}

func TestAWSCodeCommitListRepos(t *testing.T) {
	t.Parallel()
	testCases := []struct {
		name                   string
		repositories           []*awsCodeCommitTestRepository
		cloneProtocol          string
		tagFilters             []*v1alpha1.TagFilter
		expectTagFilters       []rgsatypes.TagFilter
		listRepositoryError    error
		expectOverallError     bool
		expectListAtCodeCommit bool
	}{
		{
			name:          "ListRepos by tag with https",
			cloneProtocol: "https",
			repositories: []*awsCodeCommitTestRepository{
				{
					name:             "repo1",
					id:               "8235624d-d248-4df9-a983-2558b01dbe83",
					arn:              "arn:aws:codecommit:us-east-1:111111111111:repo1",
					defaultBranch:    "main",
					expectedCloneURL: "https://git-codecommit.us-east-1.amazonaws.com/v1/repos/repo1",
					valid:            true,
				},
			},
			tagFilters: []*v1alpha1.TagFilter{
				{Key: "key1", Value: "value1"},
				{Key: "key1", Value: "value2"},
				{Key: "key2"},
			},
			expectTagFilters: []rgsatypes.TagFilter{
				{Key: aws.String("key1"), Values: []string{"value1", "value2"}},
				{Key: aws.String("key2")},
			},
			expectOverallError:     false,
			expectListAtCodeCommit: false,
		},
		{
			name:          "ListRepos by tag with https-fips",
			cloneProtocol: "https-fips",
			repositories: []*awsCodeCommitTestRepository{
				{
					name:             "repo1",
					id:               "8235624d-d248-4df9-a983-2558b01dbe83",
					arn:              "arn:aws:codecommit:us-east-1:111111111111:repo1",
					defaultBranch:    "main",
					expectedCloneURL: "https://git-codecommit-fips.us-east-1.amazonaws.com/v1/repos/repo1",
					valid:            true,
				},
			},
			tagFilters: []*v1alpha1.TagFilter{
				{Key: "key1"},
			},
			expectTagFilters: []rgsatypes.TagFilter{
				{Key: aws.String("key1")},
			},
			expectOverallError:     false,
			expectListAtCodeCommit: false,
		},
		{
			name:          "ListRepos without tag with invalid repo",
			cloneProtocol: "ssh",
			repositories: []*awsCodeCommitTestRepository{
				{
					name:             "repo1",
					id:               "8235624d-d248-4df9-a983-2558b01dbe83",
					arn:              "arn:aws:codecommit:us-east-1:111111111111:repo1",
					defaultBranch:    "main",
					expectedCloneURL: "ssh://git-codecommit.us-east-1.amazonaws.com/v1/repos/repo1",
					valid:            true,
				},
				{
					name:  "repo2",
					id:    "640d5859-d265-4e27-a9fa-e0731eb13ed7",
					arn:   "arn:aws:codecommit:us-east-1:111111111111:repo2",
					valid: false,
				},
				{
					name:                     "repo3-nil-metadata",
					id:                       "24a6ee96-d3a0-4be6-a595-c5e5b1ab1617",
					arn:                      "arn:aws:codecommit:us-east-1:111111111111:repo3-nil-metadata",
					getRepositoryNilMetadata: true,
					valid:                    false,
				},
			},
			expectOverallError:     false,
			expectListAtCodeCommit: true,
		},
		{
			name:          "ListRepos with invalid protocol",
			cloneProtocol: "invalid-protocol",
			repositories: []*awsCodeCommitTestRepository{
				{
					name:          "repo1",
					id:            "8235624d-d248-4df9-a983-2558b01dbe83",
					arn:           "arn:aws:codecommit:us-east-1:111111111111:repo1",
					defaultBranch: "main",
					valid:         true,
				},
			},
			expectOverallError:     true,
			expectListAtCodeCommit: true,
		},
		{
			name:                   "ListRepos error on listRepos",
			cloneProtocol:          "https",
			listRepositoryError:    errors.New("list repo error"),
			expectOverallError:     true,
			expectListAtCodeCommit: true,
		},
		{
			name:          "ListRepos error on getRepo",
			cloneProtocol: "https",
			repositories: []*awsCodeCommitTestRepository{
				{
					name:               "repo1",
					id:                 "8235624d-d248-4df9-a983-2558b01dbe83",
					arn:                "arn:aws:codecommit:us-east-1:111111111111:repo1",
					defaultBranch:      "main",
					getRepositoryError: errors.New("get repo error"),
					valid:              true,
				},
			},
			expectOverallError:     true,
			expectListAtCodeCommit: true,
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			t.Parallel()
			codeCommitClient := mocks.NewAWSCodeCommitClient(t)
			taggingClient := mocks.NewAWSTaggingClient(t)
			ctx := t.Context()
			codecommitRepoNameIdPairs := make([]codecommittypes.RepositoryNameIdPair, 0)
			resourceTaggings := make([]rgsatypes.ResourceTagMapping, 0)
			validRepositories := make([]*awsCodeCommitTestRepository, 0)

			for _, repo := range testCase.repositories {
				repoMetadata := &codecommittypes.RepositoryMetadata{
					AccountId:      aws.String(repo.accountId),
					Arn:            aws.String(repo.arn),
					CloneUrlHttp:   aws.String("https://git-codecommit.us-east-1.amazonaws.com/v1/repos/" + repo.name),
					CloneUrlSsh:    aws.String("ssh://git-codecommit.us-east-1.amazonaws.com/v1/repos/" + repo.name),
					DefaultBranch:  aws.String(repo.defaultBranch),
					RepositoryId:   aws.String(repo.id),
					RepositoryName: aws.String(repo.name),
				}
				if repo.getRepositoryNilMetadata {
					repoMetadata = nil
				}
				codeCommitClient.EXPECT().GetRepository(mock.Anything, &codecommit.GetRepositoryInput{RepositoryName: aws.String(repo.name)}).
					Return(&codecommit.GetRepositoryOutput{RepositoryMetadata: repoMetadata}, repo.getRepositoryError).Maybe()
				codecommitRepoNameIdPairs = append(codecommitRepoNameIdPairs, codecommittypes.RepositoryNameIdPair{
					RepositoryId:   aws.String(repo.id),
					RepositoryName: aws.String(repo.name),
				})
				resourceTaggings = append(resourceTaggings, rgsatypes.ResourceTagMapping{
					ResourceARN: aws.String(repo.arn),
				})
				if repo.valid {
					validRepositories = append(validRepositories, repo)
				}
			}

			if testCase.expectListAtCodeCommit {
				codeCommitClient.EXPECT().ListRepositories(mock.Anything, &codecommit.ListRepositoriesInput{}).
					Return(&codecommit.ListRepositoriesOutput{
						Repositories: codecommitRepoNameIdPairs,
					}, testCase.listRepositoryError).Maybe()
			} else {
				taggingClient.EXPECT().GetResources(mock.Anything, mock.MatchedBy(equalIgnoringTagFilterOrder(&resourcegroupstaggingapi.GetResourcesInput{
					TagFilters:          testCase.expectTagFilters,
					ResourceTypeFilters: []string{resourceTypeCodeCommitRepository},
				}))).
					Return(&resourcegroupstaggingapi.GetResourcesOutput{
						ResourceTagMappingList: resourceTaggings,
					}, testCase.listRepositoryError).Maybe()
			}

			provider := &AWSCodeCommitProvider{
				codeCommitClient: codeCommitClient,
				taggingClient:    taggingClient,
				tagFilters:       testCase.tagFilters,
			}
			repos, err := provider.ListRepos(ctx, testCase.cloneProtocol)
			if testCase.expectOverallError {
				assert.Error(t, err)
			} else {
				assert.Len(t, repos, len(validRepositories))
				for i, repo := range repos {
					originRepo := validRepositories[i]
					assert.Equal(t, originRepo.accountId, repo.Organization)
					assert.Equal(t, originRepo.name, repo.Repository)
					assert.Equal(t, originRepo.id, repo.RepositoryId)
					assert.Equal(t, originRepo.defaultBranch, repo.Branch)
					assert.Equal(t, originRepo.expectedCloneURL, repo.URL)
					assert.Empty(t, repo.SHA, "SHA is always empty")
				}
			}
		})
	}
}

func TestAWSCodeCommitRepoHasPath(t *testing.T) {
	t.Parallel()
	organization := "111111111111"
	repoName := "repo1"
	branch := "main"

	testCases := []struct {
		name                  string
		path                  string
		expectedGetFolderPath string
		getFolderOutput       *codecommit.GetFolderOutput
		getFolderError        error
		expectOverallError    bool
		expectedResult        bool
	}{
		{
			name:                  "RepoHasPath on regular file",
			path:                  "lib/config.yaml",
			expectedGetFolderPath: "/lib",
			getFolderOutput: &codecommit.GetFolderOutput{
				Files: []codecommittypes.File{
					{RelativePath: aws.String("config.yaml")},
				},
			},
			expectOverallError: false,
			expectedResult:     true,
		},
		{
			name:                  "RepoHasPath on folder",
			path:                  "lib/config",
			expectedGetFolderPath: "/lib",
			getFolderOutput: &codecommit.GetFolderOutput{
				SubFolders: []codecommittypes.Folder{
					{RelativePath: aws.String("config")},
				},
			},
			expectOverallError: false,
			expectedResult:     true,
		},
		{
			name:                  "RepoHasPath on submodules",
			path:                  "/lib/submodule/",
			expectedGetFolderPath: "/lib",
			getFolderOutput: &codecommit.GetFolderOutput{
				SubModules: []codecommittypes.SubModule{
					{RelativePath: aws.String("submodule")},
				},
			},
			expectOverallError: false,
			expectedResult:     true,
		},
		{
			name:                  "RepoHasPath on symlink",
			path:                  "./lib/service.json",
			expectedGetFolderPath: "/lib",
			getFolderOutput: &codecommit.GetFolderOutput{
				SymbolicLinks: []codecommittypes.SymbolicLink{
					{RelativePath: aws.String("service.json")},
				},
			},
			expectOverallError: false,
			expectedResult:     true,
		},
		{
			name:                  "RepoHasPath when no match",
			path:                  "no-match.json",
			expectedGetFolderPath: "/",
			getFolderOutput: &codecommit.GetFolderOutput{
				Files: []codecommittypes.File{
					{RelativePath: aws.String("config.yaml")},
				},
				SubFolders: []codecommittypes.Folder{
					{RelativePath: aws.String("config")},
				},
				SubModules: []codecommittypes.SubModule{
					{RelativePath: aws.String("submodule")},
				},
				SymbolicLinks: []codecommittypes.SymbolicLink{
					{RelativePath: aws.String("service.json")},
				},
			},
			expectOverallError: false,
			expectedResult:     false,
		},
		{
			name:                  "RepoHasPath when parent folder not found",
			path:                  "lib/submodule",
			expectedGetFolderPath: "/lib",
			getFolderError:        &codecommittypes.FolderDoesNotExistException{},
			expectOverallError:    false,
		},
		{
			name:                  "RepoHasPath when unknown error",
			path:                  "lib/submodule",
			expectedGetFolderPath: "/lib",
			getFolderError:        errors.New("unknown error"),
			expectOverallError:    true,
		},
		{
			name:               "RepoHasPath on root folder - './'",
			path:               "./",
			expectOverallError: false,
			expectedResult:     true,
		},
		{
			name:               "RepoHasPath on root folder - '/'",
			path:               "/",
			expectOverallError: false,
			expectedResult:     true,
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			t.Parallel()
			codeCommitClient := mocks.NewAWSCodeCommitClient(t)
			taggingClient := mocks.NewAWSTaggingClient(t)
			ctx := t.Context()
			if testCase.expectedGetFolderPath != "" {
				codeCommitClient.EXPECT().GetFolder(mock.Anything, &codecommit.GetFolderInput{
					CommitSpecifier: aws.String(branch),
					FolderPath:      aws.String(testCase.expectedGetFolderPath),
					RepositoryName:  aws.String(repoName),
				}).
					Return(testCase.getFolderOutput, testCase.getFolderError).Maybe()
			}
			provider := &AWSCodeCommitProvider{
				codeCommitClient: codeCommitClient,
				taggingClient:    taggingClient,
			}
			actual, err := provider.RepoHasPath(ctx, &Repository{
				Organization: organization,
				Repository:   repoName,
				Branch:       branch,
			}, testCase.path)
			if testCase.expectOverallError {
				assert.Error(t, err)
			} else {
				assert.Equal(t, testCase.expectedResult, actual)
			}
		})
	}
}

func TestAWSCodeCommitGetBranches(t *testing.T) {
	t.Parallel()
	name := "repo1"
	id := "1a64adc4-2fb5-4abd-afe7-127984ba83c0"
	defaultBranch := "main"
	organization := "111111111111"
	cloneURL := "https://git-codecommit.us-east-1.amazonaws.com/v1/repos/repo1"

	testCases := []struct {
		name               string
		branches           []string
		apiError           error
		expectOverallError bool
		allBranches        bool
	}{
		{
			name:        "GetBranches all branches",
			branches:    []string{"main", "feature/codecommit", "chore/go-upgrade"},
			allBranches: true,
		},
		{
			name:        "GetBranches default branch only",
			allBranches: false,
		},
		{
			name:        "GetBranches default branch only",
			allBranches: false,
		},
		{
			name:               "GetBranches all branches on api error",
			apiError:           errors.New("api error"),
			expectOverallError: true,
			allBranches:        true,
		},
		{
			name:               "GetBranches default branch on api error",
			apiError:           errors.New("api error"),
			expectOverallError: true,
			allBranches:        false,
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			t.Parallel()
			codeCommitClient := mocks.NewAWSCodeCommitClient(t)
			taggingClient := mocks.NewAWSTaggingClient(t)
			ctx := t.Context()
			if testCase.allBranches {
				branches := make([]string, len(testCase.branches))
				copy(branches, testCase.branches)
				codeCommitClient.EXPECT().ListBranches(mock.Anything, &codecommit.ListBranchesInput{
					RepositoryName: aws.String(name),
				}).
					Return(&codecommit.ListBranchesOutput{Branches: branches}, testCase.apiError).Maybe()
			} else {
				codeCommitClient.EXPECT().GetRepository(mock.Anything, &codecommit.GetRepositoryInput{RepositoryName: aws.String(name)}).
					Return(&codecommit.GetRepositoryOutput{RepositoryMetadata: &codecommittypes.RepositoryMetadata{
						AccountId:     aws.String(organization),
						DefaultBranch: aws.String(defaultBranch),
					}}, testCase.apiError).Maybe()
			}
			provider := &AWSCodeCommitProvider{
				codeCommitClient: codeCommitClient,
				taggingClient:    taggingClient,
				allBranches:      testCase.allBranches,
			}
			actual, err := provider.GetBranches(ctx, &Repository{
				Organization: organization,
				Repository:   name,
				URL:          cloneURL,
				RepositoryId: id,
			})
			if testCase.expectOverallError {
				assert.Error(t, err)
			} else {
				assertCopiedProperties := func(repo *Repository) {
					assert.Equal(t, id, repo.RepositoryId)
					assert.Equal(t, name, repo.Repository)
					assert.Equal(t, cloneURL, repo.URL)
					assert.Equal(t, organization, repo.Organization)
					assert.Empty(t, repo.SHA)
				}
				actualBranches := make([]string, 0)
				for _, repo := range actual {
					assertCopiedProperties(repo)
					actualBranches = append(actualBranches, repo.Branch)
				}
				if testCase.allBranches {
					assert.ElementsMatch(t, testCase.branches, actualBranches)
				} else {
					assert.ElementsMatch(t, []string{defaultBranch}, actualBranches)
				}
			}
		})
	}
}

// equalIgnoringTagFilterOrder provides an argumentMatcher function that can be used to compare equality of GetResourcesInput ignoring the tagFilter ordering.
func equalIgnoringTagFilterOrder(expected *resourcegroupstaggingapi.GetResourcesInput) func(*resourcegroupstaggingapi.GetResourcesInput) bool {
	return func(actual *resourcegroupstaggingapi.GetResourcesInput) bool {
		sort.Slice(actual.TagFilters, func(i, j int) bool {
			keyI := ""
			keyJ := ""
			if actual.TagFilters[i].Key != nil {
				keyI = *actual.TagFilters[i].Key
			}
			if actual.TagFilters[j].Key != nil {
				keyJ = *actual.TagFilters[j].Key
			}
			return keyI < keyJ
		})
		// Ignore unexported fields in AWS SDK v2 types
		return cmp.Equal(expected, actual,
			cmpopts.IgnoreUnexported(
				rgsatypes.TagFilter{},
				resourcegroupstaggingapi.GetResourcesInput{},
			),
		)
	}
}
