package cmd

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"log"
	"os"
	"path/filepath"
	"testing"

	"github.com/linkerd/linkerd2/pkg/charts/linkerd2"
	"github.com/linkerd/linkerd2/pkg/cmd"
	"github.com/linkerd/linkerd2/pkg/inject"
	"github.com/linkerd/linkerd2/pkg/k8s"
	"github.com/linkerd/linkerd2/testutil"
)

type testCase struct {
	inputFileName          string
	goldenFileName         string
	reportFileName         string
	injectProxy            bool
	testInjectConfig       *linkerd2.Values
	enableDebugSidecarFlag bool
}

func mkFilename(filename string, verbose bool) string {
	if verbose {
		return fmt.Sprintf("%s.verbose", filename)
	}
	return filename
}

func testUninjectAndInject(t *testing.T, tc testCase) {
	t.Helper()

	file, err := os.Open("testdata/" + tc.inputFileName)
	if err != nil {
		t.Errorf("error opening test input file: %v\n", err)
	}

	read := bufio.NewReader(file)

	output := new(bytes.Buffer)
	report := new(bytes.Buffer)
	transformer := &resourceTransformerInject{
		injectProxy:         tc.injectProxy,
		values:              tc.testInjectConfig,
		overrideAnnotations: getOverrideAnnotations(tc.testInjectConfig, defaultConfig()),
		enableDebugSidecar:  tc.enableDebugSidecarFlag,
		allowNsInject:       true,
		overrider:           inject.GetOverriddenValues,
	}

	if exitCode := uninjectAndInject([]io.Reader{read}, report, output, transformer, "yaml"); exitCode != 0 {
		t.Errorf("Unexpected error injecting YAML: %v", report)
	}
	if err := testDataDiffer.DiffTestYAML(tc.goldenFileName, output.String()); err != nil {
		t.Error(err)
	}

	reportFileName := mkFilename(tc.reportFileName, verbose)
	testDataDiffer.DiffTestdata(t, reportFileName, report.String())
}

func defaultConfig() *linkerd2.Values {
	defaultConfig, err := testInstallValues()
	if err != nil {
		log.Fatalf("Unexpected error: %v", err)
	}
	defaultConfig.LinkerdVersion = "test-inject-control-plane-version"
	defaultConfig.Proxy.Image.Version = "test-inject-proxy-version"
	defaultConfig.DebugContainer.Image.Version = "test-inject-debug-version"

	return defaultConfig
}

