// Copyright (c) 2017 Intel Corporation
//
// 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 tfortools

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"math/rand"
	"reflect"
	"strings"
	"testing"
	"text/template"
)

type testint int

var templateUsageTests = []struct {
	obj      interface{}
	expected string
}{
	{int(0), "int"},
	{testint(0), "tfortools.testint"},
	{[]int{}, "[]int"},
	{false, "bool"},
	{[5]int{}, "[5]int"},
	{func(int) (int, error) { return 0, nil }, "func(int) (int, error)"},
	{"", "string"},
	{struct {
		X       int
		Y       string
		hidden  float64
		Invalid chan int
		Empty   struct{}
	}{}, "struct {\nX int\nY string\nEmpty struct {\n}\n}"},
	{map[string]struct{ X int }{}, "map[string]struct {\nX int\n}"},
	{map[string]struct{ x int }{}, "map[string]struct {\n}"},
	{struct {
		hidden int
		Embed  struct {
			X int
		}
	}{}, "struct {\nEmbed struct {\nX int\n}\n}"},
	{struct{ hidden int }{}, "struct {\n}"},
	{struct{ Empty struct{} }{}, "struct { Empty struct{\n}}"},
	{struct{}{}, "struct {\n}"},
	{struct {
		X int            `test:"tag" tfortools:"It's " tfortools:"an \"int\""`
		Y []int          `test:"tag"`
		Z map[string]int `test:"tag"`
		B struct {
			A int `tfortools:"Another int"`
		} `test:"tag"`
	}{}, "struct { X int `test:\"tag\"` // It's an \"int\"\n Y []int `test:\"tag\"`; Z map[string]int `test:\"tag\"`; B struct {\nA int // Another int\n} `test:\"tag\"`} "},
	{[]struct {
		X int
		Y string
	}{}, "[]struct { X int; Y string}"},
	{[]*struct {
		X int
		Y string
	}{}, "[]*struct { X int; Y string}"},
}

// Check GenerateUsageUndecorated generates the correct output
//
// Call GenerateUsageUndecorated on a variety of different objects including
// ints, slices, structs, structs with tags, structs with private members, etc.
//
// The expected output should be generated by each test.
func TestTemplateGenerateUsage(t *testing.T) {
	for _, s := range templateUsageTests {
		gen := GenerateUsageUndecorated(s.obj)
		var buf bytes.Buffer
		formatType(&buf, []byte(s.expected))
		trimmedGen := strings.TrimSpace(gen)
		trimmedExpected := strings.TrimSpace(buf.String())
		if trimmedGen != trimmedExpected {
			t.Errorf("Bad template usage. Found\n%s, expected\n%s",
				trimmedGen, trimmedExpected)
		}
	}
	fmt.Println()
}

// Verify that help text is generated correctly when using a Config option
//
// Check that all the following generated help strings are equal:
// 1. Help generated using a nil Config
// 2. Help generated using a Config object created with OptAllFNs
// 3. Help generated using a Config object created with all the available options.
// 4. Help generated using a Config object created by specifying all the available options twice.
// 5. Help generated using a Config object created by specifying all the available options in a
// different sort order.
func TestOptions(t *testing.T) {
	nilHelp := GenerateUsageDecorated("-f", 0, nil)
	cfgAllHelp := GenerateUsageDecorated("-f", 0, NewConfig(OptAllFns))

	if nilHelp != cfgAllHelp {
		t.Errorf("Default help message and message generated with a Config object do not match: %s %s",
			nilHelp, cfgAllHelp)
	}

	opts := []func(*Config){
		OptFilter,
		OptFilterContains,
		OptFilterHasPrefix,
		OptFilterHasSuffix,
		OptFilterFolded,
		OptFilterRegexp,
		OptToJSON,
		OptToCSV,
		OptSelect,
		OptSelectAlt,
		OptTable,
		OptTableAlt,
		OptHTable,
		OptHTableAlt,
		OptTableX,
		OptTableXAlt,
		OptHTableX,
		OptHTableXAlt,
		OptCols,
		OptSort,
		OptRows,
		OptHead,
		OptTail,
		OptDescribe,
		OptPromote,
		OptSliceof,
		OptToTable,
	}

	// Check that specifying an option twice does not lead to duplicate help.

	cfgDoubleHelp := GenerateUsageDecorated("-f", 0, NewConfig(append(opts, opts...)...))
	if nilHelp != cfgDoubleHelp {
		t.Errorf("Default help message and message generated with options do not match: %s %s",
			nilHelp, cfgDoubleHelp)
	}

	for range opts {
		x, y := rand.Intn(len(opts)), rand.Intn(len(opts))
		opts[x], opts[y] = opts[y], opts[x]
	}

	cfgReorderedHelp := GenerateUsageDecorated("-f", 0, NewConfig(opts...))
	if nilHelp != cfgReorderedHelp {
		t.Errorf("Default help message and message generated with randomly ordered options do not match: %s %s",
			nilHelp, cfgDoubleHelp)
	}
}

