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

package yaml_test

import (
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	yaml "go.yaml.in/yaml/v3"
	"sigs.k8s.io/kustomize/kyaml/errors"
	. "sigs.k8s.io/kustomize/kyaml/yaml"
)

const (
	NodeSampleData = `n: o
a: b
c: d
`
)

func TestResourceNode_SetValue(t *testing.T) {
	instance := *NewScalarRNode("foo")
	instanceCopy := instance
	instance.SetYNode(&yaml.Node{Kind: yaml.ScalarNode, Value: "bar"})
	assert.Equal(t, `bar
`, assertNoErrorString(t)(instanceCopy.String()))
	assert.Equal(t, `bar
`, assertNoErrorString(t)(instance.String()))

	instance = *NewScalarRNode("foo")
	instanceCopy = instance
	instance.SetYNode(nil)
	instance.SetYNode(&yaml.Node{Kind: yaml.ScalarNode, Value: "bar"})
	assert.Equal(t, `foo
`, assertNoErrorString(t)(instanceCopy.String()))
	assert.Equal(t, `bar
`, assertNoErrorString(t)(instance.String()))
}

func TestAppend(t *testing.T) {
	node, err := Parse(NodeSampleData)
	require.NoError(t, err)
	rn, err := node.Pipe(Append(NewScalarRNode("").YNode()))
	if assert.Error(t, err) {
		assert.Contains(t, err.Error(), "wrong node kind")
	}
	assert.Nil(t, rn)

	s := `- a
- b
`
	node, err = Parse(s)
	require.NoError(t, err)
	rn, err = node.Pipe(Append())
	require.NoError(t, err)
	assert.Nil(t, rn)
}

func TestGetElementByIndex(t *testing.T) {
	node, err := Parse(`
- 0
- 1
- 2
`)
	require.NoError(t, err)

	rn, err := node.Pipe(GetElementByIndex(0))
	require.NoError(t, err)
	assert.Equal(t, "0\n", assertNoErrorString(t)(rn.String()))

	rn, err = node.Pipe(GetElementByIndex(2))
	require.NoError(t, err)
	assert.Equal(t, "2\n", assertNoErrorString(t)(rn.String()))

	rn, err = node.Pipe(GetElementByIndex(-1))
	require.NoError(t, err)
	assert.Equal(t, "2\n", assertNoErrorString(t)(rn.String()))
}

func TestGetElementByKey(t *testing.T) {
	node, err := Parse(`
- b: c
- i
- d: e
- f: g
- f: h
`)
	require.NoError(t, err)

	rn, err := node.Pipe(GetElementByKey("b"))
	require.NoError(t, err)
	assert.Equal(t, "b: c\n", assertNoErrorString(t)(rn.String()))

	rn, err = node.Pipe(GetElementByKey("f"))
	require.NoError(t, err)
	assert.Equal(t, "f: g\n", assertNoErrorString(t)(rn.String()))
}

func TestElementSetter(t *testing.T) {
	orig := MustParse(`
- a: b
- scalarValue
- c: d
# null will be removed
- null
`)

	// ElementSetter will update node, so make a copy
	node := orig.Copy()
	// Remove an element, because ElementSetter.Element is left nil.
	rn, err := node.Pipe(ElementSetter{Keys: []string{"a"}, Values: []string{"b"}})
	require.NoError(t, err)
	assert.Nil(t, rn)
	assert.Equal(t, `- scalarValue
- c: d
`, assertNoErrorString(t)(node.String()))

	node = orig.Copy()
	// Nothing happens because no element is matched
	rn, err = node.Pipe(ElementSetter{Keys: []string{"a"}, Values: []string{"zebra"}})
	require.NoError(t, err)
	assert.Nil(t, rn)
	assert.Equal(t, `- a: b
- scalarValue
- c: d
`, assertNoErrorString(t)(node.String()))

	node = orig.Copy()
	// When the element is not found, return an empty ist
	_, err = node.Pipe(ElementSetter{Keys: []string{"a"}})
	require.NoError(t, err)
	assert.Equal(t, `[]
`, assertNoErrorString(t)(node.String()))

	_, err = node.Pipe(ElementSetter{Values: []string{"b"}})
	require.NoError(t, err)
	assert.Equal(t, `[]
`, assertNoErrorString(t)(node.String()))

	node = MustParse(`
- a: b
- c: d
`)
	// If given a key and no values, ElementSetter will
	// change node to be an empty list
	rn, err = node.Pipe(ElementSetter{Keys: []string{"a"}})
	require.NoError(t, err)
	assert.Nil(t, rn)
	assert.Equal(t, `[]
`, assertNoErrorString(t)(node.String()))

	node = MustParse(`
- a: b
- c: d
`)
	// Return error because ElementSetter will assume all elements are scalar when
	// there is only value provided.
	_, err = node.Pipe(ElementSetter{Values: []string{"b"}})
	assert.EqualError(t, err, `wrong node kind: expected ScalarNode but got MappingNode: node contents:
a: b
`)

	node = MustParse(`
- a
- b
`)
	// b is removed since ElementSetter use the value "b" to match the
	// scalar values.
	rn, err = node.Pipe(ElementSetter{Values: []string{"b"}})
	require.NoError(t, err)
	assert.Nil(t, rn)
	assert.Equal(t, `- a
`, assertNoErrorString(t)(node.String()))

	node = orig.Copy()
	// Set an element, replacing 'a: b' with 'e: f'
	newElement := NewMapRNode(&map[string]string{
		"e": "f",
	})
	rn, err = node.Pipe(ElementSetter{
		Keys:    []string{"a"},
		Values:  []string{"b"},
		Element: newElement.YNode(),
	})
	require.NoError(t, err)
	assert.Equal(t, rn, newElement)
	assert.Equal(t, `- e: f
- scalarValue
- c: d
`, assertNoErrorString(t)(node.String()))

	node = orig.Copy()
	// Set an element with scalar, {"a": "b"} to "foo"
	newElement = NewScalarRNode("foo")
	rn, err = node.Pipe(ElementSetter{
		Keys:    []string{"a"},
		Values:  []string{"b"},
		Element: newElement.YNode(),
	})
	require.NoError(t, err)
	assert.Equal(t, rn, newElement)
	assert.Equal(t, `- foo
- scalarValue
- c: d
`, assertNoErrorString(t)(node.String()))

	node = orig.Copy()
	// Append an element, {"x": "y"} is not in the list
	// so the element will be appended.
	newElement = NewMapRNode(&map[string]string{
		"e": "f",
	})
	rn, err = node.Pipe(ElementSetter{
		Keys:    []string{"x"},
		Values:  []string{"y"},
		Element: newElement.YNode(),
	})
	require.NoError(t, err)
	assert.Equal(t, rn, newElement)
	assert.Equal(t, `- a: b
- scalarValue
- c: d
- e: f
`, assertNoErrorString(t)(node.String()))
}