func TestUninjectAndInject(t *testing.T) {
	defaultValues := defaultConfig()

	overrideConfig := defaultConfig()
	overrideConfig.Proxy.Image.Version = "override"

	proxyResourceConfig := defaultConfig()
	proxyResourceConfig.Proxy.Resources = &linkerd2.Resources{
		CPU: linkerd2.Constraints{
			Request: "110m",
			Limit:   "160m",
		},
		Memory: linkerd2.Constraints{
			Request: "100Mi",
			Limit:   "150Mi",
		},
	}

	cniEnabledConfig := defaultConfig()
	cniEnabledConfig.CNIEnabled = true

	opaquePortsConfig := defaultConfig()
	opaquePortsConfig.Proxy.OpaquePorts = "3000,5000-6000,mysql"

	ingressConfig := defaultConfig()
	ingressConfig.Proxy.IsIngress = true

	proxyIgnorePortsConfig := defaultConfig()
	proxyIgnorePortsConfig.ProxyInit.IgnoreInboundPorts = "22,8100-8102"
	proxyIgnorePortsConfig.ProxyInit.IgnoreOutboundPorts = "5432"

	testCases := []testCase{
		{
			inputFileName:    "inject_emojivoto_deployment.input.yml",
			goldenFileName:   "inject_emojivoto_deployment.golden.yml",
			reportFileName:   "inject_emojivoto_deployment.report",
			injectProxy:      true,
			testInjectConfig: defaultValues,
		},
		{
			inputFileName:  "inject_emojivoto_deployment.input.yml",
			goldenFileName: "inject_emojivoto_deployment_overridden_noinject.golden.yml",
			reportFileName: "inject_emojivoto_deployment.report",
			injectProxy:    false,
			testInjectConfig: func() *linkerd2.Values {
				values := defaultConfig()
				values.Proxy.Ports.Admin = 1234
				return values
			}(),
		},
		{
			inputFileName:  "inject_emojivoto_deployment.input.yml",
			goldenFileName: "inject_emojivoto_deployment_overridden.golden.yml",
			reportFileName: "inject_emojivoto_deployment.report",
			injectProxy:    true,
			testInjectConfig: func() *linkerd2.Values {
				values := defaultConfig()
				values.Proxy.Ports.Admin = 1234
				return values
			}(),
		},
		{
			inputFileName:  "inject_emojivoto_deployment.input.yml",
			goldenFileName: "inject_emojivoto_deployment_access_log.golden.yml",
			reportFileName: "inject_emojivoto_deployment.report",
			injectProxy:    true,
			testInjectConfig: func() *linkerd2.Values {
				values := defaultConfig()
				values.Proxy.AccessLog = "apache"
				return values
			}(),
		},
		{
			inputFileName:    "inject_emojivoto_list.input.yml",
			goldenFileName:   "inject_emojivoto_list.golden.yml",
			reportFileName:   "inject_emojivoto_list.report",
			injectProxy:      true,
			testInjectConfig: defaultValues,
		},
		{
			inputFileName:    "inject_emojivoto_deployment_hostNetwork_false.input.yml",
			goldenFileName:   "inject_emojivoto_deployment_hostNetwork_false.golden.yml",
			reportFileName:   "inject_emojivoto_deployment_hostNetwork_false.report",
			injectProxy:      true,
			testInjectConfig: defaultValues,
		},
		{
			inputFileName:    "inject_emojivoto_deployment_capabilities.input.yml",
			goldenFileName:   "inject_emojivoto_deployment_capabilities.golden.yml",
			reportFileName:   "inject_emojivoto_deployment.report",
			injectProxy:      true,
			testInjectConfig: defaultValues,
		},
		{
			inputFileName:    "inject_emojivoto_deployment_injectDisabled.input.yml",
			goldenFileName:   "inject_emojivoto_deployment_injectDisabled.input.yml",
			reportFileName:   "inject_emojivoto_deployment_injectDisabled.report",
			injectProxy:      true,
			testInjectConfig: defaultValues,
		},
		{
			inputFileName:    "inject_emojivoto_deployment_controller_name.input.yml",
			goldenFileName:   "inject_emojivoto_deployment_controller_name.golden.yml",
			reportFileName:   "inject_emojivoto_deployment_controller_name.report",
			injectProxy:      true,
			testInjectConfig: defaultValues,
		},
		{
			inputFileName:    "inject_emojivoto_statefulset.input.yml",
			goldenFileName:   "inject_emojivoto_statefulset.golden.yml",
			reportFileName:   "inject_emojivoto_statefulset.report",
			injectProxy:      true,
			testInjectConfig: defaultValues,
		},
		{
			inputFileName:    "inject_emojivoto_cronjob.input.yml",
			goldenFileName:   "inject_emojivoto_cronjob.golden.yml",
			reportFileName:   "inject_emojivoto_cronjob.report",
			injectProxy:      false,
			testInjectConfig: defaultValues,
		},
		{
			inputFileName:    "inject_emojivoto_cronjob_nometa.input.yml",
			goldenFileName:   "inject_emojivoto_cronjob_nometa.golden.yml",
			reportFileName:   "inject_emojivoto_cronjob.report",
			injectProxy:      false,
			testInjectConfig: defaultValues,
		},
		{
			inputFileName:    "inject_emojivoto_pod.input.yml",
			goldenFileName:   "inject_emojivoto_pod.golden.yml",
			reportFileName:   "inject_emojivoto_pod.report",
			injectProxy:      true,
			testInjectConfig: defaultValues,
		},
		{
			inputFileName:    "inject_emojivoto_pod_with_requests.input.yml",
			goldenFileName:   "inject_emojivoto_pod_with_requests.golden.yml",
			reportFileName:   "inject_emojivoto_pod_with_requests.report",
			injectProxy:      true,
			testInjectConfig: proxyResourceConfig,
		},
		{
			inputFileName:    "inject_emojivoto_deployment_udp.input.yml",
			goldenFileName:   "inject_emojivoto_deployment_udp.golden.yml",
			reportFileName:   "inject_emojivoto_deployment_udp.report",
			injectProxy:      true,
			testInjectConfig: defaultValues,
		},
		{
			inputFileName:    "inject_emojivoto_already_injected.input.yml",
			goldenFileName:   "inject_emojivoto_already_injected.golden.yml",
			reportFileName:   "inject_emojivoto_already_injected.report",
			injectProxy:      true,
			testInjectConfig: defaultValues,
		},
		{
			inputFileName:    "inject_contour.input.yml",
			goldenFileName:   "inject_contour.golden.yml",
			reportFileName:   "inject_contour.report",
			injectProxy:      true,
			testInjectConfig: defaultValues,
		},
		{
			inputFileName:    "inject_emojivoto_deployment_empty_resources.input.yml",
			goldenFileName:   "inject_emojivoto_deployment_empty_resources.golden.yml",
			reportFileName:   "inject_emojivoto_deployment_empty_resources.report",
			injectProxy:      true,
			testInjectConfig: defaultValues,
		},
		{
			inputFileName:    "inject_emojivoto_list_empty_resources.input.yml",
			goldenFileName:   "inject_emojivoto_list_empty_resources.golden.yml",
			reportFileName:   "inject_emojivoto_list_empty_resources.report",
			injectProxy:      true,
			testInjectConfig: defaultValues,
		},
		{
			inputFileName:    "inject_emojivoto_deployment.input.yml",
			goldenFileName:   "inject_emojivoto_deployment_no_init_container.golden.yml",
			reportFileName:   "inject_emojivoto_deployment.report",
			injectProxy:      true,
			testInjectConfig: cniEnabledConfig,
		},
		{
			inputFileName:    "inject_emojivoto_deployment_config_overrides.input.yml",
			goldenFileName:   "inject_emojivoto_deployment_config_overrides.golden.yml",
			reportFileName:   "inject_emojivoto_deployment.report",
			injectProxy:      true,
			testInjectConfig: overrideConfig,
		},
		{
			inputFileName:          "inject_emojivoto_deployment.input.yml",
			goldenFileName:         "inject_emojivoto_deployment_debug.golden.yml",
			reportFileName:         "inject_emojivoto_deployment.report",
			injectProxy:            true,
			testInjectConfig:       defaultValues,
			enableDebugSidecarFlag: true,
		},
		{
			inputFileName:          "inject_tap_deployment.input.yml",
			goldenFileName:         "inject_tap_deployment_debug.golden.yml",
			reportFileName:         "inject_tap_deployment_debug.report",
			injectProxy:            true,
			testInjectConfig:       defaultValues,
			enableDebugSidecarFlag: true,
		},
		{
			inputFileName:    "inject_emojivoto_namespace_good.input.yml",
			goldenFileName:   "inject_emojivoto_namespace_good.golden.yml",
			reportFileName:   "inject_emojivoto_namespace_good.golden.report",
			injectProxy:      false,
			testInjectConfig: defaultConfig(),
		},
		{
			inputFileName:    "inject_emojivoto_namespace_good.input.yml",
			goldenFileName:   "inject_emojivoto_namespace_overidden_good.golden.yml",
			reportFileName:   "inject_emojivoto_namespace_good.golden.report",
			injectProxy:      false,
			testInjectConfig: defaultConfig(),
		},
		{
			inputFileName:    "inject_emojivoto_deployment.input.yml",
			goldenFileName:   "inject_emojivoto_deployment_proxyignores.golden.yml",
			reportFileName:   "inject_emojivoto_deployment.report",
			injectProxy:      true,
			testInjectConfig: proxyIgnorePortsConfig,
		},
		{
			inputFileName:    "inject_emojivoto_pod.input.yml",
			goldenFileName:   "inject_emojivoto_pod_proxyignores.golden.yml",
			reportFileName:   "inject_emojivoto_pod.report",
			injectProxy:      true,
			testInjectConfig: proxyIgnorePortsConfig,
		},
		{
			inputFileName:    "inject_emojivoto_deployment.input.yml",
			goldenFileName:   "inject_emojivoto_deployment_opaque_ports.golden.yml",
			reportFileName:   "inject_emojivoto_deployment_opaque_ports.report",
			injectProxy:      true,
			testInjectConfig: opaquePortsConfig,
		},
		{
			inputFileName:    "inject_emojivoto_pod.input.yml",
			goldenFileName:   "inject_emojivoto_pod_ingress.golden.yml",
			reportFileName:   "inject_emojivoto_pod_ingress.report",
			injectProxy:      true,
			testInjectConfig: ingressConfig,
		},
		{
			inputFileName:  "inject_emojivoto_deployment.input.yml",
			goldenFileName: "inject_emojivoto_deployment_default_inbound_policy.golden.yml",
			reportFileName: "inject_emojivoto_deployment_default_inbound_policy.golden.report",
			injectProxy:    false,
			testInjectConfig: func() *linkerd2.Values {
				values := defaultConfig()
				values.Proxy.DefaultInboundPolicy = k8s.AllAuthenticated
				return values
			}(),
		},
		{
			inputFileName:  "inject_emojivoto_pod.input.yml",
			goldenFileName: "inject_emojivoto_pod_default_inbound_policy.golden.yml",
			reportFileName: "inject_emojivoto_pod_default_inbound_policy.golden.report",
			injectProxy:    false,
			testInjectConfig: func() *linkerd2.Values {
				values := defaultConfig()
				values.Proxy.DefaultInboundPolicy = k8s.AllAuthenticated
				return values
			}(),
		},
		{
			inputFileName:  "inject_emojivoto_deployment.input.yml",
			goldenFileName: "inject_emojivoto_deployment_native_sidecar.golden.yml",
			reportFileName: "inject_emojivoto_deployment.report",
			injectProxy:    true,
			testInjectConfig: func() *linkerd2.Values {
				values := defaultConfig()
				values.Proxy.NativeSidecar = true
				return values
			}(),
		},
		{
			inputFileName:  "inject_emojivoto_deployment.input.yml",
			goldenFileName: "inject_emojivoto_deployment_params.golden.yml",
			reportFileName: "inject_emojivoto_deployment.report",
			injectProxy:    true,
			testInjectConfig: func() *linkerd2.Values {
				values := defaultConfig()
				values.Proxy.Inbound = linkerd2.ProxyParams{
					"scope": linkerd2.ProxyScopeParams{
						"proto": linkerd2.ProxyProtoParams{
							"appleSauce": "valueA",
							"blueberry":  3.14,
						},
					},
				}
				values.Proxy.Outbound = linkerd2.ProxyParams{
					"scope": linkerd2.ProxyScopeParams{
						"proto": linkerd2.ProxyProtoParams{
							"applesauce": "valueA",
							"blueBerry":  true,
						},
					},
				}
				return values
			}(),
		},
	}

	for i, tc := range testCases {
		tc := tc // pin
		verbose = true
		t.Run(fmt.Sprintf("%d: %s --verbose", i, tc.inputFileName), func(t *testing.T) {
			testUninjectAndInject(t, tc)
		})
		verbose = false
		t.Run(fmt.Sprintf("%d: %s", i, tc.inputFileName), func(t *testing.T) {
			testUninjectAndInject(t, tc)
		})
	}
}

