// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you 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 common

import (
	"encoding/json"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"

	"github.com/elastic/beats/libbeat/logp"
)

func TestConvertNestedMapStr(t *testing.T) {
	logp.TestingSetup()

	type io struct {
		Input  MapStr
		Output MapStr
	}

	type String string

	tests := []io{
		{
			Input: MapStr{
				"key": MapStr{
					"key1": "value1",
				},
			},
			Output: MapStr{
				"key": MapStr{
					"key1": "value1",
				},
			},
		},
		{
			Input: MapStr{
				"key": MapStr{
					"key1": String("value1"),
				},
			},
			Output: MapStr{
				"key": MapStr{
					"key1": "value1",
				},
			},
		},
		{
			Input: MapStr{
				"key": MapStr{
					"key1": []string{"value1", "value2"},
				},
			},
			Output: MapStr{
				"key": MapStr{
					"key1": []string{"value1", "value2"},
				},
			},
		},
		{
			Input: MapStr{
				"key": MapStr{
					"key1": []String{"value1", "value2"},
				},
			},
			Output: MapStr{
				"key": MapStr{
					"key1": []interface{}{"value1", "value2"},
				},
			},
		},
		{
			Input: MapStr{
				"@timestamp": MustParseTime("2015-03-01T12:34:56.123Z"),
			},
			Output: MapStr{
				"@timestamp": MustParseTime("2015-03-01T12:34:56.123Z"),
			},
		},
		{
			Input: MapStr{
				"env":  nil,
				"key2": uintptr(88),
				"key3": func() { t.Log("hello") },
			},
			Output: MapStr{},
		},
		{
			Input: MapStr{
				"key": []MapStr{
					{"keyX": []String{"value1", "value2"}},
				},
			},
			Output: MapStr{
				"key": []MapStr{
					{"keyX": []interface{}{"value1", "value2"}},
				},
			},
		},
		{
			Input: MapStr{
				"key": []interface{}{
					MapStr{"key1": []string{"value1", "value2"}},
				},
			},
			Output: MapStr{
				"key": []interface{}{
					MapStr{"key1": []string{"value1", "value2"}},
				},
			},
		},
		{
			MapStr{"k": map[string]int{"hits": 1}},
			MapStr{"k": MapStr{"hits": float64(1)}},
		},
	}

	for i, test := range tests {
		assert.Equal(t, test.Output, ConvertToGenericEvent(test.Input), "Test case %d", i)
	}
}

func TestConvertNestedStruct(t *testing.T) {
	logp.TestingSetup()

	type io struct {
		Input  MapStr
		Output MapStr
	}

	type TestStruct struct {
		A string
		B int
	}

	tests := []io{
		{
			Input: MapStr{
				"key": MapStr{
					"key1": TestStruct{
						A: "hello",
						B: 5,
					},
				},
			},
			Output: MapStr{
				"key": MapStr{
					"key1": MapStr{
						"A": "hello",
						"B": float64(5),
					},
				},
			},
		},
		{
			Input: MapStr{
				"key": []interface{}{
					TestStruct{
						A: "hello",
						B: 5,
					},
				},
			},
			Output: MapStr{
				"key": []interface{}{
					MapStr{
						"A": "hello",
						"B": float64(5),
					},
				},
			},
		},
	}

	for i, test := range tests {
		assert.EqualValues(t, test.Output, ConvertToGenericEvent(test.Input), "Test case %v", i)
	}
}

