// Copyright 2021 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 android

import (
	"android/soong/bazel"
	"testing"
)

func TestExpandVars(t *testing.T) {
	android_arm64_config := TestConfig("out", nil, "", nil)
	android_arm64_config.BuildOS = Android
	android_arm64_config.BuildArch = Arm64

	testCases := []struct {
		description     string
		config          Config
		stringScope     ExportedStringVariables
		stringListScope ExportedStringListVariables
		configVars      ExportedConfigDependingVariables
		toExpand        string
		expectedValues  []string
	}{
		{
			description:    "no expansion for non-interpolated value",
			toExpand:       "foo",
			expectedValues: []string{"foo"},
		},
		{
			description: "single level expansion for string var",
			stringScope: ExportedStringVariables{
				"foo": "bar",
			},
			toExpand:       "${foo}",
			expectedValues: []string{"bar"},
		},
		{
			description: "single level expansion with short-name for string var",
			stringScope: ExportedStringVariables{
				"foo": "bar",
			},
			toExpand:       "${config.foo}",
			expectedValues: []string{"bar"},
		},
		{
			description: "single level expansion string list var",
			stringListScope: ExportedStringListVariables{
				"foo": []string{"bar"},
			},
			toExpand:       "${foo}",
			expectedValues: []string{"bar"},
		},
		{
			description: "mixed level expansion for string list var",
			stringScope: ExportedStringVariables{
				"foo": "${bar}",
				"qux": "hello",
			},
			stringListScope: ExportedStringListVariables{
				"bar": []string{"baz", "${qux}"},
			},
			toExpand:       "${foo}",
			expectedValues: []string{"baz hello"},
		},
		{
			description: "double level expansion",
			stringListScope: ExportedStringListVariables{
				"foo": []string{"${bar}"},
				"bar": []string{"baz"},
			},
			toExpand:       "${foo}",
			expectedValues: []string{"baz"},
		},
		{
			description: "double level expansion with a literal",
			stringListScope: ExportedStringListVariables{
				"a": []string{"${b}", "c"},
				"b": []string{"d"},
			},
			toExpand:       "${a}",
			expectedValues: []string{"d c"},
		},
		{
			description: "double level expansion, with two variables in a string",
			stringListScope: ExportedStringListVariables{
				"a": []string{"${b} ${c}"},
				"b": []string{"d"},
				"c": []string{"e"},
			},
			toExpand:       "${a}",
			expectedValues: []string{"d e"},
		},
		{
			description: "triple level expansion with two variables in a string",
			stringListScope: ExportedStringListVariables{
				"a": []string{"${b} ${c}"},
				"b": []string{"${c}", "${d}"},
				"c": []string{"${d}"},
				"d": []string{"foo"},
			},
			toExpand:       "${a}",
			expectedValues: []string{"foo foo foo"},
		},
		{
			description: "expansion with config depending vars",
			configVars: ExportedConfigDependingVariables{
				"a": func(c Config) string { return c.BuildOS.String() },
				"b": func(c Config) string { return c.BuildArch.String() },
			},
			config:         android_arm64_config,
			toExpand:       "${a}-${b}",
			expectedValues: []string{"android-arm64"},
		},
		{
			description: "double level multi type expansion",
			stringListScope: ExportedStringListVariables{
				"platform": []string{"${os}-${arch}"},
				"const":    []string{"const"},
			},
			configVars: ExportedConfigDependingVariables{
				"os":   func(c Config) string { return c.BuildOS.String() },
				"arch": func(c Config) string { return c.BuildArch.String() },
				"foo":  func(c Config) string { return "foo" },
			},
			config:         android_arm64_config,
			toExpand:       "${const}/${platform}/${foo}",
			expectedValues: []string{"const/android-arm64/foo"},
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.description, func(t *testing.T) {
			output, _ := expandVar(testCase.config, testCase.toExpand, testCase.stringScope, testCase.stringListScope, testCase.configVars)
			if len(output) != len(testCase.expectedValues) {
				t.Errorf("Expected %d values, got %d", len(testCase.expectedValues), len(output))
			}
			for i, actual := range output {
				expectedValue := testCase.expectedValues[i]
				if actual != expectedValue {
					t.Errorf("Actual value '%s' doesn't match expected value '%s'", actual, expectedValue)
				}
			}
		})
	}
}