type injectCmd struct {
	inputFileName        string
	stdErrGoldenFileName string
	stdOutGoldenFileName string
	exitCode             int
	injectProxy          bool
	values               *linkerd2.Values
}

func testInjectCmd(t *testing.T, tc injectCmd) {
	t.Helper()

	testConfig := tc.values
	if testConfig == nil {
		var err error
		testConfig, err = testInstallValues()
		if err != nil {
			t.Fatalf("Unexpected error: %v", err)
		}
	}
	testConfig.Proxy.Image.Version = "testinjectversion"

	errBuffer := &bytes.Buffer{}
	outBuffer := &bytes.Buffer{}

	in, err := os.Open(fmt.Sprintf("testdata/%s", tc.inputFileName))
	if err != nil {
		t.Fatalf("Unexpected error: %v", err)
	}

	transformer := &resourceTransformerInject{
		injectProxy: tc.injectProxy,
		values:      testConfig,
		overrider:   inject.GetOverriddenValues,
	}
	exitCode := runInjectCmd([]io.Reader{in}, errBuffer, outBuffer, transformer, "yaml")
	if exitCode != tc.exitCode {
		t.Fatalf("Expected exit code to be %d but got: %d", tc.exitCode, exitCode)
	}
	if tc.stdOutGoldenFileName != "" {
		testDataDiffer.DiffTestdata(t, tc.stdOutGoldenFileName, outBuffer.String())
	} else if outBuffer.Len() != 0 {
		t.Fatalf("Expected no standard output, but got: %s", outBuffer)
	}

	stdErrGoldenFileName := mkFilename(tc.stdErrGoldenFileName, verbose)
	testDataDiffer.DiffTestdata(t, stdErrGoldenFileName, errBuffer.String())
}