func TestNormalizeValue(t *testing.T) {
	logp.TestingSetup()

	var nilStringPtr *string
	var nilTimePtr *time.Time
	someString := "foo"

	type mybool bool
	type myint int32
	type myuint uint8

	var tests = []struct {
		in  interface{}
		out interface{}
	}{
		{nil, nil},
		{&someString, someString},   // Pointers are dereferenced.
		{nilStringPtr, nil},         // Nil pointers are dropped.
		{nilTimePtr, nil},           // Nil pointers are dropped.
		{NetString("test"), "test"}, // It honors the TextMarshaler contract.
		{true, true},
		{int8(8), int8(8)},
		{uint8(8), uint8(8)},
		{"hello", "hello"},
		{map[string]interface{}{"foo": "bar"}, MapStr{"foo": "bar"}},

		// Other map types are converted using marshalUnmarshal which will lose
		// type information for arrays which become []interface{} and numbers
		// which all become float64.
		{map[string]string{"foo": "bar"}, MapStr{"foo": "bar"}},
		{map[string][]string{"list": {"foo", "bar"}}, MapStr{"list": []interface{}{"foo", "bar"}}},

		{[]string{"foo", "bar"}, []string{"foo", "bar"}},
		{[]bool{true, false}, []bool{true, false}},
		{[]string{"foo", "bar"}, []string{"foo", "bar"}},
		{[]int{10, 11}, []int{10, 11}},
		{[]MapStr{{"foo": "bar"}}, []MapStr{{"foo": "bar"}}},
		{[]map[string]interface{}{{"foo": "bar"}}, []MapStr{{"foo": "bar"}}},

		// Wrapper types are converted to primitives using reflection.
		{mybool(true), true},
		{myint(32), int64(32)},
		{myuint(8), uint64(8)},

		// Slices of wrapper types are converted to an []interface{} of primitives.
		{[]mybool{true, false}, []interface{}{true, false}},
		{[]myint{32}, []interface{}{int64(32)}},
		{[]myuint{8}, []interface{}{uint64(8)}},
	}

	for i, test := range tests {
		out, err := normalizeValue(test.in)
		if err != nil {
			t.Error(err)
			continue
		}

		assert.Equal(t, test.out, out, "Test case %v", i)
	}

	var floatTests = []struct {
		in  interface{}
		out interface{}
	}{
		{float32(1), float64(1)},
		{float64(1), float64(1)},
	}

	for i, test := range floatTests {
		out, err := normalizeValue(test.in)
		if err != nil {
			t.Error(err)
			continue
		}
		assert.InDelta(t, test.out, float64(out.(Float)), 0.000001, "(approximate) Test case %v", i)
	}
}

func TestNormalizeMapError(t *testing.T) {
	badInputs := []MapStr{
		{"func": func() {}},
		{"chan": make(chan struct{})},
		{"uintptr": uintptr(123)},
	}

	for i, in := range badInputs {
		_, errs := normalizeMap(in, "bad.type")
		if assert.Len(t, errs, 1) {
			t.Log(errs[0])
			assert.Contains(t, errs[0].Error(), "key=bad.type", "Test case %v", i)
		}
	}
}

func TestJoinKeys(t *testing.T) {
	assert.Equal(t, "", joinKeys(""))
	assert.Equal(t, "co", joinKeys("co"))
	assert.Equal(t, "co.elastic", joinKeys("", "co", "elastic"))
	assert.Equal(t, "co.elastic", joinKeys("co", "elastic"))
}

func TestMarshalUnmarshalMap(t *testing.T) {
	tests := []struct {
		in  MapStr
		out MapStr
	}{
		{MapStr{"names": []string{"a", "b"}}, MapStr{"names": []interface{}{"a", "b"}}},
	}

	for i, test := range tests {
		var out MapStr
		err := marshalUnmarshal(test.in, &out)
		if err != nil {
			t.Error(err)
			continue
		}

		assert.Equal(t, test.out, out, "Test case %v", i)
	}
}

func TestMarshalUnmarshalArray(t *testing.T) {
	tests := []struct {
		in  interface{}
		out interface{}
	}{
		{[]string{"a", "b"}, []interface{}{"a", "b"}},
	}

	for i, test := range tests {
		var out interface{}
		err := marshalUnmarshal(test.in, &out)
		if err != nil {
			t.Error(err)
			continue
		}

		assert.Equal(t, test.out, out, "Test case %v", i)
	}
}

func TestMarshalFloatValues(t *testing.T) {
	assert := assert.New(t)

	var f float64

	f = 5

	a := MapStr{
		"f": Float(f),
	}

	b, err := json.Marshal(a)
	assert.Nil(err)
	assert.Equal(string(b), "{\"f\":5.000000}")
}

