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

package hasher

import (
	"strings"
	"testing"

	"sigs.k8s.io/kustomize/kyaml/yaml"
)

func TestSortArrayAndComputeHash(t *testing.T) {
	array1 := []string{"a", "b", "c", "d"}
	array2 := []string{"c", "b", "d", "a"}
	h1, err := SortArrayAndComputeHash(array1)
	if err != nil {
		t.Errorf("unexpected error %v", err)
	}
	if h1 == "" {
		t.Errorf("failed to hash %v", array1)
	}
	h2, err := SortArrayAndComputeHash(array2)
	if err != nil {
		t.Errorf("unexpected error %v", err)
	}
	if h2 == "" {
		t.Errorf("failed to hash %v", array2)
	}
	if h1 != h2 {
		t.Errorf("hash is not consistent with reordered list: %s %s", h1, h2)
	}
}

func Test_hex256(t *testing.T) {
	// hash the empty string to be sure that sha256 is being used
	expect := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
	sum := hex256("")
	if expect != sum {
		t.Errorf("expected hash %q but got %q", expect, sum)
	}
}

func TestConfigMapHash(t *testing.T) {
	cases := []struct {
		desc   string
		cmYaml string
		hash   string
		err    string
	}{
		// empty map
		{"empty data", `
apiVersion: v1
kind: ConfigMap`, "6ct58987ht", ""},
		// one key
		{"one key", `
apiVersion: v1
kind: ConfigMap
data:
  one: ""`, "9g67k2htb6", ""},
		// three keys (tests sorting order)
		{"three keys", `
apiVersion: v1
kind: ConfigMap
data:
  two: 2
  one: ""
  three: 3`, "7757f9kkct", ""},
		// empty binary data map
		{"empty binary data", `
apiVersion: v1
kind: ConfigMap`, "6ct58987ht", ""},
		// one key with binary data
		{"one key with binary data", `
apiVersion: v1
kind: ConfigMap
binaryData:
  one: ""`, "6mtk2m274t", ""},
		// three keys with binary data (tests sorting order)
		{"three keys with binary data", `
apiVersion: v1
kind: ConfigMap
binaryData:
  two: 2
  one: ""
  three: 3`, "9th7kc28dg", ""},
		// two keys, one with string and another with binary data
		{"two keys with one each", `
apiVersion: v1
kind: ConfigMap
data:
  one: ""
binaryData:
  two: ""`, "698h7c7t9m", ""},
	}
	h := &Hasher{}
	for _, c := range cases {
		node, err := yaml.Parse(c.cmYaml)
		if err != nil {
			t.Fatal(err)
		}
		hashed, err := h.Hash(node)
		if SkipRest(t, c.desc, err, c.err) {
			continue
		}
		if c.hash != hashed {
			t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h)
		}
	}
}

func TestSecretHash(t *testing.T) {
	cases := []struct {
		desc       string
		secretYaml string
		hash       string
		err        string
	}{
		// empty map
		{"empty data", `
apiVersion: v1
kind: Secret
type: my-type`, "5gmgkf8578", ""},
		// one key
		{"one key", `
apiVersion: v1
kind: Secret
type: my-type
data:
  one: ""`, "74bd68bm66", ""},
		// three keys (tests sorting order)
		{"three keys", `
apiVersion: v1
kind: Secret
type: my-type
data:
  two: 2
  one: ""
  three: 3`, "4gf75c7476", ""},
		// with stringdata
		{"stringdata", `
apiVersion: v1
kind: Secret
type: my-type
data:
  one: ""
stringData:
  two: 2`, "c4h4264gdb", ""},
		// empty stringdata
		{"empty stringdata", `
apiVersion: v1
kind: Secret
type: my-type
data:
  one: ""`, "74bd68bm66", ""},
	}
	h := &Hasher{}
	for _, c := range cases {
		node, err := yaml.Parse(c.secretYaml)
		if err != nil {
			t.Fatal(err)
		}
		hashed, err := h.Hash(node)
		if SkipRest(t, c.desc, err, c.err) {
			continue
		}
		if c.hash != hashed {
			t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h)
		}
	}
}

func TestBasicHash(t *testing.T) {
	cases := map[string]struct {
		res  string
		hash string
		err  string
	}{
		"minimal": {`
apiVersion: test/v1
kind: TestResource
metadata:
  name: my-resource`, "244782mkb7", ""},
		"with spec": {`
apiVersion: test/v1
kind: TestResource
metadata:
  name: my-resource
spec:
  foo: 1
  bar: abc`, "59m2mdccg4", ""},
	}
	h := &Hasher{}
	for n := range cases {
		c := cases[n]
		t.Run(n, func(t *testing.T) {
			node, err := yaml.Parse(c.res)
			if err != nil {
				t.Fatal(err)
			}
			hashed, err := h.Hash(node)
			if SkipRest(t, n, err, c.err) {
				return
			}
			if c.hash != hashed {
				t.Errorf("case %q, expect hash %q but got %q", n, c.hash, h)
			}
		})
	}
}