// Check that OptAllFns makes a copy of the globals it uses
//
// Store the old length of funcMap and funcHelpSlice, create a new Config
// object using OptAllFNs, add a new custom function, and then recheck the
// lengths of the two global variables.
//
// The lengths of the global variables should not have changed.
func TestOptAllFns(t *testing.T) {
	oldMapLen := len(funcMap)
	oldSliceLen := len(funcHelpSlice)
	cfg := NewConfig(OptAllFns)
	err := cfg.AddCustomFn(func() int {
		return 0
	}, "zero", "- zero \"Returns\" zero")
	if err != nil {
		t.Errorf("AddCustomFn failed with err : %v", err)
	}

	if oldMapLen != len(funcMap) {
		t.Errorf("Global funcmap should not be modified")
	}
	if oldSliceLen != len(funcHelpSlice) {
		t.Errorf("Global funcHelpSlice should not be modified")
	}
}

// Check an error is returned when cols is used incorrectly.
//
// Invoke the cols function with an invalid parameter and check
// the error returned.
//
// OutputToTemplate should not panic and a template.ExecError should
// be returned.  The Name field of the error should be "cols".
func TestBadCols(t *testing.T) {
	data := []struct{ FirstName, MiddleName, Surname string }{}
	script := `{{cols . "Age"}}`
	err := OutputToTemplate(ioutil.Discard, "cols", script, data, nil)
	if err == nil {
		t.Errorf("Error expected")
	}
	terr := err.(template.ExecError)
	if terr.Name != "cols" {
		t.Errorf("terr.Name should be cols")
	}
}

// Test tfortools functions work with slice of pointers to structures
//
// Sort a slice of pointers to structs, extract two columns, filter by surname,
// check one of the values, and generate a table.
//
// The sort, cols and table function should work fine.  The data extracted
// should match what is expected.
func TestSliceOfPointers(t *testing.T) {
	data := []*struct{ FirstName, MiddleName, Surname string }{
		{"Marcus", "Tullius", "Cicero"},
		{"Gaius", "Julius", "Caesar"},
		{"Marcus", "Licinius", "Crassus"},
	}
	script1 := `
{{- with cols (sort . "MiddleName") "FirstName" "Surname"}}
  {{- select (filter . "Surname" "Caesar") "FirstName"}}
{{- end}}`
	script2 := `{{table .}}`

	var b bytes.Buffer
	err := OutputToTemplate(&b, "indirect1", script1, data, nil)
	if err != nil {
		t.Errorf("Unexpected error processing slice of pointers to structs: %v",
			err)
	}
	found := strings.TrimSpace(b.String())
	if found != "Gaius" {
		t.Errorf("Expected Gaius got %s", found)
	}

	b.Reset()
	err = OutputToTemplate(ioutil.Discard, "indirect2", script2, data, nil)
	if err != nil {
		t.Errorf("Unable to generate a table of pointers to structs: %v", err)
	}
}

func testToTableInvalid(t *testing.T, data [][]string) {
	defer func() {
		err := recover()
		if err == nil {
			t.Errorf("Expected totable %v to fail", data)
		} else if _, ok := err.(template.ExecError); !ok {
			t.Errorf("Unexpected error type %T", err)
		}
	}()

	_ = toTable(data)
}

func TestToTable(t *testing.T) {
	data := [][]string{
		{"lowercase", " Contains Spaces ", "*invalid_char", "1_num_start"},
		{"A", "B", "10", "10.5"},
	}
	fieldNames := []string{"Lowercase", "Contains_Spaces", "X_invalid_char", "X1_num_start"}
	fieldTypes := []reflect.Type{reflect.TypeOf(""), reflect.TypeOf(""), reflect.TypeOf(10),
		reflect.TypeOf(10.5)}
	res := toTable(data)
	if res == nil {
		t.Errorf("Expected slice of structures, got 0 element slice")
	}
	typ := reflect.ValueOf(res).Index(0).Type()
	for i, fname := range fieldNames {
		field := typ.Field(i)
		if field.Name != fname {
			t.Errorf("Unexpected structure name, wanted %s got %s",
				fname, field.Name)
		}
		if field.Type != fieldTypes[i] {
			t.Errorf("Unexpected field type, wanted %s got %s",
				fieldTypes[i], field.Type)
		}
	}

	// Test invalid input

	testToTableInvalid(t, [][]string{
		{"lowercase", " Contains Spaces ", "*invalid_char", "1_num_start"},
		{"A", "B", "10", "10.5"},
		{"A", "B", "i'm a string", "10.5"},
	})

	testToTableInvalid(t, nil)

	testToTableInvalid(t, [][]string{
		{"lowercase", "lowercase", "*invalid_char", "1_num_start"},
		{"A", "B", "10", "10.5"},
		{"A", "B", "i'm a string", "10.5"},
	})

	testToTableInvalid(t, [][]string{
		{"", "lowercase", "*invalid_char", "1_num_start"},
		{"A", "B", "10", "10.5"},
		{"A", "B", "i'm a string", "10.5"},
	})

}

type testStruct struct{}

func (t *testStruct) DoSomething() {

}

func (t *testStruct) Get1() string {
	return ""
}

func (t *testStruct) Get2() (string, string) {
	return "", ""
}

func TestDescribeMethods(t *testing.T) {
	var buf bytes.Buffer

	expected := `tfortools.testStruct

Methods:

DoSomething()
Get1() string
Get2() (string, string)
`
	if err := OutputToTemplate(&buf, "names", `{{describe .}}`, testStruct{}, nil); err != nil {
		panic(err)
	}

	if buf.String() != expected {
		t.Errorf("Expected\n%s\ngot\n%s", expected, buf.String())
	}
}