func TestElementSetterMultipleKeys(t *testing.T) {
	orig := MustParse(`
- a: b
  c: d
- scalarValue
- e: f
# null will be removed
- null
`)

	// ElementSetter will update node, so make a copy
	node := orig.Copy()
	// Remove an element using one key-value pair,
	// because ElementSetter.Element is left nil.
	rn, err := node.Pipe(ElementSetter{
		Keys:   []string{"a"},
		Values: []string{"b"},
	})
	require.NoError(t, err)
	assert.Nil(t, rn)
	assert.Equal(t, `- scalarValue
- e: f
`, assertNoErrorString(t)(node.String()))

	node = orig.Copy()
	// Remove an element using multiple key-value pairs,
	// because ElementSetter.Element is left nil.
	rn, err = node.Pipe(ElementSetter{
		Keys:   []string{"a", "c"},
		Values: []string{"b", "d"},
	})
	require.NoError(t, err)
	assert.Nil(t, rn)
	assert.Equal(t, `- scalarValue
- e: f
`, assertNoErrorString(t)(node.String()))

	node = orig.Copy()
	// Should do nothing, because Element is nil
	// and there is no element which matches all
	// give key-value pairs
	rn, err = node.Pipe(ElementSetter{
		Keys:   []string{"a", "c"},
		Values: []string{"b", "wrong value"},
	})
	require.NoError(t, err)
	assert.Nil(t, rn)
	assert.Equal(t, `- a: b
  c: d
- scalarValue
- e: f
`, assertNoErrorString(t)(node.String()))

	node = orig.Copy()
	// Set an element, with a single key-value pair
	// replacing 'a: b, c: d' with 'g: h'
	newElement := NewMapRNode(&map[string]string{
		"g": "h",
	})
	rn, err = node.Pipe(ElementSetter{
		Keys:    []string{"a"},
		Values:  []string{"b"},
		Element: newElement.YNode(),
	})
	require.NoError(t, err)
	assert.Equal(t, rn, newElement)
	assert.Equal(t, `- g: h
- scalarValue
- e: f
`, assertNoErrorString(t)(node.String()))

	node = orig.Copy()
	// Set an element, with multiple key-value pairs
	// replacing 'a: b, c: d' with 'g: h'
	newElement = NewMapRNode(&map[string]string{
		"g": "h",
	})
	rn, err = node.Pipe(ElementSetter{
		Keys:    []string{"a", "c"},
		Values:  []string{"b", "d"},
		Element: newElement.YNode(),
	})
	require.NoError(t, err)
	assert.Equal(t, rn, newElement)
	assert.Equal(t, `- g: h
- scalarValue
- e: f
`, assertNoErrorString(t)(node.String()))

	node = orig.Copy()
	// Set an element scalar,
	// {'a: b, c: d'} to "foo"
	newElement = NewScalarRNode("foo")
	rn, err = node.Pipe(ElementSetter{
		Keys:    []string{"a", "c"},
		Values:  []string{"b", "d"},
		Element: newElement.YNode(),
	})
	require.NoError(t, err)
	assert.Equal(t, rn, newElement)
	assert.Equal(t, `- foo
- scalarValue
- e: f
`, assertNoErrorString(t)(node.String()))

	node = orig.Copy()
	// Append an element
	// There is no element which matches all given
	// key-value pairs, so the element will be appended.
	newElement = NewMapRNode(&map[string]string{
		"g": "h",
	})
	rn, err = node.Pipe(ElementSetter{
		Keys:    []string{"a", "c"},
		Values:  []string{"b", "wrong value"},
		Element: newElement.YNode(),
	})
	require.NoError(t, err)
	assert.Equal(t, rn, newElement)
	assert.Equal(t, `- a: b
  c: d
- scalarValue
- e: f
- g: h
`, assertNoErrorString(t)(node.String()))
}

