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

package kioutil_test

import (
	"bytes"
	"math/rand"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"sigs.k8s.io/kustomize/kyaml/kio"
	"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
	"sigs.k8s.io/kustomize/kyaml/yaml"
)

func TestSortNodes_moreThan10(t *testing.T) {
	input := `
a: b
---
c: d
---
e: f
---
g: h
---
i: j
---
k: l
---
m: n
---
o: p
---
q: r
---
s: t
---
u: v
---
w: x
---
y: z
`
	actual := &bytes.Buffer{}
	rw := kio.ByteReadWriter{Reader: bytes.NewBufferString(input), Writer: actual}
	nodes, err := rw.Read()
	if !assert.NoError(t, err) {
		t.Fail()
	}

	// randomize the list
	rand.Seed(time.Now().UnixNano())
	rand.Shuffle(len(nodes), func(i, j int) { nodes[i], nodes[j] = nodes[j], nodes[i] })

	// sort them back into their original order
	if !assert.NoError(t, kioutil.SortNodes(nodes)) {
		t.Fail()
	}

	// check the sorted values
	expected := strings.Split(input, "---")
	for i := range nodes {
		a := strings.TrimSpace(nodes[i].MustString())
		b := strings.TrimSpace(expected[i])
		if !assert.Contains(t, a, b) {
			t.Fail()
		}
	}

	if !assert.NoError(t, rw.Write(nodes)) {
		t.Fail()
	}

	assert.Equal(t, strings.TrimSpace(input), strings.TrimSpace(actual.String()))
}

func TestDefaultPathAnnotation(t *testing.T) {
	var tests = []struct {
		dir      string
		input    string // input
		expected string // expected result
		name     string
	}{
		{
			`foo`,
			`apiVersion: v1
kind: Bar
metadata:
  name: a
  namespace: b
`,
			`apiVersion: v1
kind: Bar
metadata:
  name: a
  namespace: b
  annotations:
    internal.config.kubernetes.io/path: 'foo/b/bar_a.yaml'
    config.kubernetes.io/path: 'foo/b/bar_a.yaml'
`, `with namespace`},
		{
			`foo`,
			`apiVersion: v1
kind: Bar
metadata:
  name: a
`,
			`apiVersion: v1
kind: Bar
metadata:
  name: a
  annotations:
    internal.config.kubernetes.io/path: 'foo/bar_a.yaml'
    config.kubernetes.io/path: 'foo/bar_a.yaml'
`, `without namespace`},

		{
			``,
			`apiVersion: v1
kind: Bar
metadata:
  name: a
  namespace: b
`,
			`apiVersion: v1
kind: Bar
metadata:
  name: a
  namespace: b
  annotations:
    internal.config.kubernetes.io/path: 'b/bar_a.yaml'
    config.kubernetes.io/path: 'b/bar_a.yaml'
`, `without dir`},
		{
			``,
			`apiVersion: v1
kind: Bar
metadata:
  name: a
  namespace: b
  annotations:
    internal.config.kubernetes.io/path: 'a/b.yaml'
    config.kubernetes.io/path: 'a/b.yaml'
`,
			`apiVersion: v1
kind: Bar
metadata:
  name: a
  namespace: b
  annotations:
    internal.config.kubernetes.io/path: 'a/b.yaml'
    config.kubernetes.io/path: 'a/b.yaml'
`, `skip`},
	}

	for _, s := range tests {
		n := yaml.MustParse(s.input)
		err := kioutil.DefaultPathAnnotation(s.dir, []*yaml.RNode{n})
		if !assert.NoError(t, err, s.name) {
			t.FailNow()
		}
		if !assert.Equal(t, s.expected, n.MustString(), s.name) {
			t.FailNow()
		}
	}
}

