package revel

import (
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"reflect"
	"sort"
	"strings"
	"testing"
	"time"
)

type A struct {
	Id      int
	Name    string
	B       B
	private int
}

type B struct {
	Extra string
}

var (
	PARAMS = map[string][]string{
		"int":             {"1"},
		"int8":            {"1"},
		"int16":           {"1"},
		"int32":           {"1"},
		"int64":           {"1"},
		"uint":            {"1"},
		"uint8":           {"1"},
		"uint16":          {"1"},
		"uint32":          {"1"},
		"uint64":          {"1"},
		"float32":         {"1.000000"},
		"float64":         {"1.000000"},
		"str":             {"hello"},
		"bool-true":       {"true"},
		"bool-1":          {"1"},
		"bool-on":         {"on"},
		"bool-false":      {"false"},
		"bool-0":          {"0"},
		"bool-off":        {""},
		"date":            {"1982-07-09"},
		"datetime":        {"1982-07-09 21:30"},
		"customDate":      {"07/09/1982"},
		"arr[0]":          {"1"},
		"arr[1]":          {"2"},
		"arr[3]":          {"3"},
		"uarr[]":          {"1", "2"},
		"arruarr[0][]":    {"1", "2"},
		"arruarr[1][]":    {"3", "4"},
		"2darr[0][0]":     {"0"},
		"2darr[0][1]":     {"1"},
		"2darr[1][0]":     {"10"},
		"2darr[1][1]":     {"11"},
		"A.Id":            {"123"},
		"A.Name":          {"rob"},
		"B.Id":            {"123"},
		"B.Name":          {"rob"},
		"B.B.Extra":       {"hello"},
		"pB.Id":           {"123"},
		"pB.Name":         {"rob"},
		"pB.B.Extra":      {"hello"},
		"priv.private":    {"123"},
		"arrC[0].Id":      {"5"},
		"arrC[0].Name":    {"rob"},
		"arrC[0].B.Extra": {"foo"},
		"arrC[1].Id":      {"8"},
		"arrC[1].Name":    {"bill"},
		"m[a]":            {"foo"},
		"m[b]":            {"bar"},
		"m2[1]":           {"foo"},
		"m2[2]":           {"bar"},
		"m3[a]":           {"1"},
		"m3[b]":           {"2"},
		"invalidInt":      {"xyz"},
		"invalidInt2":     {""},
		"invalidBool":     {"xyz"},
		"invalidArr":      {"xyz"},
		"int8-overflow":   {"1024"},
		"uint8-overflow":  {"1024"},
	}

	testDate     = time.Date(1982, time.July, 9, 0, 0, 0, 0, time.UTC)
	testDatetime = time.Date(1982, time.July, 9, 21, 30, 0, 0, time.UTC)
)

var binderTestCases = map[string]interface{}{
	"int":        1,
	"int8":       int8(1),
	"int16":      int16(1),
	"int32":      int32(1),
	"int64":      int64(1),
	"uint":       1,
	"uint8":      uint8(1),
	"uint16":     uint16(1),
	"uint32":     uint32(1),
	"uint64":     uint64(1),
	"float32":    float32(1.0),
	"float64":    float64(1.0),
	"str":        "hello",
	"bool-true":  true,
	"bool-1":     true,
	"bool-on":    true,
	"bool-false": false,
	"bool-0":     false,
	"bool-off":   false,
	"date":       testDate,
	"datetime":   testDatetime,
	"customDate": testDate,
	"arr":        []int{1, 2, 0, 3},
	"uarr":       []int{1, 2},
	"arruarr":    [][]int{{1, 2}, {3, 4}},
	"2darr":      [][]int{{0, 1}, {10, 11}},
	"A":          A{Id: 123, Name: "rob"},
	"B":          A{Id: 123, Name: "rob", B: B{Extra: "hello"}},
	"pB":         &A{Id: 123, Name: "rob", B: B{Extra: "hello"}},
	"arrC": []A{
		{
			Id:   5,
			Name: "rob",
			B:    B{"foo"},
		},
		{
			Id:   8,
			Name: "bill",
		},
	},
	"m":  map[string]string{"a": "foo", "b": "bar"},
	"m2": map[int]string{1: "foo", 2: "bar"},
	"m3": map[string]int{"a": 1, "b": 2},

	// TODO: Tests that use TypeBinders

	// Invalid value tests (the result should always be the zero value for that type)
	// The point of these is to ensure that invalid user input does not cause panics.
	"invalidInt":     0,
	"invalidInt2":    0,
	"invalidBool":    false,
	"invalidArr":     []int{},
	"priv":           A{},
	"int8-overflow":  int8(0),
	"uint8-overflow": uint8(0),
}