func TestRunInjectCmd(t *testing.T) {
	testCases := []injectCmd{
		{
			inputFileName:        "inject_gettest_deployment.bad.input.yml",
			stdErrGoldenFileName: "inject_gettest_deployment.bad.golden",
			exitCode:             1,
			injectProxy:          true,
		},
		{
			inputFileName:        "inject_tap_deployment.input.yml",
			stdErrGoldenFileName: "inject_tap_deployment.bad.golden",
			exitCode:             1,
			injectProxy:          false,
		},
		{
			inputFileName:        "inject_gettest_deployment.good.input.yml",
			stdOutGoldenFileName: "inject_gettest_deployment.good.golden.yml",
			stdErrGoldenFileName: "inject_gettest_deployment.good.golden.stderr",
			exitCode:             0,
			injectProxy:          true,
		},
		{
			inputFileName:        "inject_emojivoto_deployment_automountServiceAccountToken_false.input.yml",
			stdOutGoldenFileName: "inject_emojivoto_deployment_automountServiceAccountToken_false.golden.yml",
			stdErrGoldenFileName: "inject_emojivoto_deployment_automountServiceAccountToken_false.golden.stderr",
			exitCode:             0,
			injectProxy:          true,
		},
		{
			inputFileName:        "inject_emojivoto_deployment_automountServiceAccountToken_false.input.yml",
			stdOutGoldenFileName: "inject_emojivoto_deployment_automountServiceAccountToken_false_volumeProjection_disabled.golden.yml",
			stdErrGoldenFileName: "inject_emojivoto_deployment_automountServiceAccountToken_false_volumeProjection_disabled.golden.stderr",
			exitCode:             1,
			injectProxy:          false,
			values: func() *linkerd2.Values {
				values, _ := testInstallValues()
				values.Identity.ServiceAccountTokenProjection = false
				return values
			}(),
		},
		{
			inputFileName:        "inject_emojivoto_istio.input.yml",
			stdOutGoldenFileName: "inject_emojivoto_istio.golden.yml",
			stdErrGoldenFileName: "inject_emojivoto_istio.golden.stderr",
			exitCode:             1,
			injectProxy:          true,
		},
		{
			inputFileName:        "inject_emojivoto_deployment_hostNetwork_true.input.yml",
			stdOutGoldenFileName: "inject_emojivoto_deployment_hostNetwork_true.golden.yml",
			stdErrGoldenFileName: "inject_emojivoto_deployment_hostNetwork_true.golden.stderr",
			exitCode:             1,
			injectProxy:          true,
		},
	}

	for i, tc := range testCases {
		tc := tc // pin
		verbose = true
		t.Run(fmt.Sprintf("%d: %s --verbose", i, tc.inputFileName), func(t *testing.T) {
			testInjectCmd(t, tc)
		})
		verbose = false
		t.Run(fmt.Sprintf("%d: %s", i, tc.inputFileName), func(t *testing.T) {
			testInjectCmd(t, tc)
		})
	}
}