func TestDefaultPathAndIndexAnnotation(t *testing.T) {
	var tests = []struct {
		dir      string
		input    string // input
		expected string // expected result
		name     string
	}{
		{
			`foo`,
			`apiVersion: v1
kind: Bar
metadata:
  name: a
  namespace: b
`,
			`apiVersion: v1
kind: Bar
metadata:
  name: a
  namespace: b
  annotations:
    internal.config.kubernetes.io/path: 'foo/b/bar_a.yaml'
    config.kubernetes.io/path: 'foo/b/bar_a.yaml'
    internal.config.kubernetes.io/index: '0'
    config.kubernetes.io/index: '0'
`, `with namespace`},
		{
			`foo`,
			`apiVersion: v1
kind: Bar
metadata:
  name: a
`,
			`apiVersion: v1
kind: Bar
metadata:
  name: a
  annotations:
    internal.config.kubernetes.io/path: 'foo/bar_a.yaml'
    config.kubernetes.io/path: 'foo/bar_a.yaml'
    internal.config.kubernetes.io/index: '0'
    config.kubernetes.io/index: '0'
`, `without namespace`},

		{
			``,
			`apiVersion: v1
kind: Bar
metadata:
  name: a
  namespace: b
`,
			`apiVersion: v1
kind: Bar
metadata:
  name: a
  namespace: b
  annotations:
    internal.config.kubernetes.io/path: 'b/bar_a.yaml'
    config.kubernetes.io/path: 'b/bar_a.yaml'
    internal.config.kubernetes.io/index: '0'
    config.kubernetes.io/index: '0'
`, `without dir`},
		{
			``,
			`apiVersion: v1
kind: Bar
metadata:
  name: a
  namespace: b
  annotations:
    internal.config.kubernetes.io/path: 'a/b.yaml'
    config.kubernetes.io/path: 'a/b.yaml'
    internal.config.kubernetes.io/index: '5'
    config.kubernetes.io/index: '5'
`,
			`apiVersion: v1
kind: Bar
metadata:
  name: a
  namespace: b
  annotations:
    internal.config.kubernetes.io/path: 'a/b.yaml'
    config.kubernetes.io/path: 'a/b.yaml'
    internal.config.kubernetes.io/index: '5'
    config.kubernetes.io/index: '5'
`, `skip`},
	}

	for _, s := range tests {
		out := &bytes.Buffer{}
		r := kio.ByteReadWriter{
			Reader:                bytes.NewBufferString(s.input),
			Writer:                out,
			KeepReaderAnnotations: true,
			OmitReaderAnnotations: true,
		}
		n, err := r.Read()
		if !assert.NoError(t, err, s.name) {
			t.FailNow()
		}
		if !assert.NoError(t, kioutil.DefaultPathAndIndexAnnotation(s.dir, n), s.name) {
			t.FailNow()
		}
		if !assert.NoError(t, r.Write(n), s.name) {
			t.FailNow()
		}
		if !assert.Equal(t, s.expected, out.String(), s.name) {
			t.FailNow()
		}
	}
}

func TestCreatePathAnnotationValue(t *testing.T) {
	var tests = []struct {
		dir      string
		meta     yaml.ResourceMeta // input
		expected string            // expected result
		name     string
	}{
		{
			`dir`,
			yaml.ResourceMeta{
				TypeMeta: yaml.TypeMeta{
					APIVersion: "apps/v1",
					Kind:       "foo",
				},
				ObjectMeta: yaml.ObjectMeta{
					NameMeta: yaml.NameMeta{
						Name: "bar", Namespace: "baz",
					},
				},
			},
			`dir/baz/foo_bar.yaml`, `with namespace`,
		},
		{
			``,
			yaml.ResourceMeta{
				TypeMeta: yaml.TypeMeta{
					APIVersion: "apps/v1",
					Kind:       "foo",
				},
				ObjectMeta: yaml.ObjectMeta{
					NameMeta: yaml.NameMeta{
						Name: "bar", Namespace: "baz",
					},
				},
			},
			`baz/foo_bar.yaml`, `without dir`,
		},
		{
			`dir`,
			yaml.ResourceMeta{
				TypeMeta: yaml.TypeMeta{
					APIVersion: "apps/v1",
					Kind:       "foo",
				},
				ObjectMeta: yaml.ObjectMeta{
					NameMeta: yaml.NameMeta{Name: "bar"},
				},
			},
			`dir/foo_bar.yaml`, `without namespace`,
		},
		{
			``,
			yaml.ResourceMeta{
				TypeMeta: yaml.TypeMeta{
					APIVersion: "apps/v1",
					Kind:       "foo",
				},
				ObjectMeta: yaml.ObjectMeta{
					NameMeta: yaml.NameMeta{Name: "bar"},
				},
			},
			`foo_bar.yaml`, `without namespace or dir`,
		},
		{
			``,
			yaml.ResourceMeta{
				TypeMeta: yaml.TypeMeta{
					APIVersion: "apps/v1",
					Kind:       "foo",
				},
				ObjectMeta: yaml.ObjectMeta{},
			},
			`foo_.yaml`, `without namespace, dir or name`,
		},
		{
			``,
			yaml.ResourceMeta{
				TypeMeta: yaml.TypeMeta{
					APIVersion: "apps/v1",
				},
				ObjectMeta: yaml.ObjectMeta{},
			},
			`_.yaml`, `without any`,
		},
	}

	for _, s := range tests {
		p := kioutil.CreatePathAnnotationValue(s.dir, s.meta)
		if !assert.Equal(t, s.expected, p, s.name) {
			t.FailNow()
		}
	}
}

