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

package krusty_test

import (
	"bytes"
	"encoding/base64"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"sigs.k8s.io/kustomize/api/internal/loader"
	"sigs.k8s.io/kustomize/api/krusty"
	"sigs.k8s.io/kustomize/api/resmap"
	kusttest_test "sigs.k8s.io/kustomize/api/testutils/kusttest"
	"sigs.k8s.io/kustomize/kyaml/yaml"
)

func TestRemoteLoad_LocalProtocol(t *testing.T) {
	type testRepos struct {
		root          string
		simple        string
		noSuffix      string
		hash          string
		multiBaseDev  string
		withSubmodule string
	}

	// creates git repos under a root temporary directory with the following structure
	// root/
	//   simple.git/			- base with just a pod
	//   nosuffix/				- same as simple.git/ without the .git suffix
	//   hash-xx/				- same as simple.git/ with random hash at xx
	//   multibase.git/			- base with a dev overlay
	//   with-submodule.git/	- includes `simple` as a submodule
	//     submodule/			- the submodule referencing `simple`
	createGitRepos := func(t *testing.T) testRepos {
		t.Helper()

		bash := func(script string) {
			cmd := exec.Command("sh", "-c", script)
			o, err := cmd.CombinedOutput()
			if err != nil {
				t.Fatalf("error running %v\nerr: %v\n%s", script, err, string(o))
			}
		}
		root := t.TempDir()

		hashPath, err := os.MkdirTemp(root, "hash-")
		require.NoError(t, err)
		hashDir := filepath.Base(hashPath)

		bash(fmt.Sprintf(`
set -eux

export ROOT="%s"
export HASH_DIR="%s"
export GIT_AUTHOR_EMAIL=nobody@kustomize.io
export GIT_AUTHOR_NAME=Nobody
export GIT_COMMITTER_EMAIL=nobody@kustomize.io
export GIT_COMMITTER_NAME=Nobody

cp -r testdata/remoteload/simple $ROOT/simple.git
(
	cd $ROOT/simple.git
	git config --global protocol.file.allow always
	git init --initial-branch=main
	git add .
	git commit -m "import"
	git checkout -b change-image
	cat >>kustomization.yaml <<EOF

images:
- name: nginx
  newName: nginx
  newTag: "2"
EOF
	git commit -am "image change"
	git checkout main
)
cp -r $ROOT/simple.git $ROOT/nosuffix
cp -r testdata/remoteload/multibase $ROOT/multibase.git
(
	cd $ROOT/multibase.git
	git init --initial-branch=main
	git add .
	git commit -m "import"
)
cp -r testdata/remoteload/with-submodule $ROOT/with-submodule.git # see README
cp -r $ROOT/simple.git/. $ROOT/$HASH_DIR
(
	cd $ROOT/with-submodule.git
	git init --initial-branch=main
	git add .
	git commit -m "import"
	git checkout -b relative-submodule
	git submodule add ../$HASH_DIR submodule
	git commit -m "relative submodule"
	git checkout main
	git submodule add $ROOT/simple.git submodule
	git commit -m "submodule"	
)
`, root, hashDir))
		return testRepos{
			root: root,
			// The strings below aren't currently used, and more serve as documentation.
			simple:        "simple.git",
			noSuffix:      "nosuffix",
			hash:          hashDir,
			multiBaseDev:  "multibase.git",
			withSubmodule: "with-submodule.git",
		}
	}

	const simpleBuild = `apiVersion: v1
kind: Pod
metadata:
  labels:
    app: myapp
  name: myapp-pod
spec:
  containers:
  - image: nginx:1.7.9
    name: nginx
`
	var simpleBuildWithNginx2 = strings.ReplaceAll(simpleBuild, "nginx:1.7.9", "nginx:2")
	var multibaseDevExampleBuild = strings.ReplaceAll(simpleBuild, "myapp-pod", "dev-myapp-pod")

	repos := createGitRepos(t)
	tests := []struct {
		name          string
		kustomization string
		expected      string
		err           string
		skip          bool
	}{
		{
			name: "simple",
			kustomization: `
resources:
- file://$ROOT/simple.git
`,
			expected: simpleBuild,
		},
		{
			name: "without git suffix",
			kustomization: `
resources:
- file://$ROOT/nosuffix
`,
			expected: simpleBuild,
		},
		{
			name: "has path",
			kustomization: `
resources:
- file://$ROOT/multibase.git/dev
`,
			expected: multibaseDevExampleBuild,
		},
		{
			name: "has path without git suffix",
			kustomization: `
resources:
- file://$ROOT/multibase//dev
`,
			expected: multibaseDevExampleBuild,
		},
		{
			name: "has ref",
			kustomization: `
resources: 
- "file://$ROOT/simple.git?ref=change-image"
`,

			expected: simpleBuildWithNginx2,
		},
		{
			// Version is the same as ref
			name: "has version",
			kustomization: `
resources:
- file://$ROOT/simple.git?version=change-image
`,
			expected: simpleBuildWithNginx2,
		},
		{
			name: "has submodule",
			kustomization: `
resources:
- file://$ROOT/with-submodule.git/submodule
`,
			expected: simpleBuild,
		},
		{
			name: "has relative submodule",
			kustomization: `
resources:
- file://$ROOT/with-submodule.git/submodule?ref=relative-submodule
`,

			expected: simpleBuild,
		},
		{
			name: "has timeout",
			kustomization: `
resources:
- file://$ROOT/simple.git?timeout=500
`,
			expected: simpleBuild,
		},
		{
			name: "has submodule but not initialized",
			kustomization: `
resources:
- file://$ROOT/with-submodule.git/submodule?submodules=0
`,
			err: "unable to find one of 'kustomization.yaml', 'kustomization.yml' or 'Kustomization' in directory",
		},
		{
			name: "has origin annotation",
			skip: true, // The annotated path should be "pod.yaml" but is "notCloned/pod.yaml"
			kustomization: `
resources:
- file://$ROOT/simple.git
buildMetadata: [originAnnotations]
`,
			expected: `apiVersion: v1
kind: Pod
metadata:
  annotations:
    config.kubernetes.io/origin: |
      path: pod.yaml
      repo: file://$ROOT/simple.git
  labels:
    app: myapp
  name: myapp-pod
spec:
  containers:
  - image: nginx:1.7.9
    name: nginx
`,
		},
		{
			name: "has ref path timeout and origin annotation",
			kustomization: `
resources:
- file://$ROOT/multibase.git/dev?version=main&timeout=500
buildMetadata: [originAnnotations]
`,
			expected: `apiVersion: v1
kind: Pod
metadata:
  annotations:
    config.kubernetes.io/origin: |
      path: base/pod.yaml
      repo: file://$ROOT/multibase.git
      ref: main
  labels:
    app: myapp
  name: dev-myapp-pod
spec:
  containers:
  - image: nginx:1.7.9
    name: nginx
`,
		},
		{
			name: "repo does not exist ",
			kustomization: `
resources:
- file:///not/a/real/repo
`,
			err: "fatal: '/not/a/real/repo' does not appear to be a git repository",
		},
	}

	t.Setenv("LC_ALL", "C")

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if test.skip {
				t.SkipNow()
			}

			kust := strings.ReplaceAll(test.kustomization, "$ROOT", repos.root)
			fSys, tmpDir := kusttest_test.CreateKustDir(t, kust)

			b := krusty.MakeKustomizer(krusty.MakeDefaultOptions())
			m, err := b.Run(
				fSys,
				tmpDir.String())

			if test.err != "" {
				require.Error(t, err)
				require.Regexp(t, test.err, err.Error())
			} else {
				require.NoError(t, err)
				checkYaml(t, m, strings.ReplaceAll(test.expected, "$ROOT", repos.root))
			}
		})
	}
}

