// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0

package git

import (
	"fmt"
	"path/filepath"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestNewRepoSpecFromUrl_Permute(t *testing.T) {
	// Generate all many permutations of host, RepoPath, pathName, and ref.
	// Not all combinations make sense, but the parsing is very permissive and
	// we probably stil don't want to break backwards compatibility for things
	// that are unintentionally supported.
	var schemeAuthority = []struct{ raw, normalized string }{
		{"gitHub.com/", "https://github.com/"},
		{"github.com:", "https://github.com/"},
		{"http://github.com/", "https://github.com/"},
		{"https://github.com/", "https://github.com/"},
		{"hTTps://github.com/", "https://github.com/"},
		{"https://git-codecommit.us-east-2.amazonaws.com/", "https://git-codecommit.us-east-2.amazonaws.com/"},
		{"https://fabrikops2.visualstudio.com/", "https://fabrikops2.visualstudio.com/"},
		{"ssh://git.example.com:7999/", "ssh://git.example.com:7999/"},
		{"git::https://gitlab.com/", "https://gitlab.com/"},
		{"git::http://git.example.com/", "http://git.example.com/"},
		{"git::https://git.example.com/", "https://git.example.com/"},
		{"git@github.com:", "git@github.com:"},
		{"git@github.com/", "git@github.com:"},
		{"git::git@github.com:", "git@github.com:"},
		{"org-12345@github.com:", "org-12345@github.com:"},
		{"org-12345@github.com/", "org-12345@github.com:"},
	}
	var repoPaths = []string{"someOrg/someRepo", "kubernetes/website"}
	var pathNames = []string{"README.md", "foo/krusty.txt", ""}
	var refArgs = []string{"group/version", "someBranch", "master", "v0.1.0", ""}

	makeURL := func(hostFmt, repoPath, path, ref string) string {
		if len(path) > 0 {
			repoPath = filepath.Join(repoPath, path)
		}
		url := hostFmt + repoPath
		if ref != "" {
			url += refQuery + ref
		}
		return url
	}

	var i int
	for _, v := range schemeAuthority {
		hostRaw := v.raw
		hostSpec := v.normalized
		for _, repoPath := range repoPaths {
			for _, pathName := range pathNames {
				for _, hrefArg := range refArgs {
					t.Run(fmt.Sprintf("t%d", i), func(t *testing.T) {
						uri := makeURL(hostRaw, repoPath, pathName, hrefArg)
						rs, err := NewRepoSpecFromURL(uri)
						require.NoErrorf(t, err, "unexpected error creating RepoSpec for uri %s", uri)
						assert.Equal(t, hostSpec, rs.Host, "unexpected host for uri %s", uri)
						assert.Equal(t, repoPath, rs.RepoPath, "unexpected RepoPath for uri %s", uri)
						assert.Equal(t, pathName, rs.KustRootPath, "unexpected KustRootPath for uri %s", uri)
						assert.Equal(t, hrefArg, rs.Ref, "unexpected ref for uri %s", uri)
					})
					i++
				}
			}
		}
	}
}

func TestNewRepoSpecFromUrlErrors(t *testing.T) {
	badData := map[string]struct {
		url, error string
	}{
		"absolute_path": {
			"/tmp",
			"uri looks like abs path",
		},
		"relative path": {
			"../../tmp",
			"failed to parse scheme",
		},
		"local path that looks somewhat like a github url": {
			"src/github.com/org/repo/path",
			"failed to parse scheme",
		},
		"no_slashes": {
			"iauhsdiuashduas",
			"failed to parse scheme",
		},
		"bad_scheme": {
			"htxxxtp://github.com/",
			"failed to parse scheme",
		},
		"no_org_repo": {
			"ssh://git.example.com",
			"failed to parse repo path segment",
		},
		"hashicorp_git_only": {
			"git::___",
			"failed to parse scheme",
		},
		"query_after_host": {
			"https://host?ref=group/version/minor_version",
			"failed to parse repo path segment",
		},
		"path_exits_repo": {
			"https://github.com/org/repo.git//path/../../exits/repo",
			"url path exits repo",
		},
		"bad github separator": {
			"github.com!org/repo.git//path",
			"failed to parse scheme",
		},
		"mysterious gh: prefix previously supported is no longer handled": {
			"gh:org/repo",
			"failed to parse scheme",
		},
		"invalid Github url missing orgrepo": {
			"https://github.com/thisisa404.yaml",
			"failed to parse repo path segment",
		},
		"file protocol with excessive slashes": { // max valid is three: two for the scheme and one for the root
			"file:////tmp//path/to/whatever",
			"failed to parse repo path segment",
		},
		"unsupported protocol after username (invalid)": {
			"git@scp://github.com/org/repo.git//path",
			"failed to parse repo path segment",
		},
		"supported protocol after username (invalid)": {
			"git@ssh://github.com/org/repo.git//path",
			"failed to parse repo path segment",
		},
	}

	for name, testCase := range badData {
		t.Run(name, func(t *testing.T) {
			_, err := NewRepoSpecFromURL(testCase.url)
			require.Error(t, err)
			require.Contains(t, err.Error(), testCase.error)
		})
	}
}

func TestNewRepoSpecFromUrl_Smoke(t *testing.T) {
	// A set of end to end parsing tests that smoke out obvious issues
	// No tests for submodules and timeout as the expectations are hard-coded
	// to the defaults for compactness.
	testcases := []struct {
		name      string
		input     string
		repoSpec  RepoSpec
		cloneSpec string
		absPath   string
	}{
		{
			name:      "https aws code commit url",
			input:     "https://git-codecommit.us-east-2.amazonaws.com/someorg/somerepo/somedir",
			cloneSpec: "https://git-codecommit.us-east-2.amazonaws.com/someorg/somerepo",
			absPath:   notCloned.Join("somedir"),
			repoSpec: RepoSpec{
				Host:         "https://git-codecommit.us-east-2.amazonaws.com/",
				RepoPath:     "someorg/somerepo",
				KustRootPath: "somedir",
			},
		},
		{
			name:      "https aws code commit url with params",
			input:     "https://git-codecommit.us-east-2.amazonaws.com/someorg/somerepo/somedir?ref=testbranch",
			cloneSpec: "https://git-codecommit.us-east-2.amazonaws.com/someorg/somerepo",
			absPath:   notCloned.Join("somedir"),
			repoSpec: RepoSpec{
				Host:         "https://git-codecommit.us-east-2.amazonaws.com/",
				RepoPath:     "someorg/somerepo",
				KustRootPath: "somedir",
				Ref:          "testbranch",
			},
		},
		{
			name:      "legacy azure https url with params",
			input:     "https://fabrikops2.visualstudio.com/someorg/somerepo?ref=master",
			cloneSpec: "https://fabrikops2.visualstudio.com/someorg/somerepo",
			absPath:   notCloned.String(),
			repoSpec: RepoSpec{
				Host:     "https://fabrikops2.visualstudio.com/",
				RepoPath: "someorg/somerepo",
				Ref:      "master",
			},
		},
		{
			name:      "http github url without git suffix",
			input:     "http://github.com/someorg/somerepo/somedir",
			cloneSpec: "https://github.com/someorg/somerepo",
			absPath:   notCloned.Join("somedir"),
			repoSpec: RepoSpec{
				Host:         "https://github.com/",
				RepoPath:     "someorg/somerepo",
				KustRootPath: "somedir",
			},
		},
		{
			name:      "scp github url without git suffix",
			input:     "git@github.com:someorg/somerepo/somedir",
			cloneSpec: "git@github.com:someorg/somerepo",
			absPath:   notCloned.Join("somedir"),
			repoSpec: RepoSpec{
				Host:         "git@github.com:",
				RepoPath:     "someorg/somerepo",
				KustRootPath: "somedir",
			},
		},
		{
			name:      "http github url with git suffix",
			input:     "http://github.com/someorg/somerepo.git/somedir",
			cloneSpec: "https://github.com/someorg/somerepo.git",
			absPath:   notCloned.Join("somedir"),
			repoSpec: RepoSpec{
				Host:         "https://github.com/",
				RepoPath:     "someorg/somerepo.git",
				KustRootPath: "somedir",
			},
		},
		{
			name:      "scp github url with git suffix",
			input:     "git@github.com:someorg/somerepo.git/somedir",
			cloneSpec: "git@github.com:someorg/somerepo.git",
			absPath:   notCloned.Join("somedir"),
			repoSpec: RepoSpec{
				Host:         "git@github.com:",
				RepoPath:     "someorg/somerepo.git",
				KustRootPath: "somedir",
			},
		},
		{
			name:      "non-github_scp",
			input:     "git@gitlab2.sqtools.ru:infra/kubernetes/thanos-base.git?ref=v0.1.0",
			cloneSpec: "git@gitlab2.sqtools.ru:infra/kubernetes/thanos-base.git",
			absPath:   notCloned.String(),
			repoSpec: RepoSpec{
				Host:     "git@gitlab2.sqtools.ru:",
				RepoPath: "infra/kubernetes/thanos-base.git",
				Ref:      "v0.1.0",
			},
		},
		{
			name:      "non-github_scp with path delimiter",
			input:     "git@bitbucket.org:company/project.git//path?ref=branch",
			cloneSpec: "git@bitbucket.org:company/project.git",
			absPath:   notCloned.Join("path"),
			repoSpec: RepoSpec{
				Host:         "git@bitbucket.org:",
				RepoPath:     "company/project.git",
				KustRootPath: "path",
				Ref:          "branch",
			},
		},
		{
			name:      "non-github_scp incorrectly using slash (invalid but currently passed through to git)",
			input:     "git@bitbucket.org/company/project.git//path?ref=branch",
			cloneSpec: "git@bitbucket.org/company/project.git",
			absPath:   notCloned.Join("path"),
			repoSpec: RepoSpec{
				Host:         "git@bitbucket.org/",
				RepoPath:     "company/project.git",
				KustRootPath: "path",
				Ref:          "branch",
			},
		},
		{
			name:      "non-github_git-user_ssh",
			input:     "ssh://git@bitbucket.org/company/project.git//path?ref=branch",
			cloneSpec: "ssh://git@bitbucket.org/company/project.git",
			absPath:   notCloned.Join("path"),
			repoSpec: RepoSpec{
				Host:         "ssh://git@bitbucket.org/",
				RepoPath:     "company/project.git",
				KustRootPath: "path",
				Ref:          "branch",
			},
		},
		{
			name:      "_git host delimiter in non-github url",
			input:     "https://itfs.mycompany.com/collection/project/_git/somerepos",
			cloneSpec: "https://itfs.mycompany.com/collection/project/_git/somerepos",
			absPath:   notCloned.String(),
			repoSpec: RepoSpec{
				Host:     "https://itfs.mycompany.com/",
				RepoPath: "collection/project/_git/somerepos",
			},
		},
		{
			name:      "_git host delimiter in non-github url with params",
			input:     "https://itfs.mycompany.com/collection/project/_git/somerepos?version=v1.0.0",
			cloneSpec: "https://itfs.mycompany.com/collection/project/_git/somerepos",
			absPath:   notCloned.String(),
			repoSpec: RepoSpec{
				Host:     "https://itfs.mycompany.com/",
				RepoPath: "collection/project/_git/somerepos",
				Ref:      "v1.0.0",
			},
		},
		{
			name:      "_git host delimiter in non-github url with kust root path and params",
			input:     "https://itfs.mycompany.com/collection/project/_git/somerepos/somedir?version=v1.0.0",
			cloneSpec: "https://itfs.mycompany.com/collection/project/_git/somerepos",
			absPath:   notCloned.Join("somedir"),
			repoSpec: RepoSpec{
				Host:         "https://itfs.mycompany.com/",
				RepoPath:     "collection/project/_git/somerepos",
				KustRootPath: "somedir",
				Ref:          "v1.0.0",
			},
		},
		{
			name:      "_git host delimiter in non-github url with no kust root path",
			input:     "git::https://itfs.mycompany.com/collection/project/_git/somerepos",
			cloneSpec: "https://itfs.mycompany.com/collection/project/_git/somerepos",
			absPath:   notCloned.String(),
			repoSpec: RepoSpec{
				Host:     "https://itfs.mycompany.com/",
				RepoPath: "collection/project/_git/somerepos",
			},
		},
		{
			name:      "https bitbucket url with git suffix",
			input:     "https://bitbucket.example.com/scm/project/repository.git",
			cloneSpec: "https://bitbucket.example.com/scm/project/repository.git",
			absPath:   notCloned.String(),
			repoSpec: RepoSpec{
				Host:     "https://bitbucket.example.com/",
				RepoPath: "scm/project/repository.git",
			},
		},
		{
			name:      "ssh aws code commit url",
			input:     "ssh://git-codecommit.us-east-2.amazonaws.com/someorg/somerepo/somepath",
			cloneSpec: "ssh://git-codecommit.us-east-2.amazonaws.com/someorg/somerepo",
			absPath:   notCloned.Join("somepath"),
			repoSpec: RepoSpec{
				Host:         "ssh://git-codecommit.us-east-2.amazonaws.com/",
				RepoPath:     "someorg/somerepo",
				KustRootPath: "somepath",
			},
		},
		{
			name:      "scp Github with slash fixed to colon",
			input:     "git@github.com/someorg/somerepo/somepath",
			cloneSpec: "git@github.com:someorg/somerepo",
			absPath:   notCloned.Join("somepath"),
			repoSpec: RepoSpec{
				Host:         "git@github.com:",
				RepoPath:     "someorg/somerepo",
				KustRootPath: "somepath",
			},
		},
		{
			name:      "https Github with double slash path delimiter and params",
			input:     "https://github.com/kubernetes-sigs/kustomize//examples/multibases/dev/?ref=v1.0.6",
			cloneSpec: "https://github.com/kubernetes-sigs/kustomize",
			absPath:   notCloned.Join("/examples/multibases/dev"),
			repoSpec: RepoSpec{
				Host:         "https://github.com/",
				RepoPath:     "kubernetes-sigs/kustomize",
				KustRootPath: "examples/multibases/dev/",
				Ref:          "v1.0.6",
			},
		},
		{
			name:      "file protocol with git-suffixed repo path and params",
			input:     "file://a/b/c/someRepo.git/somepath?ref=someBranch",
			cloneSpec: "file://a/b/c/someRepo.git",
			absPath:   notCloned.Join("somepath"),
			repoSpec: RepoSpec{
				Host:         "file://",
				RepoPath:     "a/b/c/someRepo.git",
				KustRootPath: "somepath",
				Ref:          "someBranch",
			},
		},
		{
			name:      "file protocol with two slashes, with kust root path and params",
			input:     "file://a/b/c/someRepo//somepath?ref=someBranch",
			cloneSpec: "file://a/b/c/someRepo",
			absPath:   notCloned.Join("somepath"),
			repoSpec: RepoSpec{
				Host:         "file://",
				RepoPath:     "a/b/c/someRepo",
				KustRootPath: "somepath",
				Ref:          "someBranch",
			},
		},
		{
			name:      "file protocol with two slashes, with ref and no kust root path",
			input:     "file://a/b/c/someRepo?ref=someBranch",
			cloneSpec: "file://a/b/c/someRepo",
			absPath:   notCloned.String(),
			repoSpec: RepoSpec{
				Host:     "file://",
				RepoPath: "a/b/c/someRepo",
				Ref:      "someBranch",
			},
		},
		{
			name:      "file protocol with three slashes, with ref and no kust root path",
			input:     "file:///a/b/c/someRepo?ref=someBranch",
			cloneSpec: "file:///a/b/c/someRepo",
			absPath:   notCloned.String(),
			repoSpec: RepoSpec{
				Host:     "file://",
				RepoPath: "/a/b/c/someRepo",
				Ref:      "someBranch",
			},
		},
		{
			name:      "ssh Github with double-slashed path delimiter and params",
			input:     "ssh://git@github.com/kubernetes-sigs/kustomize//examples/multibases/dev?ref=v1.0.6",
			cloneSpec: "git@github.com:kubernetes-sigs/kustomize",
			absPath:   notCloned.Join("examples/multibases/dev"),
			repoSpec: RepoSpec{
				Host:         "git@github.com:",
				RepoPath:     "kubernetes-sigs/kustomize",
				KustRootPath: "examples/multibases/dev",
				Ref:          "v1.0.6",
			},
		},
		{
			name:      "file protocol with three slashes, no kust root path or params",
			input:     "file:///a/b/c/someRepo",
			cloneSpec: "file:///a/b/c/someRepo",
			absPath:   notCloned.String(),
			repoSpec: RepoSpec{
				Host:     "file://",
				RepoPath: "/a/b/c/someRepo",
			},
		},
		{
			name:      "file protocol with three slashes, no repo or kust root path or params",
			input:     "file:///",
			cloneSpec: "file:///",
			absPath:   notCloned.String(),
			repoSpec: RepoSpec{
				Host:     "file://",
				RepoPath: "/",
			},
		},
		{
			name:      "arbitrary https host with double-slash path delimiter",
			input:     "https://example.org/path/to/repo//examples/multibases/dev",
			cloneSpec: "https://example.org/path/to/repo",
			absPath:   notCloned.Join("/examples/multibases/dev"),
			repoSpec: RepoSpec{
				Host:         "https://example.org/",
				RepoPath:     "path/to/repo",
				KustRootPath: "examples/multibases/dev",
			},
		},
		{
			name:      "arbitrary https host with .git repo suffix",
			input:     "https://example.org/path/to/repo.git/examples/multibases/dev",
			cloneSpec: "https://example.org/path/to/repo.git",
			absPath:   notCloned.Join("/examples/multibases/dev"),
			repoSpec: RepoSpec{
				Host:         "https://example.org/",
				RepoPath:     "path/to/repo.git",
				KustRootPath: "examples/multibases/dev",
			},
		},
		{
			name:      "arbitrary ssh host with double-slash path delimiter",
			input:     "ssh://alice@example.com/path/to/repo//examples/multibases/dev",
			cloneSpec: "ssh://alice@example.com/path/to/repo",
			absPath:   notCloned.Join("/examples/multibases/dev"),
			repoSpec: RepoSpec{
				Host:         "ssh://alice@example.com/",
				RepoPath:     "path/to/repo",
				KustRootPath: "examples/multibases/dev",
			},
		},
		{
			name:      "query_slash",
			input:     "https://authority/org/repo?ref=group/version",
			cloneSpec: "https://authority/org/repo",
			absPath:   notCloned.String(),
			repoSpec: RepoSpec{
				Host:     "https://authority/",
				RepoPath: "org/repo",
				Ref:      "group/version",
			},
		},
		{
			name:      "query_git_delimiter",
			input:     "https://authority/org/repo/?ref=includes_git/for_some_reason",
			cloneSpec: "https://authority/org/repo",
			absPath:   notCloned.String(),
			repoSpec: RepoSpec{
				Host:     "https://authority/",
				RepoPath: "org/repo",
				Ref:      "includes_git/for_some_reason",
			},
		},
		{
			name:      "query_git_suffix",
			input:     "https://authority/org/repo/?ref=includes.git/for_some_reason",
			cloneSpec: "https://authority/org/repo",
			absPath:   notCloned.String(),
			repoSpec: RepoSpec{
				Host:     "https://authority/",
				RepoPath: "org/repo",
				Ref:      "includes.git/for_some_reason",
			},
		},
		{
			name:      "non_parsable_path",
			input:     "https://authority/org/repo/%-invalid-uri-so-not-parsable-by-net/url.Parse",
			cloneSpec: "https://authority/org/repo",
			absPath:   notCloned.Join("%-invalid-uri-so-not-parsable-by-net/url.Parse"),
			repoSpec: RepoSpec{
				Host:         "https://authority/",
				RepoPath:     "org/repo",
				KustRootPath: "%-invalid-uri-so-not-parsable-by-net/url.Parse",
			},
		},
		{
			name:      "non-git username with non-github host",
			input:     "ssh://myusername@bitbucket.org/ourteamname/ourrepositoryname.git//path?ref=branch",
			cloneSpec: "ssh://myusername@bitbucket.org/ourteamname/ourrepositoryname.git",
			absPath:   notCloned.Join("path"),
			repoSpec: RepoSpec{
				Host:         "ssh://myusername@bitbucket.org/",
				RepoPath:     "ourteamname/ourrepositoryname.git",
				KustRootPath: "path",
				Ref:          "branch",
			},
		},
		{
			name:      "username-like filepath with file protocol",
			input:     "file://git@home/path/to/repository.git//path?ref=branch",
			cloneSpec: "file://git@home/path/to/repository.git",
			absPath:   notCloned.Join("path"),
			repoSpec: RepoSpec{
				Host:         "file://",
				RepoPath:     "git@home/path/to/repository.git",
				KustRootPath: "path",
				Ref:          "branch",
			},
		},
		{
			name:      "username with http protocol (invalid but currently passed through to git)",
			input:     "http://git@home.com/path/to/repository.git//path?ref=branch",
			cloneSpec: "http://git@home.com/path/to/repository.git",
			absPath:   notCloned.Join("path"),
			repoSpec: RepoSpec{
				Host:         "http://git@home.com/",
				RepoPath:     "path/to/repository.git",
				KustRootPath: "path",
				Ref:          "branch",
			},
		},
		{
			name:      "username with https protocol (invalid but currently passed through to git)",
			input:     "https://git@home.com/path/to/repository.git//path?ref=branch",
			cloneSpec: "https://git@home.com/path/to/repository.git",
			absPath:   notCloned.Join("path"),
			repoSpec: RepoSpec{
				Host:         "https://git@home.com/",
				RepoPath:     "path/to/repository.git",
				KustRootPath: "path",
				Ref:          "branch",
			},
		},
		{
			name:      "complex github ssh url from docs",
			input:     "ssh://git@ssh.github.com:443/YOUR-USERNAME/YOUR-REPOSITORY.git",
			cloneSpec: "ssh://git@ssh.github.com:443/YOUR-USERNAME/YOUR-REPOSITORY.git",
			absPath:   notCloned.String(),
			repoSpec: RepoSpec{
				Host:         "ssh://git@ssh.github.com:443/",
				RepoPath:     "YOUR-USERNAME/YOUR-REPOSITORY.git",
				KustRootPath: "",
			},
		},
		{
			name:      "colon behind slash not scp delimiter",
			input:     "git@gitlab.com/user:name/YOUR-REPOSITORY.git/path",
			cloneSpec: "git@gitlab.com/user:name/YOUR-REPOSITORY.git",
			absPath:   notCloned.Join("path"),
			repoSpec: RepoSpec{
				Host:         "git@gitlab.com/",
				RepoPath:     "user:name/YOUR-REPOSITORY.git",
				KustRootPath: "path",
			},
		},
		{
			name:      "gitlab URLs with explicit git suffix",
			input:     "git@gitlab.com:gitlab-tests/sample-project.git",
			cloneSpec: "git@gitlab.com:gitlab-tests/sample-project.git",
			absPath:   notCloned.String(),
			repoSpec: RepoSpec{
				Host:     "git@gitlab.com:",
				RepoPath: "gitlab-tests/sample-project.git",
			},
		},
		{
			name:      "gitlab URLs without explicit git suffix",
			input:     "git@gitlab.com:gitlab-tests/sample-project",
			cloneSpec: "git@gitlab.com:gitlab-tests/sample-project",
			absPath:   notCloned.String(),
			repoSpec: RepoSpec{
				Host:     "git@gitlab.com:",
				RepoPath: "gitlab-tests/sample-project",
			},
		},
		{
			name:      "azure host with _git and // path separator",
			input:     "https://username@dev.azure.com/org/project/_git/repo//path/to/kustomization/root",
			cloneSpec: "https://username@dev.azure.com/org/project/_git/repo",
			absPath:   notCloned.Join("path/to/kustomization/root"),
			repoSpec: RepoSpec{
				Host:         "https://username@dev.azure.com/",
				RepoPath:     "org/project/_git/repo",
				KustRootPath: "path/to/kustomization/root",
			},
		},
		{
			name:      "legacy format azure host with _git",
			input:     "https://org.visualstudio.com/project/_git/repo/path/to/kustomization/root",
			cloneSpec: "https://org.visualstudio.com/project/_git/repo",
			absPath:   notCloned.Join("path/to/kustomization/root"),
			repoSpec: RepoSpec{
				Host:         "https://org.visualstudio.com/",
				RepoPath:     "project/_git/repo",
				KustRootPath: "path/to/kustomization/root",
			},
		},
		{
			name:      "ssh on github with custom username for custom ssh certificate authority",
			input:     "ssh://org-12345@github.com/kubernetes-sigs/kustomize",
			cloneSpec: "org-12345@github.com:kubernetes-sigs/kustomize",
			absPath:   notCloned.String(),
			repoSpec: RepoSpec{
				Host:     "org-12345@github.com:",
				RepoPath: "kubernetes-sigs/kustomize",
			},
		},
		{
			name:      "scp on github with custom username for custom ssh certificate authority",
			input:     "org-12345@github.com/kubernetes-sigs/kustomize",
			cloneSpec: "org-12345@github.com:kubernetes-sigs/kustomize",
			absPath:   notCloned.String(),
			repoSpec: RepoSpec{
				Host:     "org-12345@github.com:",
				RepoPath: "kubernetes-sigs/kustomize",
			},
		},
		{
			name:      "scp format gist url",
			input:     "git@gist.github.com:bc7947cb727d7f9217e7862d961a1ffd.git",
			cloneSpec: "git@gist.github.com:bc7947cb727d7f9217e7862d961a1ffd.git",
			absPath:   notCloned.String(),
			repoSpec: RepoSpec{
				Host:     "git@gist.github.com:",
				RepoPath: "bc7947cb727d7f9217e7862d961a1ffd.git",
			},
		},
		{
			name:      "https gist url",
			input:     "https://gist.github.com/bc7947cb727d7f9217e7862d961a1ffd.git",
			cloneSpec: "https://gist.github.com/bc7947cb727d7f9217e7862d961a1ffd.git",
			absPath:   notCloned.String(),
			repoSpec: RepoSpec{
				Host:     "https://gist.github.com/",
				RepoPath: "bc7947cb727d7f9217e7862d961a1ffd.git",
			},
		},
	}
	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			rs, err := NewRepoSpecFromURL(tc.input)
			require.NoError(t, err)
			assert.Equal(t, tc.cloneSpec, rs.CloneSpec(), "cloneSpec mismatch")
			assert.Equal(t, tc.absPath, rs.AbsPath(), "absPath mismatch")
			// some values have defaults. Clear them here so test cases remain compact.
			// This means submodules and timeout cannot be tested here. That's fine since
			// they are tested in TestParseQuery.
			rs.raw = ""
			rs.Dir = ""
			rs.Submodules = false
			rs.Timeout = 0
			assert.Equal(t, &tc.repoSpec, rs)
		})
	}
}

