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

package kio_test

import (
	"bytes"
	"strings"
	"testing"

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

func TestByteReadWriter(t *testing.T) {
	type testCase struct {
		name           string
		err            string
		input          string
		expectedOutput string
		instance       kio.ByteReadWriter
	}

	testCases := []testCase{
		{
			name: "round_trip",
			input: `
apiVersion: config.kubernetes.io/v1
kind: ResourceList
items:
- kind: Deployment
  spec:
    replicas: 1
- kind: Service
  spec:
    selectors:
      foo: bar
`,
			expectedOutput: `
apiVersion: config.kubernetes.io/v1
kind: ResourceList
items:
- kind: Deployment
  spec:
    replicas: 1
- kind: Service
  spec:
    selectors:
      foo: bar
`,
		},

		{
			name: "function_config",
			input: `
apiVersion: config.kubernetes.io/v1
kind: ResourceList
items:
- kind: Deployment
  spec:
    replicas: 1
- kind: Service
  spec:
    selectors:
      foo: bar
functionConfig:
  a: b # something
`,
			expectedOutput: `
apiVersion: config.kubernetes.io/v1
kind: ResourceList
items:
- kind: Deployment
  spec:
    replicas: 1
- kind: Service
  spec:
    selectors:
      foo: bar
functionConfig:
  a: b # something
`,
		},

		{
			name: "results",
			input: `
apiVersion: config.kubernetes.io/v1
kind: ResourceList
items:
- kind: Deployment
  spec:
    replicas: 1
- kind: Service
  spec:
    selectors:
      foo: bar
results:
  a: b # something
`,
			expectedOutput: `
apiVersion: config.kubernetes.io/v1
kind: ResourceList
items:
- kind: Deployment
  spec:
    replicas: 1
- kind: Service
  spec:
    selectors:
      foo: bar
results:
  a: b # something
`,
		},

		{
			name: "drop_invalid_resource_list_field",
			input: `
apiVersion: config.kubernetes.io/v1
kind: ResourceList
items:
- kind: Deployment
  spec:
    replicas: 1
- kind: Service
  spec:
    selectors:
      foo: bar
foo:
  a: b # something
`,
			expectedOutput: `
apiVersion: config.kubernetes.io/v1
kind: ResourceList
items:
- kind: Deployment
  spec:
    replicas: 1
- kind: Service
  spec:
    selectors:
      foo: bar
`,
		},

		{
			name: "list",
			input: `
apiVersion: v1
kind: List
items:
- kind: Deployment
  spec:
    replicas: 1
- kind: Service
  spec:
    selectors:
      foo: bar
`,
			expectedOutput: `
apiVersion: v1
kind: List
items:
- kind: Deployment
  spec:
    replicas: 1
- kind: Service
  spec:
    selectors:
      foo: bar
`,
		},

		{
			name: "multiple_documents",
			input: `
kind: Deployment
spec:
  replicas: 1
---
kind: Service
spec:
  selectors:
    foo: bar
`,
			expectedOutput: `
kind: Deployment
spec:
  replicas: 1
---
kind: Service
spec:
  selectors:
    foo: bar
`,
		},

		{
			name: "keep_annotations",
			input: `
kind: Deployment
spec:
  replicas: 1
---
kind: Service
spec:
  selectors:
    foo: bar
`,
			expectedOutput: `
kind: Deployment
spec:
  replicas: 1
metadata:
  annotations:
    config.kubernetes.io/index: '0'
    internal.config.kubernetes.io/index: '0'
---
kind: Service
spec:
  selectors:
    foo: bar
metadata:
  annotations:
    config.kubernetes.io/index: '1'
    internal.config.kubernetes.io/index: '1'
`,
			instance: kio.ByteReadWriter{KeepReaderAnnotations: true},
		},

		{
			name: "manual_override_wrap",
			input: `
apiVersion: config.kubernetes.io/v1
kind: ResourceList
items:
- kind: Deployment
  spec:
    replicas: 1
- kind: Service
  spec:
    selectors:
      foo: bar
functionConfig:
  a: b # something
`,
			expectedOutput: `
kind: Deployment
spec:
  replicas: 1
---
kind: Service
spec:
  selectors:
    foo: bar
`,
			instance: kio.ByteReadWriter{NoWrap: true},
		},

		{
			name: "manual_override_function_config",
			input: `
apiVersion: config.kubernetes.io/v1
kind: ResourceList
items:
- kind: Deployment
  spec:
    replicas: 1
- kind: Service
  spec:
    selectors:
      foo: bar
functionConfig:
  a: b # something
`,
			expectedOutput: `
apiVersion: config.kubernetes.io/v1
kind: ResourceList
items:
- kind: Deployment
  spec:
    replicas: 1
- kind: Service
  spec:
    selectors:
      foo: bar
functionConfig:
  c: d
`,
			instance: kio.ByteReadWriter{FunctionConfig: yaml.MustParse(`c: d`)},
		},
		{
			name: "anchors_not_inflated",
			input: `
kind: ConfigMap
metadata:
  name: foo
data:
  color: &color-used blue
  feeling: *color-used
`,
			// If YAML anchors were automagically inflated,
			// the expectedOutput would be something like
			//
			// kind: ConfigMap
			// metadata:
			//   name: foo
			// data:
			//   color: blue
			//   feeling: blue
			expectedOutput: `
kind: ConfigMap
metadata:
  name: foo
data:
  color: &color-used blue
  feeling: *color-used
`,
		},
	}

	for i := range testCases {
		tc := testCases[i]
		t.Run(tc.name, func(t *testing.T) {
			var in, out bytes.Buffer
			in.WriteString(tc.input)
			w := tc.instance
			w.Writer = &out
			w.Reader = &in

			nodes, err := w.Read()
			if !assert.NoError(t, err) {
				t.FailNow()
			}

			err = w.Write(nodes)
			if !assert.NoError(t, err) {
				t.FailNow()
			}

			if tc.err != "" {
				if !assert.EqualError(t, err, tc.err) {
					t.FailNow()
				}
				return
			}

			if !assert.Equal(t,
				strings.TrimSpace(tc.expectedOutput), strings.TrimSpace(out.String())) {
				t.FailNow()
			}
		})
	}
}

func TestByteReadWriter_RetainSeqIndent(t *testing.T) {
	type testCase struct {
		name           string
		err            string
		input          string
		expectedOutput string
		instance       kio.ByteReadWriter
	}

	testCases := []testCase{
		{
			name: "round_trip with 2 space seq indent",
			input: `
apiVersion: apps/v1
kind: Deployment
spec:
  - foo
  - bar
---
apiVersion: v1
kind: Service
spec:
  - foo
  - bar
`,
			expectedOutput: `
apiVersion: apps/v1
kind: Deployment
spec:
  - foo
  - bar
---
apiVersion: v1
kind: Service
spec:
  - foo
  - bar
`,
		},
		{
			name: "round_trip with 0 space seq indent",
			input: `
apiVersion: apps/v1
kind: Deployment
spec:
- foo
- bar
---
apiVersion: v1
kind: Service
spec:
- foo
- bar
`,
			expectedOutput: `
apiVersion: apps/v1
kind: Deployment
spec:
- foo
- bar
---
apiVersion: v1
kind: Service
spec:
- foo
- bar
`,
		},
		{
			name: "round_trip with different indentations",
			input: `
apiVersion: apps/v1
kind: Deployment
spec:
  - foo
  - bar
  - baz
---
apiVersion: v1
kind: Service
spec:
- foo
- bar
`,
			expectedOutput: `
apiVersion: apps/v1
kind: Deployment
spec:
  - foo
  - bar
  - baz
---
apiVersion: v1
kind: Service
spec:
- foo
- bar
`,
		},
		{
			name: "round_trip with mixed indentations in same resource, wide wins as it is first",
			input: `
apiVersion: apps/v1
kind: Deployment
spec:
  - foo
env:
- foo
- bar
`,
			expectedOutput: `
apiVersion: apps/v1
kind: Deployment
spec:
  - foo
env:
  - foo
  - bar
`,
		},
		{
			name: "round_trip with mixed indentations in same resource, compact wins as it is first",
			input: `
apiVersion: apps/v1
kind: Deployment
spec:
- foo
env:
  - foo
  - bar
`,
			expectedOutput: `
apiVersion: apps/v1
kind: Deployment
spec:
- foo
env:
- foo
- bar
`,
		},
		{
			name: "unwrap ResourceList with annotations",
			input: `
apiVersion: config.kubernetes.io/v1
kind: ResourceList
items:
  - kind: Deployment
    metadata:
      annotations:
        internal.config.kubernetes.io/seqindent: "compact"
    spec:
      - foo
      - bar
  - kind: Service
    metadata:
      annotations:
        internal.config.kubernetes.io/seqindent: "wide"
    spec:
      - foo
      - bar
`,
			expectedOutput: `
kind: Deployment
spec:
- foo
- bar
---
kind: Service
spec:
  - foo
  - bar
`,
		},
		{
			name: "round_trip with mixed indentations in same resource, wide wins as it is first",
			input: `
apiVersion: apps/v1
kind: Deployment
spec:
  - foo
  - bar
env:
- foo
- bar
- baz
`,
			expectedOutput: `
apiVersion: apps/v1
kind: Deployment
spec:
  - foo
  - bar
env:
  - foo
  - bar
  - baz
`,
		},
	}

	for i := range testCases {
		tc := testCases[i]
		t.Run(tc.name, func(t *testing.T) {
			var in, out bytes.Buffer
			in.WriteString(tc.input)
			w := tc.instance
			w.Writer = &out
			w.Reader = &in
			w.PreserveSeqIndent = true

			nodes, err := w.Read()
			if !assert.NoError(t, err) {
				t.FailNow()
			}

			w.WrappingKind = ""
			err = w.Write(nodes)
			if !assert.NoError(t, err) {
				t.FailNow()
			}

			if tc.err != "" {
				if !assert.EqualError(t, err, tc.err) {
					t.FailNow()
				}
				return
			}

			if !assert.Equal(t,
				strings.TrimSpace(tc.expectedOutput), strings.TrimSpace(out.String())) {
				t.FailNow()
			}
		})
	}
}

func TestByteReadWriter_WrapBareSeqNode(t *testing.T) {
	type testCase struct {
		name            string
		readerErr       string
		writerErr       string
		input           string
		wrapBareSeqNode bool
		expectedOutput  string
		instance        kio.ByteReadWriter
	}

	testCases := []testCase{
		{
			name:            "round_trip bare seq node simple",
			wrapBareSeqNode: true,
			input: `
- foo
- bar
`,
			expectedOutput: `
- foo
- bar
`,
		},
		{
			name:            "round_trip bare seq node",
			wrapBareSeqNode: true,
			input: `# Use the old CRD because of the quantity validation issue:
# https://github.com/kubeflow/kubeflow/issues/5722
- op: replace
  path: /spec
  value:
    group: kubeflow.org
    names:
      kind: Notebook
      plural: notebooks
      singular: notebook
    scope: Namespaced
    subresources:
      status: {}
    versions:
    - name: v1alpha1
      served: true
      storage: false
`,
			expectedOutput: `# Use the old CRD because of the quantity validation issue:
# https://github.com/kubeflow/kubeflow/issues/5722
- op: replace
  path: /spec
  value:
    group: kubeflow.org
    names:
      kind: Notebook
      plural: notebooks
      singular: notebook
    scope: Namespaced
    subresources:
      status: {}
    versions:
    - name: v1alpha1
      served: true
      storage: false
`,
		},
		{
			name:            "error round_trip bare seq node simple",
			wrapBareSeqNode: false,
			input: `
- foo
- bar
`,
			readerErr: "wrong node kind: expected MappingNode but got SequenceNode",
		},
		{
			name:            "error round_trip bare seq node",
			wrapBareSeqNode: false,
			input: `# Use the old CRD because of the quantity validation issue:
# https://github.com/kubeflow/kubeflow/issues/5722
- op: replace
  path: /spec
  value:
    group: kubeflow.org
    names:
      kind: Notebook
      plural: notebooks
      singular: notebook
    scope: Namespaced
    subresources:
      status: {}
    versions:
    - name: v1alpha1
      served: true
      storage: false
`,
			readerErr: "wrong node kind: expected MappingNode but got SequenceNode",
		},
		{
			name:            "round_trip bare seq node json",
			wrapBareSeqNode: true,
			input:           `[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--namespaced"}]`,
			expectedOutput:  `[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--namespaced"}]`,
		},
		{
			name:            "error round_trip invalid yaml node",
			wrapBareSeqNode: false,
			input:           "I am not valid",
			readerErr:       "wrong node kind: expected MappingNode but got ScalarNode",
		},
	}

	for i := range testCases {
		tc := testCases[i]
		t.Run(tc.name, func(t *testing.T) {
			var in, out bytes.Buffer
			in.WriteString(tc.input)
			w := tc.instance
			w.Writer = &out
			w.Reader = &in
			w.PreserveSeqIndent = true
			w.WrapBareSeqNode = tc.wrapBareSeqNode

			nodes, err := w.Read()
			if tc.readerErr != "" {
				if !assert.Error(t, err) {
					t.FailNow()
				}
				if !assert.Contains(t, err.Error(), tc.readerErr) {
					t.FailNow()
				}
				return
			}

			w.WrappingKind = ""
			err = w.Write(nodes)
			if !assert.NoError(t, err) {
				t.FailNow()
			}

			if tc.writerErr != "" {
				if !assert.Error(t, err) {
					t.FailNow()
				}
				if !assert.Contains(t, err.Error(), tc.writerErr) {
					t.FailNow()
				}
				return
			}

			if !assert.Equal(t,
				strings.TrimSpace(tc.expectedOutput), strings.TrimSpace(out.String())) {
				t.FailNow()
			}
		})
	}
}

func TestByteReadWriter_ResourceListWrapping(t *testing.T) {
	singleDeployment := `kind: Deployment
apiVersion: v1
metadata:
  name: tester
  namespace: default
spec:
  replicas: 0`
	resourceList := `apiVersion: config.kubernetes.io/v1
kind: ResourceList
items:
- kind: Deployment
  apiVersion: v1
  metadata:
    name: tester
    namespace: default
  spec:
    replicas: 0`
	resourceListWithError := `apiVersion: config.kubernetes.io/v1
kind: ResourceList
items:
- kind: Deployment
  apiVersion: v1
  metadata:
    name: tester
    namespace: default
  spec:
    replicas: 0
results:
- message: some error
  severity: error`
	resourceListDifferentWrapper := strings.NewReplacer(
		"kind: ResourceList", "kind: SomethingElse",
		"apiVersion: config.kubernetes.io/v1", "apiVersion: fakeVersion",
	).Replace(resourceList)

	testCases := []struct {
		desc           string
		noWrap         bool
		wrapKind       string
		wrapAPIVersion string
		input          string
		want           string
	}{
		{
			desc:  "resource list",
			input: resourceList,
			want:  resourceList,
		},
		{
			desc:  "individual resources",
			input: singleDeployment,
			want:  singleDeployment,
		},
		{
			desc:           "no nested wrapping",
			wrapKind:       kio.ResourceListKind,
			wrapAPIVersion: kio.ResourceListAPIVersion,
			input:          resourceList,
			want:           resourceList,
		},
		{
			desc:   "unwrap resource list",
			noWrap: true,
			input:  resourceList,
			want:   singleDeployment,
		},
		{
			desc:           "wrap individual resources",
			wrapKind:       kio.ResourceListKind,
			wrapAPIVersion: kio.ResourceListAPIVersion,
			input:          singleDeployment,
			want:           resourceList,
		},
		{
			desc:           "NoWrap has precedence",
			noWrap:         true,
			wrapKind:       kio.ResourceListKind,
			wrapAPIVersion: kio.ResourceListAPIVersion,
			input:          singleDeployment,
			want:           singleDeployment,
		},
		{
			desc:           "honor specified wrapping kind",
			wrapKind:       "SomethingElse",
			wrapAPIVersion: "fakeVersion",
			input:          resourceList,
			want:           resourceListDifferentWrapper,
		},
		{
			desc:  "passthrough results",
			input: resourceListWithError,
			want:  resourceListWithError,
		},
	}

	for i := range testCases {
		tc := testCases[i]
		t.Run(tc.desc, func(t *testing.T) {
			var got bytes.Buffer
			rw := kio.ByteReadWriter{
				Reader:             strings.NewReader(tc.input),
				Writer:             &got,
				NoWrap:             tc.noWrap,
				WrappingAPIVersion: tc.wrapAPIVersion,
				WrappingKind:       tc.wrapKind,
			}

			rnodes, err := rw.Read()
			require.NoError(t, err)

			err = rw.Write(rnodes)
			require.NoError(t, err)

			assert.Equal(t, tc.want, strings.TrimSpace(got.String()))
		})
	}
}