func TestElementMatcherWithNoValue(t *testing.T) {
	node, err := Parse(`
- a: c
- b: ""
`)
	require.NoError(t, err)

	rn, err := node.Pipe(ElementMatcher{Keys: []string{"b"}})
	require.NoError(t, err)
	assert.Equal(t, "b: \"\"\n", assertNoErrorString(t)(rn.String()))

	rn, err = node.Pipe(ElementMatcher{Keys: []string{"a"}})
	require.NoError(t, err)
	assert.Nil(t, rn)

	rn, err = node.Pipe(ElementMatcher{Keys: []string{"a"}, MatchAnyValue: true})
	require.NoError(t, err)
	assert.Equal(t, "a: c\n", assertNoErrorString(t)(rn.String()))

	_, err = node.Pipe(ElementMatcher{Keys: []string{"a"}, Values: []string{"c"}, MatchAnyValue: true})
	require.Errorf(t, err, "Values must be empty when MatchAnyValue is set to true")
}

func TestElementMatcherMultipleKeys(t *testing.T) {
	node, err := Parse(`
- a: b
  c: d
- e: f
`)
	require.NoError(t, err)

	// matches all key-value pairs
	rn, err := node.Pipe(MatchElementList(
		[]string{"a", "c"}, // keys
		[]string{"b", "d"}, // values
	))
	require.NoError(t, err)
	assert.NotEmpty(t, rn)

	// matches one key value pair but not the other
	rn, err = node.Pipe(MatchElementList(
		[]string{"a", "c"}, // keys
		[]string{"b", "f"}, // values
	))
	require.NoError(t, err)
	assert.Nil(t, rn)

	// matches single given key value pair
	rn, err = node.Pipe(MatchElementList(
		[]string{"e"}, // keys
		[]string{"f"}, // values
	))
	require.NoError(t, err)
	assert.NotEmpty(t, rn)

	// matching key, but value doesn't match
	rn, err = node.Pipe(MatchElementList(
		[]string{"e"}, // keys
		[]string{"g"}, // values
	))
	require.NoError(t, err)
	assert.Nil(t, rn)
}

func TestClearField_Fn(t *testing.T) {
	node, err := Parse(NodeSampleData)
	require.NoError(t, err)
	rn, err := node.Pipe(FieldClearer{Name: "a"})
	require.NoError(t, err)
	assert.Equal(t, "n: o\nc: d\n", assertNoErrorString(t)(node.String()))
	assert.Equal(t, "b\n", assertNoErrorString(t)(rn.String()))

	node, err = Parse(NodeSampleData)
	require.NoError(t, err)
	rn, err = node.Pipe(FieldClearer{Name: "n"})
	require.NoError(t, err)
	assert.Equal(t, "a: b\nc: d\n", assertNoErrorString(t)(node.String()))
	assert.Equal(t, "o\n", assertNoErrorString(t)(rn.String()))

	node, err = Parse(NodeSampleData)
	require.NoError(t, err)
	rn, err = node.Pipe(FieldClearer{Name: "c"})
	require.NoError(t, err)
	assert.Equal(t, "n: o\na: b\n", assertNoErrorString(t)(node.String()))
	assert.Equal(t, "d\n", assertNoErrorString(t)(rn.String()))

	s := `n: o
a: b
`
	node, err = Parse(s)
	require.NoError(t, err)
	rn, err = node.Pipe(FieldClearer{Name: "o"})
	require.NoError(t, err)
	assert.Nil(t, rn)
	assert.Equal(t, "n: o\na: b\n", assertNoErrorString(t)(node.String()))

	s = `- a
- b
`
	node, err = Parse(s)
	require.NoError(t, err)
	rn, err = node.Pipe(FieldClearer{Name: "a"})
	if assert.Error(t, err) {
		assert.Contains(t, err.Error(), "wrong node kind")
	}
	assert.Nil(t, rn)
	assert.Equal(t, s, assertNoErrorString(t)(node.String()))

	// should not clear n because it is not empty
	s = `n:
  k: v
a: b
c: d
`
	node, err = Parse(s)
	require.NoError(t, err)
	rn, err = node.Pipe(FieldClearer{Name: "n", IfEmpty: true})
	require.NoError(t, err)
	assert.Equal(t, "n:\n  k: v\na: b\nc: d\n", assertNoErrorString(t)(node.String()))
	assert.Equal(t, "", assertNoErrorString(t)(rn.String()))

	// should clear n because it is empty
	s = `n: {}
a: b
c: d
`
	node, err = Parse(s)
	require.NoError(t, err)
	rn, err = node.Pipe(FieldClearer{Name: "n", IfEmpty: true})
	require.NoError(t, err)
	assert.Equal(t, "a: b\nc: d\n", assertNoErrorString(t)(node.String()))
	assert.Equal(t, "{}\n", assertNoErrorString(t)(rn.String()))
}