func TestBazelToolchainVars(t *testing.T) {
	testCases := []struct {
		name        string
		config      Config
		vars        ExportedVariables
		expectedOut string
	}{
		{
			name: "exports strings",
			vars: ExportedVariables{
				exportedStringVars: ExportedStringVariables{
					"a": "b",
					"c": "d",
				},
			},
			expectedOut: bazel.GeneratedBazelFileWarning + `

_a = "b"

_c = "d"

constants = struct(
    a = _a,
    c = _c,
)`,
		},
		{
			name: "exports string lists",
			vars: ExportedVariables{
				exportedStringListVars: ExportedStringListVariables{
					"a": []string{"b1", "b2"},
					"c": []string{"d1", "d2"},
				},
			},
			expectedOut: bazel.GeneratedBazelFileWarning + `

_a = [
    "b1",
    "b2",
]

_c = [
    "d1",
    "d2",
]

constants = struct(
    a = _a,
    c = _c,
)`,
		},
		{
			name: "exports string lists dicts",
			vars: ExportedVariables{
				exportedStringListDictVars: ExportedStringListDictVariables{
					"a": map[string][]string{"b1": {"b2"}},
					"c": map[string][]string{"d1": {"d2"}},
				},
			},
			expectedOut: bazel.GeneratedBazelFileWarning + `

_a = {
    "b1": ["b2"],
}

_c = {
    "d1": ["d2"],
}

constants = struct(
    a = _a,
    c = _c,
)`,
		},
		{
			name: "exports dict with var refs",
			vars: ExportedVariables{
				exportedVariableReferenceDictVars: ExportedVariableReferenceDictVariables{
					"a": map[string]string{"b1": "${b2}"},
					"c": map[string]string{"d1": "${config.d2}"},
				},
			},
			expectedOut: bazel.GeneratedBazelFileWarning + `

_a = {
    "b1": _b2,
}

_c = {
    "d1": _d2,
}

constants = struct(
    a = _a,
    c = _c,
)`,
		},
		{
			name: "sorts across types with variable references last",
			vars: ExportedVariables{
				exportedStringVars: ExportedStringVariables{
					"b": "b-val",
					"d": "d-val",
				},
				exportedStringListVars: ExportedStringListVariables{
					"c": []string{"c-val"},
					"e": []string{"e-val"},
				},
				exportedStringListDictVars: ExportedStringListDictVariables{
					"a": map[string][]string{"a1": {"a2"}},
					"f": map[string][]string{"f1": {"f2"}},
				},
				exportedVariableReferenceDictVars: ExportedVariableReferenceDictVariables{
					"aa": map[string]string{"b1": "${b}"},
					"cc": map[string]string{"d1": "${config.d}"},
				},
			},
			expectedOut: bazel.GeneratedBazelFileWarning + `

_a = {
    "a1": ["a2"],
}

_b = "b-val"

_c = ["c-val"]

_d = "d-val"

_e = ["e-val"]

_f = {
    "f1": ["f2"],
}

_aa = {
    "b1": _b,
}

_cc = {
    "d1": _d,
}

constants = struct(
    a = _a,
    b = _b,
    c = _c,
    d = _d,
    e = _e,
    f = _f,
    aa = _aa,
    cc = _cc,
)`,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			out := BazelToolchainVars(tc.config, tc.vars)
			if out != tc.expectedOut {
				t.Errorf("Expected \n%s, got \n%s", tc.expectedOut, out)
			}
		})
	}
}

func TestSplitStringKeepingQuotedSubstring(t *testing.T) {
	testCases := []struct {
		description string
		s           string
		delimiter   byte
		split       []string
	}{
		{
			description: "empty string returns single empty string",
			s:           "",
			delimiter:   ' ',
			split: []string{
				"",
			},
		},
		{
			description: "string with single space returns two empty strings",
			s:           " ",
			delimiter:   ' ',
			split: []string{
				"",
				"",
			},
		},
		{
			description: "string with two spaces returns three empty strings",
			s:           "  ",
			delimiter:   ' ',
			split: []string{
				"",
				"",
				"",
			},
		},
		{
			description: "string with four words returns four word string",
			s:           "hello world with words",
			delimiter:   ' ',
			split: []string{
				"hello",
				"world",
				"with",
				"words",
			},
		},
		{
			description: "string with words and nested quote returns word strings and quote string",
			s:           `hello "world with" words`,
			delimiter:   ' ',
			split: []string{
				"hello",
				`"world with"`,
				"words",
			},
		},
		{
			description: "string with escaped quote inside real quotes",
			s:           `hello \"world "with\" words"`,
			delimiter:   ' ',
			split: []string{
				"hello",
				`"world`,
				`"with" words"`,
			},
		},
		{
			description: "string with words and escaped quotes returns word strings",
			s:           `hello \"world with\" words`,
			delimiter:   ' ',
			split: []string{
				"hello",
				`"world`,
				`with"`,
				"words",
			},
		},
		{
			description: "string which is single quoted substring returns only substring",
			s:           `"hello world with words"`,
			delimiter:   ' ',
			split: []string{
				`"hello world with words"`,
			},
		},
		{
			description: "string starting with quote returns quoted string",
			s:           `"hello world with" words`,
			delimiter:   ' ',
			split: []string{
				`"hello world with"`,
				"words",
			},
		},
		{
			description: "string with starting quote and no ending quote returns quote to end of string",
			s:           `hello "world with words`,
			delimiter:   ' ',
			split: []string{
				"hello",
				`"world with words`,
			},
		},
		{
			description: "quoted string is treated as a single \"word\" unless separated by delimiter",
			s:           `hello "world"with words`,
			delimiter:   ' ',
			split: []string{
				"hello",
				`"world"with`,
				"words",
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.description, func(t *testing.T) {
			split := splitStringKeepingQuotedSubstring(tc.s, tc.delimiter)
			if len(split) != len(tc.split) {
				t.Fatalf("number of split string elements (%d) differs from expected (%d): split string (%v), expected (%v)",
					len(split), len(tc.split), split, tc.split,
				)
			}
			for i := range split {
				if split[i] != tc.split[i] {
					t.Errorf("split string element (%d), %v, differs from expected, %v", i, split[i], tc.split[i])
				}
			}
		})
	}
}