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

package refvar_test

import (
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"
	. "sigs.k8s.io/kustomize/api/filters/refvar"
)

type expected struct {
	count  int
	edited string
}

func TestPrimitiveReplacer(t *testing.T) {
	varCounts := make(map[string]int)
	f := MakePrimitiveReplacer(
		varCounts,
		map[string]interface{}{
			"FOO":        "bar",
			"ZOO":        "$(FOO)-1",
			"BLU":        "$(ZOO)-2",
			"EIGHT":      8,
			"PI":         3.14159,
			"ZINT":       "$(INT)",
			"BOOL":       "true",
			"HUGENUMBER": int64(9223372036854775807),
			"CRAZYMAP":   map[string]int{"crazy": 200},
			"ZBOOL":      "$(BOOL)",
		})
	assert.Equal(t, "$()", f(""))
	assert.Equal(t, "$(      )", f("      "))
	assert.Equal(t, "$(florida)", f("florida"))
	assert.Equal(t, "$(0)", f("0"))
	assert.Equal(t, "bar", f("FOO"))
	assert.Equal(t, "bar", f("FOO"))
	assert.Equal(t, "bar", f("FOO"))
	assert.Equal(t, 8, f("EIGHT"))
	assert.Equal(t, 8, f("EIGHT"))
	assert.Equal(t, 3.14159, f("PI"))
	assert.Equal(t, "true", f("BOOL"))
	assert.Equal(t, int64(9223372036854775807), f("HUGENUMBER"))
	assert.Equal(t, "$(FOO)-1", f("ZOO"))
	assert.Equal(t, "$(CRAZYMAP)", f("CRAZYMAP"))
	assert.Equal(t,
		map[string]int{
			"FOO":        3,
			"EIGHT":      2,
			"BOOL":       1,
			"PI":         1,
			"ZOO":        1,
			"HUGENUMBER": 1,
		},
		varCounts)
}

func TestMapReference(t *testing.T) {
	type env struct {
		Name  string
		Value interface{}
	}
	envs := []env{
		{
			Name:  "FOO",
			Value: "bar",
		},
		{
			Name:  "ZOO",
			Value: "$(FOO)-1",
		},
		{
			Name:  "BLU",
			Value: "$(ZOO)-2",
		},
		{
			Name:  "INT",
			Value: 2,
		},
		{
			Name:  "ZINT",
			Value: "$(INT)",
		},
		{
			Name:  "BOOL",
			Value: true,
		},
		{
			Name:  "ZBOOL",
			Value: "$(BOOL)",
		},
	}

	varMap := map[string]interface{}{
		"FOO":   "bar",
		"ZOO":   "$(FOO)-1",
		"BLU":   "$(ZOO)-2",
		"INT":   "2",
		"ZINT":  "$(INT)",
		"BOOL":  "true",
		"ZBOOL": "$(BOOL)",
	}

	varCounts := make(map[string]int)
	for _, env := range envs {
		varMap[env.Name] = DoReplacements(
			fmt.Sprintf("%v", env.Value),
			MakePrimitiveReplacer(varCounts, varMap))
	}

	expectedEnv := map[string]expected{
		"FOO":   {count: 1, edited: "bar"},
		"ZOO":   {count: 1, edited: "bar-1"},
		"BLU":   {count: 0, edited: "bar-1-2"},
		"INT":   {count: 1, edited: "2"},
		"ZINT":  {count: 0, edited: "2"},
		"BOOL":  {count: 1, edited: "true"},
		"ZBOOL": {count: 0, edited: "true"},
	}

	for k, v := range expectedEnv {
		if e, a := v, varMap[k]; e.edited != a || e.count != varCounts[k] {
			t.Errorf("Expected %v count=%d, got %v count=%d",
				e.edited, e.count, a, varCounts[k])
		} else {
			delete(varMap, k)
		}
	}

	if len(varMap) != 0 {
		t.Errorf("Unexpected keys in declared env: %v", varMap)
	}
}