var s = `n: o
a:
  l: m
  b:
  - f: g
  - c: e
  - h: i
r: s
`

func TestLookup_Fn_create(t *testing.T) {
	// primitive
	node, err := Parse(s)
	require.NoError(t, err)
	rn, err := node.Pipe(PathGetter{
		Path:   []string{"a", "b", "[c=d]", "t", "f", "[=h]"},
		Create: yaml.ScalarNode,
	})
	require.NoError(t, err)
	assert.Equal(t, `n: o
a:
  l: m
  b:
  - f: g
  - c: e
  - h: i
  - c: d
    t:
      f:
      - h
r: s
`, assertNoErrorString(t)(node.String()))
	assert.Equal(t, `h
`, assertNoErrorString(t)(rn.String()))
}

func TestLookup_Fn_create2(t *testing.T) {
	node, err := Parse(s)
	require.NoError(t, err)
	rn, err := node.Pipe(PathGetter{
		Path:   []string{"a", "b", "[c=d]", "t", "f"},
		Create: yaml.SequenceNode,
	})
	require.NoError(t, err)
	assert.Equal(t, `n: o
a:
  l: m
  b:
  - f: g
  - c: e
  - h: i
  - c: d
    t:
      f: []
r: s
`, assertNoErrorString(t)(node.String()))
	assert.Equal(t, `[]
`, assertNoErrorString(t)(rn.String()))
}

func TestLookup_Fn_create3(t *testing.T) {
	node, err := Parse(s)
	require.NoError(t, err)
	rn, err := node.Pipe(LookupCreate(yaml.MappingNode, "a", "b", "[c=d]", "t"))
	require.NoError(t, err)
	assert.Equal(t, `n: o
a:
  l: m
  b:
  - f: g
  - c: e
  - h: i
  - c: d
    t: {}
r: s
`, assertNoErrorString(t)(node.String()))
	assert.Equal(t, `{}
`, assertNoErrorString(t)(rn.String()))
}

func TestLookupCreate_4(t *testing.T) {
	node, err := Parse(`
a: {}
`)
	require.NoError(t, err)
	rn, err := node.Pipe(
		LookupCreate(yaml.MappingNode, "a", "b", "[c=d]", "t", "f", "[=h]"))

	node.YNode().Style = yaml.FlowStyle
	require.NoError(t, err)
	assert.Equal(t, "{a: {b: [{c: d, t: {f: [h]}}]}}\n", assertNoErrorString(t)(node.String()))
	assert.Equal(t, "h\n", assertNoErrorString(t)(rn.String()))
}

func TestLookup_Fn_create_with_wildcard_error(t *testing.T) {
	node, err := Parse(s)
	require.NoError(t, err)
	_, err = node.Pipe(LookupCreate(yaml.MappingNode, "a", "b", "*", "t"))
	require.Error(t, err, "wildcard is not supported in PathGetter")
}