func TestNormalizeTime(t *testing.T) {
	ny, err := time.LoadLocation("America/New_York")
	if err != nil {
		t.Fatal(err)
	}

	now := time.Now().In(ny)
	v, errs := normalizeValue(now, "@timestamp")
	if len(errs) > 0 {
		t.Fatal(errs)
	}

	utcCommonTime, ok := v.(Time)
	if !ok {
		t.Fatalf("expected common.Time, but got %T (%v)", v, v)
	}

	assert.Equal(t, time.UTC, time.Time(utcCommonTime).Location())
	assert.True(t, now.Equal(time.Time(utcCommonTime)))
}

// Uses TextMarshaler interface.
func BenchmarkConvertToGenericEventNetString(b *testing.B) {
	for i := 0; i < b.N; i++ {
		ConvertToGenericEvent(MapStr{"key": NetString("hola")})
	}
}

// Uses reflection.
func BenchmarkConvertToGenericEventMapStringString(b *testing.B) {
	for i := 0; i < b.N; i++ {
		ConvertToGenericEvent(MapStr{"key": map[string]string{"greeting": "hola"}})
	}
}

// Uses recursion to step into the nested MapStr.
func BenchmarkConvertToGenericEventMapStr(b *testing.B) {
	for i := 0; i < b.N; i++ {
		ConvertToGenericEvent(MapStr{"key": map[string]interface{}{"greeting": "hola"}})
	}
}

// No reflection required.
func BenchmarkConvertToGenericEventStringSlice(b *testing.B) {
	for i := 0; i < b.N; i++ {
		ConvertToGenericEvent(MapStr{"key": []string{"foo", "bar"}})
	}
}

// Uses reflection to convert the string array.
func BenchmarkConvertToGenericEventCustomStringSlice(b *testing.B) {
	type myString string
	for i := 0; i < b.N; i++ {
		ConvertToGenericEvent(MapStr{"key": []myString{"foo", "bar"}})
	}
}

// Pointers require reflection to generically dereference.
func BenchmarkConvertToGenericEventStringPointer(b *testing.B) {
	val := "foo"
	for i := 0; i < b.N; i++ {
		ConvertToGenericEvent(MapStr{"key": &val})
	}
}
func TestDeDotJSON(t *testing.T) {
	var tests = []struct {
		input  []byte
		output []byte
		valuer func() interface{}
	}{
		{
			input: []byte(`[
				{"key_with_dot.1":"value1_1"},
				{"key_without_dot_2":"value1_2"},
				{"key_with_multiple.dots.3": {"key_with_dot.2":"value2_1"}}
			]
			`),
			output: []byte(`[
				{"key_with_dot_1":"value1_1"},
				{"key_without_dot_2":"value1_2"},
				{"key_with_multiple_dots_3": {"key_with_dot_2":"value2_1"}}
			]
			`),
			valuer: func() interface{} { return []interface{}{} },
		},
		{
			input: []byte(`{
				"key_without_dot_l1": {
					"key_with_dot.l2": 1,
					"key.with.multiple.dots_l2": 2,
					"key_without_dot_l2": {
						"key_with_dot.l3": 3,
						"key.with.multiple.dots_l3": 4
					}
				}
			}
			`),
			output: []byte(`{
				"key_without_dot_l1": {
					"key_with_dot_l2": 1,
					"key_with_multiple_dots_l2": 2,
					"key_without_dot_l2": {
						"key_with_dot_l3": 3,
						"key_with_multiple_dots_l3": 4
					}
				}
			}
			`),
			valuer: func() interface{} { return map[string]interface{}{} },
		},
	}
	for _, test := range tests {
		input, output := test.valuer(), test.valuer()
		assert.Nil(t, json.Unmarshal(test.input, &input))
		assert.Nil(t, json.Unmarshal(test.output, &output))
		assert.Equal(t, output, DeDotJSON(input))
		if _, ok := test.valuer().(map[string]interface{}); ok {
			assert.Equal(t, MapStr(output.(map[string]interface{})), DeDotJSON(MapStr(input.(map[string]interface{}))))
		}
	}
}