type injectFilePath struct {
	resource     string
	resourceFile string
	expectedFile string
	stdErrFile   string
}

func testInjectFilePath(t *testing.T, tc injectFilePath) {
	in, err := read("testdata/" + tc.resourceFile)
	if err != nil {
		t.Fatal("Unexpected error: ", err)
	}

	errBuf := &bytes.Buffer{}
	actual := &bytes.Buffer{}
	values, err := testInstallValues()
	if err != nil {
		t.Fatalf("Unexpected error: %v", err)
	}
	transformer := &resourceTransformerInject{
		injectProxy: true,
		values:      values,
		overrider:   inject.GetOverriddenValues,
	}
	if exitCode := runInjectCmd(in, errBuf, actual, transformer, "yaml"); exitCode != 0 {
		t.Fatal("Unexpected error. Exit code from runInjectCmd: ", exitCode)
	}
	if err := testDataDiffer.DiffTestYAML(tc.expectedFile, actual.String()); err != nil {
		t.Error(err)
	}

	stdErrFile := mkFilename(tc.stdErrFile, verbose)
	testDataDiffer.DiffTestdata(t, stdErrFile, errBuf.String())
}

func testReadFromFolder(t *testing.T, resourceFolder string, expectedFolder string) {
	in, err := read("testdata/" + resourceFolder)
	if err != nil {
		t.Fatal("Unexpected error: ", err)
	}

	values, err := testInstallValues()
	if err != nil {
		t.Fatalf("Unexpected error: %v", err)
	}
	errBuf := &bytes.Buffer{}
	actual := &bytes.Buffer{}
	transformer := &resourceTransformerInject{
		injectProxy: true,
		values:      values,
		overrider:   inject.GetOverriddenValues,
	}
	if exitCode := runInjectCmd(in, errBuf, actual, transformer, "yaml"); exitCode != 0 {
		t.Fatal("Unexpected error. Exit code from runInjectCmd: ", exitCode)
	}

	expectedFile := filepath.Join(expectedFolder, "injected_nginx_redis.yaml")
	if err := testDataDiffer.DiffTestYAML(expectedFile, actual.String()); err != nil {
		t.Error(err)
	}

	stdErrFileName := mkFilename(filepath.Join(expectedFolder, "injected_nginx_redis.stderr"), verbose)
	testDataDiffer.DiffTestdata(t, stdErrFileName, errBuf.String())
}