func TestCopyLegacyAnnotations(t *testing.T) {
	var tests = []struct {
		input    string
		expected string
	}{
		{
			input: `apiVersion: v1
kind: Foo
metadata:
  name: foobar
  annotations:
    config.kubernetes.io/path: 'a/b.yaml'
    config.kubernetes.io/index: '5'
`,
			expected: `apiVersion: v1
kind: Foo
metadata:
  name: foobar
  annotations:
    config.kubernetes.io/path: 'a/b.yaml'
    config.kubernetes.io/index: '5'
    internal.config.kubernetes.io/path: 'a/b.yaml'
    internal.config.kubernetes.io/index: '5'
`,
		},
		{
			input: `apiVersion: v1
kind: Foo
metadata:
  name: foobar
  annotations:
    internal.config.kubernetes.io/path: 'a/b.yaml'
    internal.config.kubernetes.io/index: '5'
`,
			expected: `apiVersion: v1
kind: Foo
metadata:
  name: foobar
  annotations:
    internal.config.kubernetes.io/path: 'a/b.yaml'
    internal.config.kubernetes.io/index: '5'
    config.kubernetes.io/path: 'a/b.yaml'
    config.kubernetes.io/index: '5'
`,
		},
		{
			input: `apiVersion: v1
kind: Foo
metadata:
 name: foobar
 annotations:
   internal.config.kubernetes.io/path: 'a/b.yaml'
   config.kubernetes.io/path: 'c/d.yaml'
`,
			expected: `apiVersion: v1
kind: Foo
metadata:
  name: foobar
  annotations:
    internal.config.kubernetes.io/path: 'a/b.yaml'
    config.kubernetes.io/path: 'c/d.yaml'
`,
		},
	}

	for _, tc := range tests {
		rw := kio.ByteReadWriter{
			Reader:                bytes.NewBufferString(tc.input),
			OmitReaderAnnotations: true,
		}
		nodes, err := rw.Read()
		require.NoError(t, err)
		require.NoError(t, kioutil.CopyLegacyAnnotations(nodes[0]))
		assert.Equal(t, tc.expected, nodes[0].MustString())
	}
}

func TestCopyInternalAnnotations(t *testing.T) {
	var tests = []struct {
		input      string
		exclusions []kioutil.AnnotationKey
		expected   string
	}{
		{
			input: `apiVersion: v1
kind: Foo
metadata:
  name: src
  annotations:
    internal.config.kubernetes.io/path: 'a/b.yaml'
    internal.config.kubernetes.io/index: '5'
    internal.config.kubernetes.io/foo: 'bar'
---
apiVersion: v1
kind: Foo
metadata:
  name: dst
  annotations:
    internal.config.kubernetes.io/path: 'c/d.yaml'
    internal.config.kubernetes.io/index: '10'
`,
			expected: `apiVersion: v1
kind: Foo
metadata:
  name: dst
  annotations:
    internal.config.kubernetes.io/path: 'a/b.yaml'
    internal.config.kubernetes.io/index: '5'
    internal.config.kubernetes.io/foo: 'bar'
`,
		},
		{
			input: `apiVersion: v1
kind: Foo
metadata:
  name: src
  annotations:
    internal.config.kubernetes.io/path: 'a/b.yaml'
    internal.config.kubernetes.io/index: '5'
    internal.config.kubernetes.io/foo: 'bar-src'
---
apiVersion: v1
kind: Foo
metadata:
  name: dst
  annotations:
    internal.config.kubernetes.io/path: 'c/d.yaml'
    internal.config.kubernetes.io/index: '10'
    internal.config.kubernetes.io/foo: 'bar-dst'
`,
			exclusions: []kioutil.AnnotationKey{
				kioutil.PathAnnotation,
				kioutil.IndexAnnotation,
			},
			expected: `apiVersion: v1
kind: Foo
metadata:
  name: dst
  annotations:
    internal.config.kubernetes.io/path: 'c/d.yaml'
    internal.config.kubernetes.io/index: '10'
    internal.config.kubernetes.io/foo: 'bar-src'
`,
		},
	}

	for _, tc := range tests {
		rw := kio.ByteReadWriter{
			Reader:                bytes.NewBufferString(tc.input),
			OmitReaderAnnotations: true,
		}
		nodes, err := rw.Read()
		require.NoError(t, err)
		require.NoError(t, kioutil.CopyInternalAnnotations(nodes[0], nodes[1], tc.exclusions...))
		assert.Equal(t, tc.expected, nodes[1].MustString())
	}
}