func TestLookup(t *testing.T) {
	s := `n: o
a:
  l: m
  b:
  - f: g
  - c: e
  - c: d
    t:
      u: v
      f:
      - g
      - h
      - i
    j: k
  - h: i
    p: q
r: s
`
	node, err := Parse(s)
	require.NoError(t, err)

	// primitive
	rn, err := node.Pipe(Lookup("a", "b", "[c=d]", "t", "f", "[=h]"))
	require.NoError(t, err)
	assert.Equal(t, s, assertNoErrorString(t)(node.String()))
	assert.Equal(t, `h
`, assertNoErrorString(t)(rn.String()))

	// seq
	rn, err = node.Pipe(Lookup("a", "b", "[c=d]", "t", "f"))
	require.NoError(t, err)
	assert.Equal(t, s, assertNoErrorString(t)(node.String()))
	assert.Equal(t, `- g
- h
- i
`, assertNoErrorString(t)(rn.String()))

	rn, err = node.Pipe(Lookup("a", "b", "[c=d]", "t"))
	require.NoError(t, err)
	assert.Equal(t, s, assertNoErrorString(t)(node.String()))
	assert.Equal(t, `u: v
f:
- g
- h
- i
`, assertNoErrorString(t)(rn.String()))

	rn, err = node.Pipe(Lookup("a", "b", "[c=d]"))
	require.NoError(t, err)
	assert.Equal(t, s, assertNoErrorString(t)(node.String()))
	assert.Equal(t, `c: d
t:
  u: v
  f:
  - g
  - h
  - i
j: k
`, assertNoErrorString(t)(rn.String()))

	rn, err = node.Pipe(Lookup("a", "b"))
	require.NoError(t, err)
	assert.Equal(t, s, assertNoErrorString(t)(node.String()))
	assert.Equal(t, `- f: g
- c: e
- c: d
  t:
    u: v
    f:
    - g
    - h
    - i
  j: k
- h: i
  p: q
`, assertNoErrorString(t)(rn.String()))

	rn, err = node.Pipe(Lookup("a", "b", "0"))
	require.NoError(t, err)
	assert.Equal(t, s, assertNoErrorString(t)(node.String()))
	assert.Equal(t, `f: g
`, assertNoErrorString(t)(rn.String()))

	rn, err = node.Pipe(Lookup("a", "b", "-", "h"))
	require.NoError(t, err)
	assert.Equal(t, s, assertNoErrorString(t)(node.String()))
	assert.Equal(t, `i
`, assertNoErrorString(t)(rn.String()))

	rn, err = node.Pipe(Lookup("l"))
	require.NoError(t, err)
	assert.Equal(t, s, assertNoErrorString(t)(node.String()))
	assert.Nil(t, rn)

	rn, err = node.Pipe(Lookup("zzz"))
	require.NoError(t, err)
	assert.Equal(t, s, assertNoErrorString(t)(node.String()))
	assert.Nil(t, rn)

	rn, err = node.Pipe(Lookup("[a=b]"))
	require.Error(t, err)
	assert.Equal(t, s, assertNoErrorString(t)(node.String()))
	assert.Nil(t, rn)

	rn, err = node.Pipe(Lookup("a", "b", "f"))
	require.Error(t, err)
	assert.Equal(t, s, assertNoErrorString(t)(node.String()))
	assert.Nil(t, rn)

	rn, err = node.Pipe(Lookup("a", "b", "c=zzz"))
	require.Error(t, err)
	assert.Equal(t, s, assertNoErrorString(t)(node.String()))
	assert.Nil(t, rn)

	rn, err = node.Pipe(Lookup(" ", "a", "", "b", " ", "[c=d]", "\n", "t", "\t", "f", "  ", "[=h]", "  "))
	require.NoError(t, err)
	assert.Equal(t, s, assertNoErrorString(t)(node.String()))
	assert.Equal(t, `h
`, assertNoErrorString(t)(rn.String()))

	rn, err = node.Pipe(Lookup(" ", "a", "", "b", " ", "[]"))
	require.Error(t, err)
	assert.Nil(t, rn)

	rn, err = node.Pipe(Lookup("a", "b", "[c=d]", "t", "f", "[=c]"))
	require.NoError(t, err)
	assert.Equal(t, s, assertNoErrorString(t)(node.String()))
	assert.Nil(t, rn)

	rn, err = node.Pipe(Lookup("a", "b", "[z=z]"))
	require.NoError(t, err)
	assert.Equal(t, s, assertNoErrorString(t)(node.String()))
	assert.Nil(t, rn)

	rn, err = node.Pipe(Lookup("a", "b", "-1"))
	require.Errorf(t, err, "array index -1 cannot be negative")
	assert.Equal(t, s, assertNoErrorString(t)(node.String()))
	assert.Nil(t, rn)

	rn, err = node.Pipe(Lookup("a", "b", "99"))
	require.NoError(t, err)
	assert.Equal(t, s, assertNoErrorString(t)(node.String()))
	assert.Nil(t, rn)
}

func TestLookupFirstMatch(t *testing.T) {
	tests := []struct {
		name     string
		paths    [][]string
		wantPath []string
	}{
		{
			name:     "finds path that exists",
			paths:    [][]string{{"spec", "jobTemplate", "spec", "template", "spec", "containers"}},
			wantPath: []string{"spec", "jobTemplate", "spec", "template", "spec", "containers"},
		},
		{
			name:     "chooses first path when multiple exist: containers example",
			paths:    ConventionalContainerPaths,
			wantPath: []string{"spec", "template", "spec", "containers"},
		},
		{
			name: "chooses first path when multiple exist: annotations example",
			paths: [][]string{
				{"metadata", "annotations", "example.kustomize.io/new"},
				{"metadata", "annotations", "example.kustomize.io/deprecated"},
			},
			wantPath: []string{"metadata", "annotations", "example.kustomize.io/new"},
		},
		{
			name: "returns nil when path does not exist",
			paths: [][]string{
				{"metadata", "annotations", "example.kustomize.io/does-not-exist"},
				{"metadata", "annotations", "example.kustomize.io/also-not-exist"},
			},
			wantPath: nil,
		},
	}
	for _, tt := range tests {
		s := `
apiVersion: example.kustomize.io/v1
kind: Custom
metadata:
  annotations:
    example.kustomize.io/deprecated: foo
    example.kustomize.io/new: foo
spec:
  template:
    spec:
      containers:
      - name: foo
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: foo
`
		resource := MustParse(s)
		t.Run(tt.name, func(t *testing.T) {
			result, err := LookupFirstMatch(tt.paths).Filter(resource)
			require.NoError(t, err)
			if tt.wantPath != nil {
				assert.Equal(t, tt.wantPath, result.FieldPath())
			} else {
				assert.Nil(t, result)
			}
		})
	}
}

