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

package framework_test

import (
	"bytes"
	"strings"
	"testing"

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

	"sigs.k8s.io/kustomize/kyaml/fn/framework"
	"sigs.k8s.io/kustomize/kyaml/kio"
)

func TestSelector(t *testing.T) {
	type Test struct {
		// Name is the name of the test
		Name string

		// Filter configures the selector
		Fn func(*framework.Selector)

		// ValueFoo is the value to substitute to select the foo resource
		ValueFoo string

		// ValueBar is the value to substitute to select the bar resource
		ValueBar string

		// Value is set by the test to either ValueFoo or ValueBar
		// and substituted into the selector
		Value string
	}
	tests := []Test{
		// Test the name template
		{
			Name: "names",
			Fn: func(s *framework.Selector) {
				s.Names = []string{"{{ .Value }}"}
			},
			ValueFoo: "foo",
			ValueBar: "bar",
		},

		// Test the kind template
		{
			Name: "kinds",
			Fn: func(s *framework.Selector) {
				s.Kinds = []string{"{{ .Value }}"}
			},
			ValueFoo: "StatefulSet",
			ValueBar: "Deployment",
		},

		// Test the apiVersion template
		{
			Name: "apiVersion",
			Fn: func(s *framework.Selector) {
				s.APIVersions = []string{"{{ .Value }}"}
			},
			ValueFoo: "apps/v1beta1",
			ValueBar: "apps/v1",
		},

		// Test the namespace template
		{
			Name: "namespaces",
			Fn: func(s *framework.Selector) {
				s.Namespaces = []string{"{{ .Value }}"}
			},
			ValueFoo: "foo-default",
			ValueBar: "bar-default",
		},

		// Test the annotations template
		{
			Name: "annotations",
			Fn: func(s *framework.Selector) {
				s.Annotations = map[string]string{"key": "{{ .Value }}"}
			},
			ValueFoo: "foo-a",
			ValueBar: "bar-a",
		},

		// Test the labels template
		{
			Name: "labels",
			Fn: func(s *framework.Selector) {
				s.Labels = map[string]string{"key": "{{ .Value }}"}
			},
			ValueFoo: "foo-l",
			ValueBar: "bar-l",
		},
	}

	// input is the input resources that are selected
	input := `
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
 name: foo
 namespace: foo-default
 annotations:
   key: foo-a
 labels:
   key: foo-l
---
apiVersion: apps/v1
kind: Deployment
metadata:
 name: bar
 namespace: bar-default
 annotations:
   key: bar-a
 labels:
   key: bar-l
`
	// expectedFoo is the expected output when the FooValue is substituted
	expectedFoo := `
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: foo
  namespace: foo-default
  annotations:
    key: foo-a
    config.kubernetes.io/index: '0'
  labels:
    key: foo-l
`
	// expectedFoo is the expected output when the BarValue is substituted
	expectedBar := `
apiVersion: apps/v1
kind: Deployment
metadata:
  name: bar
  namespace: bar-default
  annotations:
    key: bar-a
    config.kubernetes.io/index: '1'
  labels:
    key: bar-l
`

	// Run the tests by substituting the FooValues
	var err error
	for i := range tests {
		test := tests[i]
		t.Run(tests[i].Name+"-foo", func(t *testing.T) {
			test.Value = test.ValueFoo
			var out bytes.Buffer
			rw := &kio.ByteReadWriter{
				Reader:                bytes.NewBufferString(input),
				Writer:                &out,
				KeepReaderAnnotations: true,
			}
			p := func(rl *framework.ResourceList) error {
				s := &framework.Selector{TemplateData: test}
				test.Fn(s)
				rl.Items, err = s.Filter(rl.Items)
				return err
			}

			require.NoError(t, framework.Execute(framework.ResourceListProcessorFunc(p), rw))
			require.Equal(t, strings.TrimSpace(expectedFoo), strings.TrimSpace(out.String()))
		})
	}

	// Run the tests by substituting the BarValues
	for i := range tests {
		test := tests[i]
		t.Run(tests[i].Name+"-bar", func(t *testing.T) {
			test.Value = test.ValueBar
			var out bytes.Buffer
			rw := &kio.ByteReadWriter{
				Reader:                bytes.NewBufferString(input),
				Writer:                &out,
				KeepReaderAnnotations: true,
			}

			p := func(rl *framework.ResourceList) error {
				s := &framework.Selector{TemplateData: test}
				test.Fn(s)
				rl.Items, err = s.Filter(rl.Items)
				return err
			}
			require.NoError(t, framework.Execute(framework.ResourceListProcessorFunc(p), rw))
			require.Equal(t, strings.TrimSpace(expectedBar), strings.TrimSpace(out.String()))
		})
	}
}