func TestConfirmInternalAnnotationUnchanged(t *testing.T) {
	var tests = []struct {
		input       string
		exclusions  []kioutil.AnnotationKey
		expectedErr string
	}{
		{
			input: `apiVersion: v1
kind: Foo
metadata:
  name: foo-1
  annotations:
    internal.config.kubernetes.io/path: 'a/b.yaml'
    internal.config.kubernetes.io/index: '5'
---
apiVersion: v1
kind: Foo
metadata:
  name: foo-2
  annotations:
    internal.config.kubernetes.io/path: 'c/d.yaml'
    internal.config.kubernetes.io/index: '10'
`,
			expectedErr: `internal annotations differ: internal.config.kubernetes.io/index, internal.config.kubernetes.io/path`,
		},
		{
			input: `apiVersion: v1
kind: Foo
metadata:
  name: foo-1
  annotations:
    internal.config.kubernetes.io/path: 'a/b.yaml'
    internal.config.kubernetes.io/index: '5'
---
apiVersion: v1
kind: Foo
metadata:
  name: foo-2
  annotations:
    internal.config.kubernetes.io/path: 'c/d.yaml'
    internal.config.kubernetes.io/index: '10'
`,
			exclusions: []kioutil.AnnotationKey{
				kioutil.PathAnnotation,
				kioutil.IndexAnnotation,
			},
			expectedErr: ``,
		},
		{
			input: `apiVersion: v1
kind: Foo
metadata:
  name: foo-1
  annotations:
    internal.config.kubernetes.io/path: 'a/b.yaml'
    internal.config.kubernetes.io/index: '5'
    internal.config.kubernetes.io/foo: 'bar-1'
---
apiVersion: v1
kind: Foo
metadata:
  name: foo-2
  annotations:
    internal.config.kubernetes.io/path: 'c/d.yaml'
    internal.config.kubernetes.io/index: '10'
    internal.config.kubernetes.io/foo: 'bar-2'
`,
			exclusions: []kioutil.AnnotationKey{
				kioutil.PathAnnotation,
				kioutil.IndexAnnotation,
			},
			expectedErr: `internal annotations differ: internal.config.kubernetes.io/foo`,
		},
		{
			input: `apiVersion: v1
kind: Foo
metadata:
  name: foo-1
  annotations:
    internal.config.kubernetes.io/path: 'a/b.yaml'
    internal.config.kubernetes.io/index: '5'
    internal.config.kubernetes.io/foo: 'bar-1'
---
apiVersion: v1
kind: Foo
metadata:
  name: foo-2
  annotations:
    internal.config.kubernetes.io/path: 'c/d.yaml'
    internal.config.kubernetes.io/index: '10'
    internal.config.kubernetes.io/foo: 'bar-1'
`,
			expectedErr: `internal annotations differ: internal.config.kubernetes.io/index, internal.config.kubernetes.io/path`,
		},
		{
			input: `apiVersion: v1
kind: Foo
metadata:
  name: foo-1
  annotations:
    internal.config.kubernetes.io/a: 'b'
    internal.config.kubernetes.io/c: 'd'
---
apiVersion: v1
kind: Foo
metadata:
  name: foo-2
  annotations:
    internal.config.kubernetes.io/e: 'f'
    internal.config.kubernetes.io/g: 'h'
`,
			expectedErr: `internal annotations differ: ` +
				`internal.config.kubernetes.io/a, internal.config.kubernetes.io/c, ` +
				`internal.config.kubernetes.io/e, internal.config.kubernetes.io/g`,
		},
	}

	for _, tc := range tests {
		rw := kio.ByteReadWriter{
			Reader:                bytes.NewBufferString(tc.input),
			OmitReaderAnnotations: true,
		}
		nodes, err := rw.Read()
		require.NoError(t, err)
		err = kioutil.ConfirmInternalAnnotationUnchanged(nodes[0], nodes[1], tc.exclusions...)
		if tc.expectedErr == "" {
			require.NoError(t, err)
		} else {
			if err == nil {
				t.Fatalf("expected error: %s\n", tc.expectedErr)
			}
			assert.Equal(t, tc.expectedErr, err.Error())
		}
	}
}

func TestGetInternalAnnotations(t *testing.T) {
	var tests = []struct {
		input    string
		expected map[string]string
	}{
		{
			input: `apiVersion: v1
kind: Foo
metadata:
  name: foobar
  annotations:
    foo: bar
    internal.config.kubernetes.io/path: 'a/b.yaml'
    internal.config.kubernetes.io/index: '5'
    internal.config.kubernetes.io/foo: 'bar'
`,
			expected: map[string]string{
				"internal.config.kubernetes.io/path":  "a/b.yaml",
				"internal.config.kubernetes.io/index": "5",
				"internal.config.kubernetes.io/foo":   "bar",
			},
		},
	}

	for _, tc := range tests {
		rw := kio.ByteReadWriter{
			Reader:                bytes.NewBufferString(tc.input),
			OmitReaderAnnotations: true,
		}
		nodes, err := rw.Read()
		require.NoError(t, err)
		assert.Equal(t, tc.expected, kioutil.GetInternalAnnotations(nodes[0]))
	}
}
