// Copyright 2015 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package proptools

import (
	"bytes"
	"os/exec"
	"reflect"
	"slices"
	"testing"
	"unsafe"
)

type escapeTestCase struct {
	name string
	in   string
	out  string
}

var ninjaEscapeTestCase = []escapeTestCase{
	{
		name: "no escaping",
		in:   `test`,
		out:  `test`,
	},
	{
		name: "leading $",
		in:   `$test`,
		out:  `$$test`,
	},
	{
		name: "trailing $",
		in:   `test$`,
		out:  `test$$`,
	},
	{
		name: "leading and trailing $",
		in:   `$test$`,
		out:  `$$test$$`,
	},
}

var shellEscapeTestCase = []escapeTestCase{
	{
		name: "no escaping",
		in:   `test`,
		out:  `test`,
	},
	{
		name: "leading $",
		in:   `$test`,
		out:  `'$test'`,
	},
	{
		name: "trailing $",
		in:   `test$`,
		out:  `'test$'`,
	},
	{
		name: "leading and trailing $",
		in:   `$test$`,
		out:  `'$test$'`,
	},
	{
		name: "single quote",
		in:   `'`,
		out:  `''\'''`,
	},
	{
		name: "multiple single quote",
		in:   `''`,
		out:  `''\'''\'''`,
	},
	{
		name: "double quote",
		in:   `""`,
		out:  `'""'`,
	},
	{
		name: "ORIGIN",
		in:   `-Wl,--rpath,${ORIGIN}/../bionic-loader-test-libs`,
		out:  `'-Wl,--rpath,${ORIGIN}/../bionic-loader-test-libs'`,
	},
}

var shellEscapeIncludingSpacesTestCase = []escapeTestCase{
	{
		name: "no escaping",
		in:   `test`,
		out:  `test`,
	},
	{
		name: "spacing",
		in:   `arg1 arg2`,
		out:  `'arg1 arg2'`,
	},
	{
		name: "single quote",
		in:   `'arg'`,
		out:  `''\''arg'\'''`,
	},
}

func TestNinjaEscaping(t *testing.T) {
	for _, testCase := range ninjaEscapeTestCase {
		got := NinjaEscape(testCase.in)
		if got != testCase.out {
			t.Errorf("%s: expected `%s` got `%s`", testCase.name, testCase.out, got)
		}
	}
}

func TestShellEscaping(t *testing.T) {
	for _, testCase := range shellEscapeTestCase {
		got := ShellEscape(testCase.in)
		if got != testCase.out {
			t.Errorf("%s: expected `%s` got `%s`", testCase.name, testCase.out, got)
		}
	}
}

func TestShellEscapeIncludingSpaces(t *testing.T) {
	for _, testCase := range shellEscapeIncludingSpacesTestCase {
		got := ShellEscapeIncludingSpaces(testCase.in)
		if got != testCase.out {
			t.Errorf("%s: expected `%s` got `%s`", testCase.name, testCase.out, got)
		}
	}
}

func TestExternalShellEscaping(t *testing.T) {
	if testing.Short() {
		return
	}
	for _, testCase := range shellEscapeTestCase {
		cmd := "echo " + ShellEscape(testCase.in)
		got, err := exec.Command("/bin/sh", "-c", cmd).Output()
		got = bytes.TrimSuffix(got, []byte("\n"))
		if err != nil {
			t.Error(err)
		}
		if string(got) != testCase.in {
			t.Errorf("%s: expected `%s` got `%s`", testCase.name, testCase.in, got)
		}
	}
}

func TestExternalShellEscapeIncludingSpaces(t *testing.T) {
	if testing.Short() {
		return
	}
	for _, testCase := range shellEscapeIncludingSpacesTestCase {
		cmd := "echo " + ShellEscapeIncludingSpaces(testCase.in)
		got, err := exec.Command("/bin/sh", "-c", cmd).Output()
		got = bytes.TrimSuffix(got, []byte("\n"))
		if err != nil {
			t.Error(err)
		}
		if string(got) != testCase.in {
			t.Errorf("%s: expected `%s` got `%s`", testCase.name, testCase.in, got)
		}
	}
}

func TestNinjaEscapeList(t *testing.T) {
	type testCase struct {
		name                 string
		in                   []string
		ninjaEscaped         []string
		shellEscaped         []string
		ninjaAndShellEscaped []string
		sameSlice            bool
	}
	testCases := []testCase{
		{
			name:      "empty",
			in:        []string{},
			sameSlice: true,
		},
		{
			name:      "nil",
			in:        nil,
			sameSlice: true,
		},
		{
			name:      "no escaping",
			in:        []string{"abc", "def", "ghi"},
			sameSlice: true,
		},
		{
			name:                 "escape first",
			in:                   []string{`$\abc`, "def", "ghi"},
			ninjaEscaped:         []string{`$$\abc`, "def", "ghi"},
			shellEscaped:         []string{`'$\abc'`, "def", "ghi"},
			ninjaAndShellEscaped: []string{`'$$\abc'`, "def", "ghi"},
		},
		{
			name:                 "escape middle",
			in:                   []string{"abc", `$\def`, "ghi"},
			ninjaEscaped:         []string{"abc", `$$\def`, "ghi"},
			shellEscaped:         []string{"abc", `'$\def'`, "ghi"},
			ninjaAndShellEscaped: []string{"abc", `'$$\def'`, "ghi"},
		},
		{
			name:                 "escape last",
			in:                   []string{"abc", "def", `$\ghi`},
			ninjaEscaped:         []string{"abc", "def", `$$\ghi`},
			shellEscaped:         []string{"abc", "def", `'$\ghi'`},
			ninjaAndShellEscaped: []string{"abc", "def", `'$$\ghi'`},
		},
	}

	testFuncs := []struct {
		name     string
		f        func([]string) []string
		expected func(tt testCase) []string
	}{
		{name: "NinjaEscapeList", f: NinjaEscapeList, expected: func(tt testCase) []string { return tt.ninjaEscaped }},
		{name: "ShellEscapeList", f: ShellEscapeList, expected: func(tt testCase) []string { return tt.shellEscaped }},
		{name: "NinjaAndShellEscapeList", f: NinjaAndShellEscapeList, expected: func(tt testCase) []string { return tt.ninjaAndShellEscaped }},
	}

	for _, tf := range testFuncs {
		t.Run(tf.name, func(t *testing.T) {
			for _, tt := range testCases {
				t.Run(tt.name, func(t *testing.T) {
					inCopy := slices.Clone(tt.in)

					got := tf.f(tt.in)

					want := tf.expected(tt)
					if tt.sameSlice {
						want = tt.in
					}

					if !reflect.DeepEqual(got, want) {
						t.Errorf("incorrect output, want %q got %q", want, got)
					}
					if len(inCopy) != len(tt.in) && (len(tt.in) == 0 || !reflect.DeepEqual(inCopy, tt.in)) {
						t.Errorf("input modified, want %#v, got %#v", inCopy, tt.in)
					}

					if (unsafe.SliceData(tt.in) == unsafe.SliceData(got)) != tt.sameSlice {
						if tt.sameSlice {
							t.Errorf("expected input and output slices to have the same backing arrays")
						} else {
							t.Errorf("expected input and output slices to have different backing arrays")
						}
					}
				})
			}
		})
	}
}