func TestMapping(t *testing.T) {
	cases := []struct {
		name     string
		input    string
		expected string
		counts   map[string]int
	}{
		{
			name:     "whole string",
			input:    "$(VAR_A)",
			expected: "A",
			counts:   map[string]int{"VAR_A": 1},
		},
		{
			name:     "repeat",
			input:    "$(VAR_A)-$(VAR_A)",
			expected: "A-A",
			counts:   map[string]int{"VAR_A": 2},
		},
		{
			name:     "multiple repeats",
			input:    "$(VAR_A)-$(VAR_B)-$(VAR_B)-$(VAR_B)-$(VAR_A)",
			expected: "A-B-B-B-A",
			counts:   map[string]int{"VAR_A": 2, "VAR_B": 3},
		},
		{
			name:     "beginning",
			input:    "$(VAR_A)-1",
			expected: "A-1",
			counts:   map[string]int{"VAR_A": 1},
		},
		{
			name:     "middle",
			input:    "___$(VAR_B)___",
			expected: "___B___",
			counts:   map[string]int{"VAR_B": 1},
		},
		{
			name:     "end",
			input:    "___$(VAR_C)",
			expected: "___C",
			counts:   map[string]int{"VAR_C": 1},
		},
		{
			name:     "compound",
			input:    "$(VAR_A)_$(VAR_B)_$(VAR_C)",
			expected: "A_B_C",
			counts:   map[string]int{"VAR_A": 1, "VAR_B": 1, "VAR_C": 1},
		},
		{
			name:     "escape & expand",
			input:    "$$(VAR_B)_$(VAR_A)",
			expected: "$(VAR_B)_A",
			counts:   map[string]int{"VAR_A": 1},
		},
		{
			name:     "compound escape",
			input:    "$$(VAR_A)_$$(VAR_B)",
			expected: "$(VAR_A)_$(VAR_B)",
		},
		{
			name:     "mixed in escapes",
			input:    "f000-$$VAR_A",
			expected: "f000-$VAR_A",
		},
		{
			name:     "backslash escape ignored",
			input:    "foo\\$(VAR_C)bar",
			expected: "foo\\Cbar",
			counts:   map[string]int{"VAR_C": 1},
		},
		{
			name:     "backslash escape ignored",
			input:    "foo\\\\$(VAR_C)bar",
			expected: "foo\\\\Cbar",
			counts:   map[string]int{"VAR_C": 1},
		},
		{
			name:     "lots of backslashes",
			input:    "foo\\\\\\\\$(VAR_A)bar",
			expected: "foo\\\\\\\\Abar",
			counts:   map[string]int{"VAR_A": 1},
		},
		{
			name:     "nested var references",
			input:    "$(VAR_A$(VAR_B))",
			expected: "$(VAR_A$(VAR_B))",
		},
		{
			name:     "nested var references second type",
			input:    "$(VAR_A$(VAR_B)",
			expected: "$(VAR_A$(VAR_B)",
		},
		{
			name:     "value is a reference",
			input:    "$(VAR_REF)",
			expected: "$(VAR_A)",
			counts:   map[string]int{"VAR_REF": 1},
		},
		{
			name:     "value is a reference x 2",
			input:    "%%$(VAR_REF)--$(VAR_REF)%%",
			expected: "%%$(VAR_A)--$(VAR_A)%%",
			counts:   map[string]int{"VAR_REF": 2},
		},
		{
			name:     "empty var",
			input:    "foo$(VAR_EMPTY)bar",
			expected: "foobar",
			counts:   map[string]int{"VAR_EMPTY": 1},
		},
		{
			name:     "unterminated expression",
			input:    "foo$(VAR_Awhoops!",
			expected: "foo$(VAR_Awhoops!",
		},
		{
			name:     "expression without operator",
			input:    "f00__(VAR_A)__",
			expected: "f00__(VAR_A)__",
		},
		{
			name:     "shell special vars pass through",
			input:    "$?_boo_$!",
			expected: "$?_boo_$!",
		},
		{
			name:     "bare operators are ignored",
			input:    "$VAR_A",
			expected: "$VAR_A",
		},
		{
			name:     "undefined vars are passed through",
			input:    "$(VAR_DNE)",
			expected: "$(VAR_DNE)",
		},
		{
			name:     "multiple (even) operators, var undefined",
			input:    "$$$$$$(BIG_MONEY)",
			expected: "$$$(BIG_MONEY)",
		},
		{
			name:     "multiple (even) operators, var defined",
			input:    "$$$$$$(VAR_A)",
			expected: "$$$(VAR_A)",
		},
		{
			name:     "multiple (odd) operators, var undefined",
			input:    "$$$$$$$(GOOD_ODDS)",
			expected: "$$$$(GOOD_ODDS)",
		},
		{
			name:     "multiple (odd) operators, var defined",
			input:    "$$$$$$$(VAR_A)",
			expected: "$$$A",
			counts:   map[string]int{"VAR_A": 1},
		},
		{
			name:     "missing open expression",
			input:    "$VAR_A)",
			expected: "$VAR_A)",
		},
		{
			name:     "shell syntax ignored",
			input:    "${VAR_A}",
			expected: "${VAR_A}",
		},
		{
			name:     "trailing incomplete expression not consumed",
			input:    "$(VAR_B)_______$(A",
			expected: "B_______$(A",
			counts:   map[string]int{"VAR_B": 1},
		},
		{
			name:     "trailing incomplete expression, no content, is not consumed",
			input:    "$(VAR_C)_______$(",
			expected: "C_______$(",
			counts:   map[string]int{"VAR_C": 1},
		},
		{
			name:     "operator at end of input string is preserved",
			input:    "$(VAR_A)foobarzab$",
			expected: "Afoobarzab$",
			counts:   map[string]int{"VAR_A": 1},
		},
		{
			name:     "shell escaped incomplete expr",
			input:    "foo-\\$(VAR_A",
			expected: "foo-\\$(VAR_A",
		},
		{
			name:     "lots of $( in middle",
			input:    "--$($($($($--",
			expected: "--$($($($($--",
		},
		{
			name:     "lots of $( in beginning",
			input:    "$($($($($--foo$(",
			expected: "$($($($($--foo$(",
		},
		{
			name:     "lots of $( at end",
			input:    "foo0--$($($($(",
			expected: "foo0--$($($($(",
		},
		{
			name:     "escaped operators in variable names are not escaped",
			input:    "$(foo$$var)",
			expected: "$(foo$$var)",
		},
		{
			name:     "newline not expanded",
			input:    "\n",
			expected: "\n",
		},
	}
	for _, tc := range cases {
		counts := make(map[string]int)
		expanded := DoReplacements(
			fmt.Sprintf("%v", tc.input),
			MakePrimitiveReplacer(counts, map[string]interface{}{
				"VAR_A":     "A",
				"VAR_B":     "B",
				"VAR_C":     "C",
				"VAR_REF":   "$(VAR_A)",
				"VAR_EMPTY": "",
			}))
		if e, a := tc.expected, expanded; e != a {
			t.Errorf("%v: expected %q, got %q", tc.name, e, a)
		}
		if len(counts) != len(tc.counts) {
			t.Errorf("%v: len(counts)=%d != len(tc.counts)=%d",
				tc.name, len(counts), len(tc.counts))
		}
		if len(tc.counts) > 0 {
			for k, expectedCount := range tc.counts {
				if c, ok := counts[k]; ok {
					if c != expectedCount {
						t.Errorf(
							"%v: k=%s, expected count %d, got %d",
							tc.name, k, expectedCount, c)
					}
				} else {
					t.Errorf(
						"%v: k=%s, expected count %d, got zero",
						tc.name, k, expectedCount)
				}
			}
		}
	}
}