func TestAndOrSelector_Composition(t *testing.T) {
	// This selector should pick the "prime-target" deployment by name
	// as well as any resources with the given labels or annotations regardless of kind
	s := framework.MatchAny(
		framework.MatchAll(
			framework.GVKMatcher("apps/v1/Deployment"),
			framework.NameMatcher("prime-target"),
		),
		framework.MatchAny(
			framework.LabelMatcher(map[string]string{
				"select": "yes",
			}),
			framework.AnnotationMatcher(map[string]string{
				"example.io/select": "yes",
			}),
		),
	)

	input, err := kio.FromBytes([]byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: prime-target
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: exclude-one
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: exclude-two
  labels:
    select: no
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: extra-target
  labels:
    select: yes
---
apiVersion: apps/v1
kind: ConfigMap
metadata:
  name: prime-target
data:
  shouldSelect: false
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: extra-target-one
  labels:
    select: yes
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: extra-target-two
  annotations:
    example.io/select: yes
`))
	require.NoError(t, err)
	result, err := s.Filter(input)
	require.NoError(t, err)

	expected := `
apiVersion: apps/v1
kind: Deployment
metadata:
  name: prime-target
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: extra-target
  labels:
    select: yes
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: extra-target-one
  labels:
    select: yes
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: extra-target-two
  annotations:
    example.io/select: yes
`
	resultStr, err := kio.StringAll(result)
	require.NoError(t, err)
	assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(resultStr))
}

func TestAndOrSelector_CompositionTemplated(t *testing.T) {
	// This selector should pick the "prime-target" deployment by name
	// as well as any resources with the given labels or annotations regardless of kind
	// Note: very similar to above test, but uses verbose expression to access templating
	type templateStruct struct {
		GVK             string
		Name            string
		LabelValue      string
		AnnotationValue string
	}

	s := framework.OrSelector{
		// This should get propagated to matchers without explicit data
		TemplateData: &templateStruct{
			GVK:             "apps/v1/Oops",
			Name:            "extra-target",
			LabelValue:      "yes",
			AnnotationValue: "yes",
		},
		Matchers: []framework.ResourceMatcher{
			&framework.AndSelector{
				TemplateData: &templateStruct{
					GVK:  "apps/v1/Deployment",
					Name: "prime-target",
				},
				Matchers: []framework.ResourceMatcher{
					framework.GVKMatcher("{{.GVK}}"),
					framework.NameMatcher("{{.Name}}"),
				},
			},
			&framework.OrSelector{
				Matchers: []framework.ResourceMatcher{
					framework.LabelMatcher(map[string]string{
						"select": "{{.LabelValue}}",
					}),
					framework.AnnotationMatcher(map[string]string{
						"example.io/select": "{{.AnnotationValue}}",
					}),
				},
			},
		},
	}

	input, err := kio.FromBytes([]byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: prime-target
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: exclude-one
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: exclude-two
  labels:
    select: no
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: extra-target
  labels:
    select: yes
---
apiVersion: apps/v1
kind: ConfigMap
metadata:
  name: prime-target
data:
  shouldSelect: false
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: extra-target-one
  labels:
    select: yes
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: extra-target-two
  annotations:
    example.io/select: yes
`))
	require.NoError(t, err)
	result, err := s.Filter(input)
	require.NoError(t, err)

	expected := `
apiVersion: apps/v1
kind: Deployment
metadata:
  name: prime-target
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: extra-target
  labels:
    select: yes
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: extra-target-one
  labels:
    select: yes
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: extra-target-two
  annotations:
    example.io/select: yes
`
	resultStr, err := kio.StringAll(result)
	require.NoError(t, err)
	assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(resultStr))
}

func TestMatchersAsFilters(t *testing.T) {
	input, err := kio.FromBytes([]byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: target
  labels:
    select: me
---
apiVersion: extensions/v1beta2
kind: Deployment
metadata:
  name: exclude
  labels:
    select: no
`))
	require.NoError(t, err)

	expected := `
apiVersion: apps/v1
kind: Deployment
metadata:
  name: target
  labels:
    select: me
`
	matchers := map[string]framework.ResourceMatcher{
		"slice": framework.NameMatcher("target"),
		"map":   framework.LabelMatcher(map[string]string{"select": "me"}),
		"func": framework.ResourceMatcherFunc(func(node *yaml.RNode) bool {
			v := node.Field("apiVersion").Value
			return strings.TrimSpace(v.MustString()) == "apps/v1"
		}),
	}
	for desc, m := range matchers {
		matcher := m
		t.Run(desc, func(t *testing.T) {
			result, err := matcher.Filter(input)
			require.NoError(t, err)
			resultStr, err := kio.StringAll(result)
			require.NoError(t, err)
			assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(resultStr))
		})
	}
}