func TestInjectFilePath(t *testing.T) {
	var (
		resourceFolder = filepath.Join("inject-filepath", "resources")
		expectedFolder = filepath.Join("inject-filepath", "expected")
	)

	t.Run("read from files", func(t *testing.T) {
		testCases := []injectFilePath{
			{
				resource:     "nginx",
				resourceFile: filepath.Join(resourceFolder, "nginx.yaml"),
				expectedFile: filepath.Join(expectedFolder, "injected_nginx.yaml"),
				stdErrFile:   filepath.Join(expectedFolder, "injected_nginx.stderr"),
			},
			{
				resource:     "redis",
				resourceFile: filepath.Join(resourceFolder, "db/redis.yaml"),
				expectedFile: filepath.Join(expectedFolder, "injected_redis.yaml"),
				stdErrFile:   filepath.Join(expectedFolder, "injected_redis.stderr"),
			},
		}

		for i, testCase := range testCases {
			testCase := testCase // pin
			verbose = true
			t.Run(fmt.Sprintf("%d %s", i, testCase.resource), func(t *testing.T) {
				testInjectFilePath(t, testCase)
			})
			verbose = false
			t.Run(fmt.Sprintf("%d %s", i, testCase.resource), func(t *testing.T) {
				testInjectFilePath(t, testCase)
			})
		}
	})

	verbose = true
	t.Run("read from folder --verbose", func(t *testing.T) {
		testReadFromFolder(t, resourceFolder, expectedFolder)
	})
	verbose = false
	t.Run("read from folder --verbose", func(t *testing.T) {
		testReadFromFolder(t, resourceFolder, expectedFolder)
	})
}

func TestToURL(t *testing.T) {
	// if the string follows a URL pattern, true has to be returned
	// if not false is returned

	tests := map[string]bool{
		"http://www.linkerd.io":  true,
		"https://www.linkerd.io": true,
		"www.linkerd.io/":        false,
		"~/foo/bar.yaml":         false,
		"./foo/bar.yaml":         false,
		"/foo/bar/baz.yml":       false,
		"../foo/bar/baz.yaml":    false,
		"https//":                false,
	}

	for url, expectedValue := range tests {
		_, ok := toURL(url)
		if ok != expectedValue {
			t.Errorf("Result mismatch for %s. expected %v, but got %v", url, expectedValue, ok)
		}
	}

}