func TestMapEntrySetter(t *testing.T) {
	withStyle := func(rn *RNode, style yaml.Style) *RNode {
		rn.YNode().Style = style
		return rn
	}
	testCases := []struct {
		desc        string
		input       string
		setter      MapEntrySetter
		expected    string
		expectedErr error
	}{
		{
			desc:  "it should override Key's value map entry",
			input: "foo: baz\n",
			setter: MapEntrySetter{
				Key:   NewScalarRNode("foo"),
				Value: NewScalarRNode("bar"),
			},
			expected: "foo: bar\n",
		},
		{
			desc:  "it should override Name's map entry",
			input: "foo: baz\n",
			setter: MapEntrySetter{
				Name:  "foo",
				Key:   NewScalarRNode("bar"),
				Value: NewScalarRNode("baz"),
			},
			expected: "bar: baz\n",
		},
		{
			desc:  "it should insert new map entry",
			input: "foo: baz\n",
			setter: MapEntrySetter{
				Key:   NewScalarRNode("bar"),
				Value: NewScalarRNode("42"),
			},
			expected: "foo: baz\nbar: 42\n",
		},
		{
			desc:  "it should override the style",
			input: "foo: baz\n",
			setter: MapEntrySetter{
				Key:   withStyle(NewScalarRNode("foo"), yaml.DoubleQuotedStyle),
				Value: withStyle(NewScalarRNode("bar"), yaml.SingleQuotedStyle),
			},
			expected: `"foo": 'bar'` + "\n",
		},
		{
			desc:  "it should return error on sequence nodes",
			input: "- foo: baz\n",
			setter: MapEntrySetter{
				Key:   NewScalarRNode("foo"),
				Value: NewScalarRNode("bar"),
			},
			expectedErr: errors.Errorf(`wrong node kind: expected MappingNode but got SequenceNode: node contents:
- foo: baz
`),
		},
	}
	for _, tc := range testCases {
		t.Run(tc.desc, func(t *testing.T) {
			node, err := Parse(tc.input)
			require.NoError(t, err)
			k, err := tc.setter.Filter(node)
			if tc.expectedErr == nil {
				require.NoError(t, err)
				assert.Equal(t, tc.expected, assertNoErrorString(t)(node.String()))
				assert.Equal(t, tc.expected, assertNoErrorString(t)(k.String()))
			} else {
				assert.NotNil(t, err)
				assert.Equal(t, tc.expectedErr.Error(), err.Error())
			}
		})
	}
}

func TestFieldSetter(t *testing.T) {
	// Change field
	node, err := Parse(`
foo: baz
`)
	require.NoError(t, err)
	instance := FieldSetter{
		Name:  "foo",
		Value: NewScalarRNode("bar"),
	}
	k, err := instance.Filter(node)
	require.NoError(t, err)
	assert.Equal(t, `foo: bar
`, assertNoErrorString(t)(node.String()))
	assert.Equal(t, `bar
`, assertNoErrorString(t)(k.String()))

	// Add field
	node, err = Parse(`
foo: baz
`)
	require.NoError(t, err)
	instance = FieldSetter{
		Name:  "bar",
		Value: NewScalarRNode("buz"),
	}
	k, err = instance.Filter(node)
	require.NoError(t, err)
	assert.Equal(t, `foo: baz
bar: buz
`, assertNoErrorString(t)(node.String()))
	assert.Equal(t, `buz
`, assertNoErrorString(t)(k.String()))

	// Clear field
	node, err = Parse(`
foo: baz
bar: buz
`)
	require.NoError(t, err)
	instance = FieldSetter{
		Name: "foo",
	}
	k, err = instance.Filter(node)
	require.NoError(t, err)
	assert.Equal(t, `bar: buz
`, assertNoErrorString(t)(node.String()))
	assert.Equal(t, `baz
`, assertNoErrorString(t)(k.String()))

	// Empty value
	node, err = Parse(`
foo
`)
	require.NoError(t, err)
	instance = FieldSetter{}
	k, err = instance.Filter(node)
	require.NoError(t, err)
	assert.Equal(t, `foo
`, assertNoErrorString(t)(node.String()))
	assert.Equal(t, `foo
`, assertNoErrorString(t)(k.String()))

	// Encounter error
	node, err = Parse(`
-a
-b
`)
	require.NoError(t, err)
	instance = FieldSetter{
		Name:  "foo",
		Value: NewScalarRNode("v"),
	}
	k, err = instance.Filter(node)
	if assert.Error(t, err) {
		assert.Contains(t, err.Error(), "wrong node kind")
	}
	assert.Nil(t, k)
}

func TestFieldSetterNumberInKeyRegression(t *testing.T) {
	node := NewMapRNode(&map[string]string{"river": "mississippi"})

	k, err := FieldSetter{
		Name:  "forty 2",
		Value: NewScalarRNode("number key one"),
	}.Filter(node)
	require.NoError(t, err)
	assert.Equal(t, `number key one
`, assertNoErrorString(t)(k.String()))

	k, err = FieldSetter{
		Name:  "fortytwo",
		Value: NewScalarRNode("number key two"),
	}.Filter(node)
	require.NoError(t, err)
	assert.Equal(t, `number key two
`, assertNoErrorString(t)(k.String()))

	k, err = FieldSetter{
		Name:  "42",
		Value: NewScalarRNode("number key three"),
	}.Filter(node)
	require.NoError(t, err)
	assert.Equal(t, `number key three
`, assertNoErrorString(t)(k.String()))

	assert.Equal(t, `river: mississippi
forty 2: number key one
fortytwo: number key two
42: number key three
`, assertNoErrorString(t)(node.String()))
}