func TestNewRepoSpecFromURL_DefaultQueryParams(t *testing.T) {
	repoSpec, err := NewRepoSpecFromURL("https://github.com/org/repo")
	require.NoError(t, err)
	require.Equal(t, defaultSubmodules, repoSpec.Submodules)
	require.Equal(t, defaultTimeout, repoSpec.Timeout)
}

func TestParseQuery(t *testing.T) {
	testcases := []struct {
		name       string
		input      string
		ref        string
		submodules bool
		timeout    time.Duration
	}{
		{
			name:       "empty",
			input:      "",
			ref:        "",
			submodules: defaultSubmodules,
			timeout:    defaultTimeout,
		},
		{
			name:       "ref",
			input:      "ref=v1.0.0",
			ref:        "v1.0.0",
			submodules: defaultSubmodules,
			timeout:    defaultTimeout,
		},
		{
			name:       "ref_slash",
			input:      "ref=kustomize/v4.5.7",
			ref:        "kustomize/v4.5.7",
			submodules: defaultSubmodules,
			timeout:    defaultTimeout,
		},
		{
			name:       "version",
			input:      "version=master",
			ref:        "master",
			submodules: defaultSubmodules,
			timeout:    defaultTimeout,
		},
		{
			name: "ref_and_version",
			// A ref value takes precedence over a version value.
			input:      "version=master&ref=v1.0.0",
			ref:        "v1.0.0",
			submodules: defaultSubmodules,
			timeout:    defaultTimeout,
		},
		{
			name: "empty_submodules",
			// Empty submodules value uses default.
			input:      "version=master&submodules=",
			ref:        "master",
			submodules: defaultSubmodules,
			timeout:    defaultTimeout,
		},
		{
			name: "bad_submodules",
			// Malformed submodules value uses default.
			input:      "version=master&submodules=maybe",
			ref:        "master",
			submodules: defaultSubmodules,
			timeout:    defaultTimeout,
		},
		{
			name:       "submodules_true",
			input:      "version=master&submodules=true",
			ref:        "master",
			submodules: true,
			timeout:    defaultTimeout,
		},
		{
			name:       "submodules_false",
			input:      "version=master&submodules=false",
			ref:        "master",
			submodules: false,
			timeout:    defaultTimeout,
		},
		{
			name: "empty_timeout",
			// Empty timeout value uses default.
			input:      "version=master&timeout=",
			ref:        "master",
			submodules: defaultSubmodules,
			timeout:    defaultTimeout,
		},
		{
			name: "bad_timeout",
			// Malformed timeout value uses default.
			input:      "version=master&timeout=jiffy",
			ref:        "master",
			submodules: defaultSubmodules,
			timeout:    defaultTimeout,
		},
		{
			name: "zero_timeout",
			// Zero timeout value uses default.
			input:      "version=master&timeout=0",
			ref:        "master",
			submodules: defaultSubmodules,
			timeout:    defaultTimeout,
		},
		{
			name:       "zero_unit_timeout",
			input:      "version=master&timeout=0s",
			ref:        "master",
			submodules: defaultSubmodules,
			timeout:    defaultTimeout,
		},
		{
			name:       "timeout",
			input:      "version=master&timeout=61",
			ref:        "master",
			submodules: defaultSubmodules,
			timeout:    61 * time.Second,
		},
		{
			name:       "timeout_unit",
			input:      "version=master&timeout=1m1s",
			ref:        "master",
			submodules: defaultSubmodules,
			timeout:    61 * time.Second,
		},
		{
			name:       "all",
			input:      "version=master&submodules=false&timeout=1m1s",
			ref:        "master",
			submodules: false,
			timeout:    61 * time.Second,
		},
	}
	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			ref, timeout, submodules := parseQuery(tc.input)
			assert.Equal(t, tc.ref, ref, "ref mismatch")
			assert.Equal(t, tc.timeout, timeout, "timeout mismatch")
			assert.Equal(t, tc.submodules, submodules, "submodules mismatch")
		})
	}
}