func TestWalk(t *testing.T) {
	// create two data files, one in the root folder and the other in a subfolder.
	// walk should be able to read the content of the two data files recursively.
	var (
		tmpFolderRoot = "linkerd-testdata"
		tmpFolderData = filepath.Join(tmpFolderRoot, "data")
	)

	if err := os.MkdirAll(tmpFolderData, os.ModeDir|os.ModePerm); err != nil {
		t.Fatal("Unexpected error: ", err)
	}
	defer func() {
		err := os.RemoveAll(tmpFolderRoot)
		if err != nil {
			t.Errorf("failed to remove temp dir %q: %v", tmpFolderRoot, err)
		}
	}()

	var (
		data  = []byte(testutil.ReadTestdata("inject_gettest_deployment.bad.input.yml"))
		file1 = filepath.Join(tmpFolderRoot, "root.txt")
		file2 = filepath.Join(tmpFolderData, "data.txt")
	)
	if err := os.WriteFile(file1, data, 0600); err != nil {
		t.Fatal("Unexpected error: ", err)
	}
	if err := os.WriteFile(file2, data, 0600); err != nil {
		t.Fatal("Unexpected error: ", err)
	}

	actual, err := walk(tmpFolderRoot)
	if err != nil {
		t.Fatal("Unexpected error: ", err)
	}

	for _, r := range actual {
		b := make([]byte, len(data))
		r.Read(b)

		if string(b) != string(data) {
			t.Errorf("Content mismatch. Expected %q, but got %q", data, b)
		}
	}
}

func TestProxyConfigurationAnnotations(t *testing.T) {
	baseValues, err := linkerd2.NewValues()
	if err != nil {
		t.Fatal(err)
	}
	values, err := baseValues.DeepCopy()
	if err != nil {
		t.Fatal(err)
	}
	values.ProxyInit.IgnoreInboundPorts = "8500-8505"
	values.ProxyInit.IgnoreOutboundPorts = "3306"
	values.Proxy.Ports.Admin = 1234
	values.Proxy.Ports.Control = 4191
	values.Proxy.Ports.Inbound = 4144
	values.Proxy.Ports.Outbound = 4141
	values.Proxy.UID = 999
	values.Proxy.GID = 999
	values.Proxy.LogLevel = "debug"
	values.Proxy.LogFormat = "cool"
	values.Proxy.EnableExternalProfiles = true
	values.Proxy.Resources.CPU.Request = "10m"
	values.Proxy.Resources.CPU.Limit = "100m"
	values.Proxy.Resources.Memory.Request = "10Mi"
	values.Proxy.Resources.Memory.Limit = "50Mi"
	values.Proxy.WaitBeforeExitSeconds = 10
	values.Proxy.Await = false
	values.Proxy.AccessLog = "apache"
	values.Proxy.ShutdownGracePeriod = "60s"
	values.Proxy.NativeSidecar = true

	expectedOverrides := map[string]string{
		k8s.ProxyIgnoreInboundPortsAnnotation:  "8500-8505",
		k8s.ProxyIgnoreOutboundPortsAnnotation: "3306",
		k8s.ProxyAdminPortAnnotation:           "1234",
		k8s.ProxyControlPortAnnotation:         "4191",
		k8s.ProxyInboundPortAnnotation:         "4144",
		k8s.ProxyOutboundPortAnnotation:        "4141",
		k8s.ProxyUIDAnnotation:                 "999",
		k8s.ProxyGIDAnnotation:                 "999",
		k8s.ProxyLogLevelAnnotation:            "debug",
		k8s.ProxyLogFormatAnnotation:           "cool",

		k8s.ProxyEnableExternalProfilesAnnotation: "true",
		k8s.ProxyCPURequestAnnotation:             "10m",
		k8s.ProxyCPULimitAnnotation:               "100m",
		k8s.ProxyMemoryRequestAnnotation:          "10Mi",
		k8s.ProxyMemoryLimitAnnotation:            "50Mi",
		k8s.ProxyWaitBeforeExitSecondsAnnotation:  "10",
		k8s.ProxyAwait:                            "disabled",
		k8s.ProxyAccessLogAnnotation:              "apache",
		k8s.ProxyShutdownGracePeriodAnnotation:    "60s",
		k8s.ProxyEnableNativeSidecarAnnotation:    "true",
	}

	overrides := getOverrideAnnotations(values, baseValues)

	diffOverrides(t, expectedOverrides, overrides)
}