func TestRemoteLoad_RemoteProtocols(t *testing.T) {
	// Slow remote tests with long timeouts.
	// TODO: If these end up flaking, they should retry. If not, remove this TODO.
	tests := []struct {
		name          string
		kustomization string
		err           string
		errT          error
		beforeTest    func(t *testing.T)
	}{
		{
			name: "https",
			kustomization: `
resources:
- https://github.com/kubernetes-sigs/kustomize//examples/multibases/dev/?submodules=0&ref=kustomize%2Fv4.5.7&timeout=300
`,
		},
		{
			name: "https with explicit .git suffix",
			kustomization: `
resources:
- https://github.com/kubernetes-sigs/kustomize.git//examples/multibases/dev/?submodules=0&ref=kustomize%2Fv4.5.7&timeout=300
`,
		},
		{
			name: "git double-colon https",
			kustomization: `
resources:
- git::https://github.com/kubernetes-sigs/kustomize//examples/multibases/dev/?submodules=0&ref=kustomize%2Fv4.5.7&timeout=300
`,
		},
		{
			name: "https raw remote file",
			kustomization: `
resources:
- https://raw.githubusercontent.com/kubernetes-sigs/kustomize/v3.1.0/examples/multibases/base/pod.yaml?timeout=300
namePrefix: dev-
`,
		},
		{
			name: "https without scheme",
			kustomization: `
resources:
- github.com/kubernetes-sigs/kustomize/examples/multibases/dev?submodules=0&ref=kustomize%2Fv4.5.7&timeout=300
`,
		},
		{
			name:       "ssh",
			beforeTest: configureGitSSHCommand,
			kustomization: `
resources:
- git@github.com/kubernetes-sigs/kustomize/examples/multibases/dev?submodules=0&ref=kustomize%2Fv4.5.7&timeout=300
`,
		},
		{
			name:       "ssh with colon",
			beforeTest: configureGitSSHCommand,
			kustomization: `
resources:
- git@github.com:kubernetes-sigs/kustomize/examples/multibases/dev?submodules=0&ref=kustomize%2Fv4.5.7&timeout=300
`,
		},
		{
			name:       "ssh scheme",
			beforeTest: configureGitSSHCommand,
			kustomization: `
resources:
- ssh://git@github.com/kubernetes-sigs/kustomize/examples/multibases/dev?submodules=0&ref=kustomize%2Fv4.5.7&timeout=300
`,
		},
		{
			name: "http error",
			kustomization: `
resources:
- https://github.com/thisisa404.yaml
`,
			err:  "accumulating resources: accumulating resources from 'https://github.com/thisisa404.yaml': HTTP Error: status code 404 (Not Found)",
			errT: loader.ErrHTTP,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if test.beforeTest != nil {
				test.beforeTest(t)
			}
			fSys, tmpDir := kusttest_test.CreateKustDir(t, test.kustomization)

			b := krusty.MakeKustomizer(krusty.MakeDefaultOptions())
			m, err := b.Run(
				fSys,
				tmpDir.String())

			if test.err != "" {
				require.Error(t, err)
				assert.Contains(t, err.Error(), test.err)
				if test.errT != nil {
					require.ErrorIs(t, err, test.errT)
				}
			} else {
				require.NoError(t, err)
				const multibaseDevExampleBuild = `apiVersion: v1
kind: Pod
metadata:
  labels:
    app: myapp
  name: dev-myapp-pod
spec:
  containers:
  - image: nginx:1.7.9
    name: nginx
`
				checkYaml(t, m, multibaseDevExampleBuild)
			}
		})
	}
}

func configureGitSSHCommand(t *testing.T) {
	t.Helper()

	// This contains a read-only Deploy Key for the kustomize repo.
	node, err := yaml.ReadFile("testdata/repo_read_only_ssh_key.yaml")
	require.NoError(t, err)
	keyB64, err := node.GetString("key")
	require.NoError(t, err)
	key, err := base64.StdEncoding.DecodeString(keyB64)
	require.NoError(t, err)

	// Write the key to a temp file and use it in SSH
	f, err := os.CreateTemp("", "kustomize_ssh")
	require.NoError(t, err)
	_, err = io.Copy(f, bytes.NewReader(key))
	require.NoError(t, err)
	cmd := fmt.Sprintf("ssh -i %s -o IdentitiesOnly=yes", f.Name())
	const SSHCommandKey = "GIT_SSH_COMMAND"
	t.Setenv(SSHCommandKey, cmd)
	t.Cleanup(func() {
		_ = os.Remove(f.Name())
	})
}

func checkYaml(t *testing.T, actual resmap.ResMap, expected string) {
	t.Helper()

	yml, err := actual.AsYaml()
	require.NoError(t, err)
	assert.Equal(t, expected, string(yml))
}