func TestSet_Fn(t *testing.T) {
	node, err := Parse(`
foo: baz
`)
	require.NoError(t, err)
	k, err := node.Pipe(Get("foo"), Set(NewScalarRNode("bar")))
	require.NoError(t, err)
	assert.Equal(t, `foo: bar
`, assertNoErrorString(t)(node.String()))
	assert.Equal(t, `bar
`, assertNoErrorString(t)(k.String()))

	node, err = Parse(`
foo: baz
`)
	require.NoError(t, err)
	_, err = node.Pipe(Set(NewScalarRNode("bar")))
	if !assert.Error(t, err) {
		return
	}
	assert.Contains(t, err.Error(), "wrong node kind")
	assert.Equal(t, `foo: baz
`, assertNoErrorString(t)(node.String()))
}

func TestErrorIfInvalid(t *testing.T) {
	err := ErrorIfInvalid(
		NewRNode(&yaml.Node{Kind: yaml.SequenceNode}), yaml.SequenceNode)
	require.NoError(t, err)

	// nil values should pass validation -- they were not specified
	err = ErrorIfInvalid(&RNode{}, yaml.SequenceNode)
	if !assert.NoError(t, err) {
		t.FailNow()
	}

	err = ErrorIfInvalid(NewRNode(&Node{Content: []*yaml.Node{{Value: "hello"}}}), yaml.SequenceNode)
	if !assert.Error(t, err) {
		t.FailNow()
	}
	assert.Contains(t, err.Error(), "wrong node kind")

	err = ErrorIfInvalid(NewRNode(&yaml.Node{}), yaml.SequenceNode)
	if assert.Error(t, err) {
		assert.Contains(t, err.Error(), "wrong node kind")
	}
	err = ErrorIfInvalid(NewRNode(&yaml.Node{}), yaml.MappingNode)
	if assert.Error(t, err) {
		assert.Contains(t, err.Error(), "wrong node kind")
	}

	err = ErrorIfInvalid(NewRNode(&yaml.Node{
		Kind:    yaml.MappingNode,
		Content: []*yaml.Node{{}, {}},
	}), yaml.MappingNode)
	require.NoError(t, err)

	err = ErrorIfInvalid(NewRNode(&yaml.Node{}), yaml.SequenceNode)
	if assert.Error(t, err) {
		assert.Contains(t, err.Error(), "wrong node kind")
	}

	err = ErrorIfInvalid(NewRNode(&yaml.Node{
		Kind:    yaml.MappingNode,
		Content: []*yaml.Node{{Kind: yaml.ScalarNode, Tag: NodeTagString, Value: "orphan"}},
	}), yaml.MappingNode)
	if assert.Error(t, err) {
		assert.Contains(t, err.Error(), "even length")
		assert.Contains(t, err.Error(), "content length 1")
		assert.Contains(t, err.Error(), `0:{kind:ScalarNode tag:"!!str" value:"orphan"}`)
	}
}

func TestSplitIndexNameValue(t *testing.T) {
	k, v, err := SplitIndexNameValue("")
	if assert.Error(t, err) {
		assert.Contains(t, err.Error(), "fieldName=fieldValue")
	}
	assert.Equal(t, "", k)
	assert.Equal(t, "", v)

	k, v, err = SplitIndexNameValue("a=b")
	require.NoError(t, err)
	assert.Equal(t, "a", k)
	assert.Equal(t, "b", v)

	k, v, err = SplitIndexNameValue("=b")
	require.NoError(t, err)
	assert.Equal(t, "", k)
	assert.Equal(t, "b", v)

	k, v, err = SplitIndexNameValue("a=b=c")
	require.NoError(t, err)
	assert.Equal(t, "a", k)
	assert.Equal(t, "b=c", v)

	k, v, err = SplitIndexNameValue("=-jar")
	require.NoError(t, err)
	assert.Equal(t, "", k)
	assert.Equal(t, "-jar", v)
}

type filter struct {
	fn func(object *RNode) (*RNode, error)
}

func (c filter) Filter(object *RNode) (*RNode, error) {
	return c.fn(object)
}