func TestProxyImageAnnotations(t *testing.T) {
	baseValues, err := linkerd2.NewValues()
	if err != nil {
		t.Fatal(err)
	}
	values, err := baseValues.DeepCopy()
	if err != nil {
		t.Fatal(err)
	}
	values.Proxy.Image = &linkerd2.Image{
		Name:       "my.registry/linkerd/proxy",
		Version:    "test-proxy-version",
		PullPolicy: "Always",
	}

	expectedOverrides := map[string]string{
		k8s.ProxyImageAnnotation:           "my.registry/linkerd/proxy",
		k8s.ProxyVersionOverrideAnnotation: "test-proxy-version",
		k8s.ProxyImagePullPolicyAnnotation: "Always",
	}

	overrides := getOverrideAnnotations(values, baseValues)

	diffOverrides(t, expectedOverrides, overrides)
}

func TestProxyInitImageAnnotations(t *testing.T) {
	baseValues, err := linkerd2.NewValues()
	if err != nil {
		t.Fatal(err)
	}
	values, err := baseValues.DeepCopy()
	if err != nil {
		t.Fatal(err)
	}
	values.ProxyInit.Image = &linkerd2.Image{
		Name:    "my.registry/linkerd/proxy-init",
		Version: "test-proxy-init-version",
	}

	expectedOverrides := map[string]string{
		k8s.ProxyInitImageAnnotation:        "my.registry/linkerd/proxy-init",
		k8s.ProxyInitImageVersionAnnotation: "test-proxy-init-version",
	}

	overrides := getOverrideAnnotations(values, baseValues)

	diffOverrides(t, expectedOverrides, overrides)
}

func TestNoAnnotations(t *testing.T) {
	baseValues, err := linkerd2.NewValues()
	if err != nil {
		t.Fatal(err)
	}
	values, err := baseValues.DeepCopy()
	if err != nil {
		t.Fatal(err)
	}

	expectedOverrides := map[string]string{}

	overrides := getOverrideAnnotations(values, baseValues)

	diffOverrides(t, expectedOverrides, overrides)
}

func TestOverwriteRegistry(t *testing.T) {
	testCases := []struct {
		image    string
		registry string
		expected string
	}{
		{
			image:    "cr.l5d.io/linkerd/image",
			registry: "my.custom.registry",
			expected: "my.custom.registry/image",
		},
		{
			image:    "cr.l5d.io/linkerd/image",
			registry: "my.custom.registry/",
			expected: "my.custom.registry/image",
		},
		{
			image:    "my.custom.registry/image",
			registry: "my.custom.registry",
			expected: "my.custom.registry/image",
		},
		{
			image:    "my.custom.registry/image",
			registry: "cr.l5d.io/linkerd",
			expected: "cr.l5d.io/linkerd/image",
		},
		{
			image:    "",
			registry: "my.custom.registry",
			expected: "",
		},
		{
			image:    "cr.l5d.io/linkerd/image",
			registry: "",
			expected: "image",
		},
		{
			image:    "image",
			registry: "cr.l5d.io/linkerd",
			expected: "cr.l5d.io/linkerd/image",
		},
	}
	for i, tc := range testCases {
		tc := tc // pin
		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
			actual := cmd.RegistryOverride(tc.image, tc.registry)
			if actual != tc.expected {
				t.Fatalf("expected %q, but got %q", tc.expected, actual)
			}
		})
	}
}

func diffOverrides(t *testing.T, expectedOverrides map[string]string, actualOverrides map[string]string) {
	if len(expectedOverrides) != len(actualOverrides) {
		t.Fatalf("expected annotations:\n%s\nbut received:\n%s", expectedOverrides, actualOverrides)
	}
	for key, expected := range expectedOverrides {
		actual := actualOverrides[key]
		if actual != expected {
			t.Fatalf("expected annotation %q with %q, but got %q", key, expected, actual)
		}
	}
}