func TestEncodeConfigMap(t *testing.T) {
	cases := []struct {
		desc   string
		cmYaml string
		expect string
		err    string
	}{
		// empty map
		{"empty data", `
apiVersion: v1
kind: ConfigMap`, `{"data":"","kind":"ConfigMap","name":""}`, ""},
		// one key
		{"one key", `
apiVersion: v1
kind: ConfigMap
data:
  one: ""`, `{"data":{"one":""},"kind":"ConfigMap","name":""}`, ""},
		// three keys (tests sorting order)
		{"three keys", `
apiVersion: v1
kind: ConfigMap
data:
  two: 2
  one: ""
  three: 3`, `{"data":{"one":"","three":3,"two":2},"kind":"ConfigMap","name":""}`, ""},
		// empty binary map
		{"empty data", `
apiVersion: v1
kind: ConfigMap`, `{"data":"","kind":"ConfigMap","name":""}`, ""},
		// one key with binary data
		{"one key", `
apiVersion: v1
kind: ConfigMap
binaryData:
  one: ""`, `{"binaryData":{"one":""},"data":"","kind":"ConfigMap","name":""}`, ""},
		// three keys with binary data (tests sorting order)
		{"three keys", `
apiVersion: v1
kind: ConfigMap
binaryData:
  two: 2
  one: ""
  three: 3`, `{"binaryData":{"one":"","three":3,"two":2},"data":"","kind":"ConfigMap","name":""}`, ""},
		// two keys, one string and one binary values
		{"two keys with one each", `
apiVersion: v1
kind: ConfigMap
data:
  one: ""
binaryData:
  two: ""`, `{"binaryData":{"two":""},"data":{"one":""},"kind":"ConfigMap","name":""}`, ""},
	}
	for _, c := range cases {
		node, err := yaml.Parse(c.cmYaml)
		if err != nil {
			t.Fatal(err)
		}
		s, err := encodeConfigMap(node)
		if SkipRest(t, c.desc, err, c.err) {
			continue
		}
		if s != c.expect {
			t.Errorf("case %q, expect %q but got %q from encode %#v", c.desc, c.expect, s, c.cmYaml)
		}
	}
}

func TestEncodeSecret(t *testing.T) {
	cases := []struct {
		desc       string
		secretYaml string
		expect     string
		err        string
	}{
		// empty map
		{"empty data", `
apiVersion: v1
kind: Secret
type: my-type`, `{"data":"","kind":"Secret","name":"","type":"my-type"}`, ""},
		// one key
		{"one key", `
apiVersion: v1
kind: Secret
type: my-type
data:
  one: ""`, `{"data":{"one":""},"kind":"Secret","name":"","type":"my-type"}`, ""},
		// three keys (tests sorting order) - note json.Marshal base64 encodes the values because they come in as []byte
		{"three keys", `
apiVersion: v1
kind: Secret
type: my-type
data:
  two: 2
  one: ""
  three: 3`, `{"data":{"one":"","three":3,"two":2},"kind":"Secret","name":"","type":"my-type"}`, ""},
		// with stringdata
		{"stringdata", `
apiVersion: v1
kind: Secret
type: my-type
data:
  one: ""
stringData:
  two: 2`, `{"data":{"one":""},"kind":"Secret","name":"","stringData":{"two":2},"type":"my-type"}`, ""},
		// empty stringdata
		{"empty stringdata", `
apiVersion: v1
kind: Secret
type: my-type
data:
  one: ""`, `{"data":{"one":""},"kind":"Secret","name":"","type":"my-type"}`, ""},
	}
	for _, c := range cases {
		node, err := yaml.Parse(c.secretYaml)
		if err != nil {
			t.Fatal(err)
		}
		s, err := encodeSecret(node)
		if SkipRest(t, c.desc, err, c.err) {
			continue
		}
		if s != c.expect {
			t.Errorf("case %q, expect %q but got %q from encode %#v", c.desc, c.expect, s, c.secretYaml)
		}
	}
}

// SkipRest returns true if there was a non-nil error or if we expected an
// error that didn't happen, and logs the appropriate error on the test object.
// The return value indicates whether we should skip the rest of the test case
// due to the error result.
func SkipRest(t *testing.T, desc string, err error, contains string) bool {
	t.Helper()
	if err != nil {
		if len(contains) == 0 {
			t.Errorf("case %q, expect nil error but got %q", desc, err.Error())
		} else if !strings.Contains(err.Error(), contains) {
			t.Errorf("case %q, expect error to contain %q but got %q", desc, contains, err.Error())
		}
		return true
	} else if len(contains) > 0 {
		t.Errorf("case %q, expect error to contain %q but got nil error", desc, contains)
		return true
	}
	return false
}