func TestResourceNode_Pipe(t *testing.T) {
	var r0, r1, r2, r3 *RNode
	var called []string

	// check the nil value case
	_, err := r0.Pipe(FieldMatcher{Name: "foo"})
	require.NoError(t, err)

	r0, r1, r2, r3 = &RNode{}, &RNode{}, &RNode{}, &RNode{}
	// all filters successful
	v, err := r0.Pipe(
		filter{fn: func(object *RNode) (*RNode, error) {
			assert.True(t, r0 == object)
			called = append(called, "a")
			return r1, nil
		}},
		filter{fn: func(object *RNode) (*RNode, error) {
			assert.True(t, object == r1, "function arg doesn't match last function output")
			called = append(called, "b")
			return r2, nil
		}},
		filter{fn: func(object *RNode) (*RNode, error) {
			assert.True(t, object == r2, "function arg doesn't match last function output")
			return r3, nil
		}},
	)
	assert.True(t, v == r3, "expected r3")
	assert.Nil(t, err)
	assert.Equal(t, called, []string{"a", "b"})

	// filter returns nil
	called = []string{}
	v, err = r0.Pipe(
		filter{fn: func(object *RNode) (*RNode, error) {
			assert.True(t, r0 == object)
			called = append(called, "a")
			return r1, nil
		}},
		filter{fn: func(object *RNode) (*RNode, error) {
			assert.True(t, object == r1, "function arg doesn't match last function output")
			called = append(called, "b")
			return nil, nil
		}},
		filter{fn: func(object *RNode) (*RNode, error) {
			assert.Fail(t, "function should be run after error")
			return nil, nil
		}},
	)
	assert.Nil(t, v)
	assert.Nil(t, err)
	assert.Equal(t, called, []string{"a", "b"})

	// filter returns an error
	called = []string{}
	v, err = r0.Pipe(
		filter{fn: func(object *RNode) (*RNode, error) {
			assert.True(t, r0 == object)
			called = append(called, "a")
			return r1, nil
		}},
		filter{fn: func(object *RNode) (*RNode, error) {
			assert.True(t, object == r1, "function arg doesn't match last function output")
			called = append(called, "b")
			return r1, fmt.Errorf("expected-error")
		}},
		filter{fn: func(object *RNode) (*RNode, error) {
			assert.Fail(t, "function should be run after error")
			return nil, nil
		}},
	)
	assert.True(t, v == r1, "expected r1 as value")
	assert.EqualError(t, err, "expected-error")
	assert.Equal(t, called, []string{"a", "b"})
}

func TestClearAnnotation(t *testing.T) {
	// create metadata.annotations field
	r0 := assertNoError(t)(Parse(`apiVersion: apps/v1
kind: Deployment
metadata:
 annotations:
   z: y
   a.b.c: d.e.f
   s: t
`))

	rn := assertNoError(t)(r0.Pipe(ClearAnnotation("a.b.c")))
	assert.Equal(t, "d.e.f\n", assertNoErrorString(t)(rn.String()))
	assert.Equal(t, `apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    z: y
    s: t
`, assertNoErrorString(t)(r0.String()))
}

func TestGetAnnotation(t *testing.T) {
	r0 := assertNoError(t)(Parse(`apiVersion: apps/v1
kind: Deployment
metadata:
 labels:
   app: java
 annotations:
   a.b.c: d.e.f
   g: h
   i: j
   k: l
 name: app`))

	rn := assertNoError(t)(
		r0.Pipe(GetAnnotation("a.b.c")))
	assert.Equal(t, "d.e.f\n", assertNoErrorString(t)(rn.String()))
}

func TestSetAnnotation_Fn(t *testing.T) {
	// create metadata.annotations field
	r0 := assertNoError(t)(Parse(`apiVersion: apps/v1
kind: Deployment`))

	rn := assertNoError(t)(r0.Pipe(SetAnnotation("a.b.c", "d.e.f")))
	assert.Equal(t, "'d.e.f'\n", assertNoErrorString(t)(rn.String()))
	assert.Equal(t, `apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    a.b.c: 'd.e.f'
`, assertNoErrorString(t)(r0.String()))
}

func TestUpdateAnnotation_Fn(t *testing.T) {
	// create metadata.annotations field
	r0 := assertNoError(t)(Parse(`apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    a.b.c: "h.i.j"
`))

	rn := assertNoError(t)(r0.Pipe(SetAnnotation("a.b.c", "d.e.f")))
	assert.Equal(t, "\"d.e.f\"\n", assertNoErrorString(t)(rn.String()))
	assert.Equal(t, `apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    a.b.c: "d.e.f"
`, assertNoErrorString(t)(r0.String()))
}

func TestRNode_GetMeta(t *testing.T) {
	s := `apiVersion: v1/apps
kind: Deployment
metadata:
  name: foo
  namespace: bar
  labels:
    kl: vl
  annotations:
    ka: va
`
	node, err := Parse(s)
	if !assert.NoError(t, err) {
		return
	}
	meta, err := node.GetMeta()
	if !assert.NoError(t, err) {
		return
	}
	assert.Equal(t, ResourceMeta{
		TypeMeta: TypeMeta{
			Kind:       "Deployment",
			APIVersion: "v1/apps",
		},
		ObjectMeta: ObjectMeta{
			NameMeta: NameMeta{
				Name:      "foo",
				Namespace: "bar",
			},
			Annotations: map[string]string{
				"ka": "va",
			},
			Labels: map[string]string{
				"kl": "vl",
			},
		},
	}, meta)
}

func assertNoError(t *testing.T) func(o *RNode, err error) *RNode {
	t.Helper()
	return func(o *RNode, err error) *RNode {
		require.NoError(t, err)
		return o
	}
}

func assertNoErrorString(t *testing.T) func(string, error) string {
	t.Helper()
	return func(s string, err error) string {
		require.NoError(t, err)
		return s
	}
}