func init() {
	DateFormat = DEFAULT_DATE_FORMAT
	DateTimeFormat = DEFAULT_DATETIME_FORMAT
	TimeFormats = append(TimeFormats, DEFAULT_DATE_FORMAT, DEFAULT_DATETIME_FORMAT, "01/02/2006")
}

// Types that files may be bound to, and a func that can read the content from
// that type.
// TODO: Is there any way to create a slice, given only the element Type?
var fileBindings = []struct{ val, arrval, f interface{} }{
	{(**os.File)(nil), []*os.File{}, ioutil.ReadAll},
	{(*[]byte)(nil), [][]byte{}, func(b []byte) []byte { return b }},
	{(*io.Reader)(nil), []io.Reader{}, ioutil.ReadAll},
	{(*io.ReadSeeker)(nil), []io.ReadSeeker{}, ioutil.ReadAll},
}

func TestBinder(t *testing.T) {
	// Reuse the mvc_test.go multipart request to test the binder.
	params := &Params{}
	ParseParams(params, NewRequest(getMultipartRequest()))
	params.Values = PARAMS

	// Values
	for k, v := range binderTestCases {
		actual := Bind(params, k, reflect.TypeOf(v))
		expected := reflect.ValueOf(v)
		valEq(t, k, actual, expected)
	}

	// Files

	// Get the keys in sorted order to make the expectation right.
	keys := []string{}
	for k, _ := range expectedFiles {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	expectedBoundFiles := make(map[string][]fh)
	for _, k := range keys {
		fhs := expectedFiles[k]
		k := nextKey(k)
		expectedBoundFiles[k] = append(expectedBoundFiles[k], fhs...)
	}

	for k, fhs := range expectedBoundFiles {

		if len(fhs) == 1 {
			// Test binding single files to: *os.File, []byte, io.Reader, io.ReadSeeker
			for _, binding := range fileBindings {
				typ := reflect.TypeOf(binding.val).Elem()
				actual := Bind(params, k, typ)
				if !actual.IsValid() || (actual.Kind() == reflect.Interface && actual.IsNil()) {
					t.Errorf("%s (%s) - Returned nil.", k, typ)
					continue
				}
				returns := reflect.ValueOf(binding.f).Call([]reflect.Value{actual})
				valEq(t, k, returns[0], reflect.ValueOf(fhs[0].content))
			}

		} else {
			// Test binding multi to:
			// []*os.File, [][]byte, []io.Reader, []io.ReadSeeker
			for _, binding := range fileBindings {
				typ := reflect.TypeOf(binding.arrval)
				actual := Bind(params, k, typ)
				if actual.Len() != len(fhs) {
					t.Fatalf("%s (%s) - Number of files: (expected) %d != %d (actual)",
						k, typ, len(fhs), actual.Len())
				}
				for i, _ := range fhs {
					returns := reflect.ValueOf(binding.f).Call([]reflect.Value{actual.Index(i)})
					if !returns[0].IsValid() {
						t.Errorf("%s (%s) - Returned nil.", k, typ)
						continue
					}
					valEq(t, k, returns[0], reflect.ValueOf(fhs[i].content))
				}
			}
		}
	}
}

// Unbinding tests

var unbinderTestCases = map[string]interface{}{
	"int":        1,
	"int8":       int8(1),
	"int16":      int16(1),
	"int32":      int32(1),
	"int64":      int64(1),
	"uint":       1,
	"uint8":      uint8(1),
	"uint16":     uint16(1),
	"uint32":     uint32(1),
	"uint64":     uint64(1),
	"float32":    float32(1.0),
	"float64":    float64(1.0),
	"str":        "hello",
	"bool-true":  true,
	"bool-false": false,
	"date":       testDate,
	"datetime":   testDatetime,
	"arr":        []int{1, 2, 0, 3},
	"2darr":      [][]int{{0, 1}, {10, 11}},
	"A":          A{Id: 123, Name: "rob"},
	"B":          A{Id: 123, Name: "rob", B: B{Extra: "hello"}},
	"pB":         &A{Id: 123, Name: "rob", B: B{Extra: "hello"}},
	"arrC": []A{
		{
			Id:   5,
			Name: "rob",
			B:    B{"foo"},
		},
		{
			Id:   8,
			Name: "bill",
		},
	},
	"m":  map[string]string{"a": "foo", "b": "bar"},
	"m2": map[int]string{1: "foo", 2: "bar"},
	"m3": map[string]int{"a": 1, "b": 2},
}

// Some of the unbinding results are not exactly what is in PARAMS, since it
// serializes implicit zero values explicitly.
var unbinderOverrideAnswers = map[string]map[string]string{
	"arr": map[string]string{
		"arr[0]": "1",
		"arr[1]": "2",
		"arr[2]": "0",
		"arr[3]": "3",
	},
	"A": map[string]string{
		"A.Id":      "123",
		"A.Name":    "rob",
		"A.B.Extra": "",
	},
	"arrC": map[string]string{
		"arrC[0].Id":      "5",
		"arrC[0].Name":    "rob",
		"arrC[0].B.Extra": "foo",
		"arrC[1].Id":      "8",
		"arrC[1].Name":    "bill",
		"arrC[1].B.Extra": "",
	},
	"m":  map[string]string{"m[a]": "foo", "m[b]": "bar"},
	"m2": map[string]string{"m2[1]": "foo", "m2[2]": "bar"},
	"m3": map[string]string{"m3[a]": "1", "m3[b]": "2"},
}

func TestUnbinder(t *testing.T) {
	for k, v := range unbinderTestCases {
		actual := make(map[string]string)
		Unbind(actual, k, v)

		// Get the expected key/values.
		expected, ok := unbinderOverrideAnswers[k]
		if !ok {
			expected = make(map[string]string)
			for k2, v2 := range PARAMS {
				if k == k2 || strings.HasPrefix(k2, k+".") || strings.HasPrefix(k2, k+"[") {
					expected[k2] = v2[0]
				}
			}
		}

		// Compare length and values.
		if len(actual) != len(expected) {
			t.Errorf("Length mismatch\nExpected length %d, actual %d\nExpected: %s\nActual: %s",
				len(expected), len(actual), expected, actual)
		}
		for k, v := range actual {
			if expected[k] != v {
				t.Errorf("Value mismatch.\nExpected: %s\nActual: %s", expected, actual)
			}
		}
	}
}

// Helpers

func valEq(t *testing.T, name string, actual, expected reflect.Value) {
	switch expected.Kind() {
	case reflect.Slice:
		// Check the type/length/element type
		if !eq(t, name+" (type)", actual.Kind(), expected.Kind()) ||
			!eq(t, name+" (len)", actual.Len(), expected.Len()) ||
			!eq(t, name+" (elem)", actual.Type().Elem(), expected.Type().Elem()) {
			return
		}

		// Check value equality for each element.
		for i := 0; i < actual.Len(); i++ {
			valEq(t, fmt.Sprintf("%s[%d]", name, i), actual.Index(i), expected.Index(i))
		}

	case reflect.Ptr:
		// Check equality on the element type.
		valEq(t, name, actual.Elem(), expected.Elem())
	case reflect.Map:
		if !eq(t, name+" (len)", actual.Len(), expected.Len()) {
			return
		}
		for _, key := range expected.MapKeys() {
			expectedValue := expected.MapIndex(key)
			actualValue := actual.MapIndex(key)
			if actualValue.IsValid() {
				valEq(t, fmt.Sprintf("%s[%s]", name, key), actualValue, expectedValue)
			} else {
				t.Errorf("Expected key %s not found", key)
			}
		}
	default:
		eq(t, name, actual.Interface(), expected.Interface())
	}
}
