// Copyright © 2016 Asteris, LLC
//
// 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 resource_test

import (
	"fmt"
	"reflect"
	"testing"
	"time"

	"github.com/asteris-llc/converge/helpers/fakerenderer"
	"github.com/asteris-llc/converge/helpers/logging"
	"github.com/asteris-llc/converge/resource"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/net/context"
)

// TestPreparerPrepare tests the Unmarshalling of Preparer into Resource
// structs.
func TestPreparerPrepare(t *testing.T) {
	defer logging.HideLogs(t)()

	// newWithField is a little utility. We have this pattern over and over
	// where we need to set a target field and then check that we render without
	// errors. This just encapsulates that logic (and error handling) so we
	// don't have to repeat it so much.
	newWithField := func(t *testing.T, key string, value interface{}) *testPreparerTarget {
		target := new(testPreparerTarget)
		prep := &resource.Preparer{
			Source: map[string]interface{}{
				key: value,
			},
			Destination: target,
		}

		_, err := prep.Prepare(context.Background(), fakerenderer.New())
		require.NoError(t, err, "%v", err)

		return target
	}

	// strings are table stakes, so let's start with those...
	t.Run("string", func(t *testing.T) {
		target := newWithField(t, "string", "a")
		assert.Equal(t, "a", target.String)
	})

	// lists of strings are also important (argument lists, etc)
	t.Run("strings", func(t *testing.T) {
		target := newWithField(t, "strings", []string{"a"})
		assert.Equal(t, []string{"a"}, target.Strings)
	})

	// We're only testing maps with strings and bools in the tested slot, but
	// this should work with any value.
	t.Run("maps", func(t *testing.T) {
		t.Run("string-key", func(t *testing.T) {
			value := map[string]interface{}{"x": 1}
			target := newWithField(t, "stringmapkey", value)
			assert.Equal(t, value, target.StringMapKey)
		})

		t.Run("bool-key", func(t *testing.T) {
			value := map[bool]interface{}{true: 1}
			target := newWithField(t, "boolmapkey", value)
			assert.Equal(t, value, target.BoolMapKey)
		})

		t.Run("string-value", func(t *testing.T) {
			value := map[interface{}]string{1: "x"}
			target := newWithField(t, "stringmapvalue", value)
			assert.Equal(t, value, target.StringMapValue)
		})

		t.Run("bool-value", func(t *testing.T) {
			value := map[interface{}]bool{1: true}
			target := newWithField(t, "boolmapvalue", value)
			assert.Equal(t, value, target.BoolMapValue)
		})
	})

	// test time.Duration with both int64 and string
	t.Run("duration", func(t *testing.T) {
		t.Run("nil", func(t *testing.T) {
			target := newWithField(t, "duration", nil)
			assert.Equal(t, time.Duration(0), target.Duration)
		})

		t.Run("int64", func(t *testing.T) {
			target := newWithField(t, "duration", 1)
			assert.Equal(t, 1*time.Second, target.Duration)
		})

		t.Run("string", func(t *testing.T) {
			duration, err := time.ParseDuration("1h")
			require.NoError(t, err)
			target := newWithField(t, "duration", "1h")
			assert.Equal(t, duration, target.Duration)
		})

		t.Run("invalid", func(t *testing.T) {
			t.Run("string", func(t *testing.T) {
				val := "1"
				prep := &resource.Preparer{
					Source:      map[string]interface{}{"duration": val},
					Destination: new(testPreparerTarget),
				}

				_, err := prep.Prepare(context.Background(), fakerenderer.New())
				assert.EqualError(t, err, fmt.Sprintf("could not convert %s to duration: time: missing unit in duration %s", val, val))
			})

			t.Run("unknown", func(t *testing.T) {
				val := 3.2
				prep := &resource.Preparer{
					Source:      map[string]interface{}{"duration": val},
					Destination: new(testPreparerTarget),
				}

				_, err := prep.Prepare(context.Background(), fakerenderer.New())
				assert.EqualError(t, err, fmt.Sprintf("cannot handle duration conversion of %v", reflect.ValueOf(val).Kind()))
			})
		})
	})

	// test time.Time
	t.Run("time", func(t *testing.T) {
		zone := time.FixedZone(time.Now().In(time.Local).Zone())
		longForm := "2006-01-02T15:04:05"
		shortForm := "2006-01-02"

		t.Run("nil", func(t *testing.T) {
			target := newWithField(t, "time", nil)
			assert.Equal(t, time.Time{}, target.Time)
		})

		t.Run("short form", func(t *testing.T) {
			val := "2003-01-02"
			expected, err := time.ParseInLocation(shortForm, val, zone)
			require.NoError(t, err)
			target := newWithField(t, "time", val)
			assert.Equal(t, expected.String(), target.Time.String())
		})

		t.Run("long form", func(t *testing.T) {
			val := "2003-01-02T01:02:03"
			expected, err := time.ParseInLocation(longForm, val, zone)
			require.NoError(t, err)
			target := newWithField(t, "time", val)
			assert.Equal(t, expected.String(), target.Time.String())
		})

		t.Run("zone provided", func(t *testing.T) {
			val := "2003-01-02T01:02:03-07:00"
			expected, err := time.Parse(time.RFC3339, val)
			require.NoError(t, err)
			target := newWithField(t, "time", val)
			assert.Equal(t, expected.String(), target.Time.String())
		})

		t.Run("invalid", func(t *testing.T) {
			t.Run("unknown", func(t *testing.T) {
				val := "3.2"
				prep := &resource.Preparer{
					Source:      map[string]interface{}{"time": val},
					Destination: new(testPreparerTarget),
				}

				_, ztErr := time.Parse(time.RFC3339, val)
				_, ltErr := time.ParseInLocation(longForm, val, zone)
				_, stErr := time.ParseInLocation(shortForm, val, zone)
				expectedErr := fmt.Errorf("could not convert time to time.Time any of:\n1. %v\n2. %v\n3. %v\n", ztErr, ltErr, stErr)
				_, err := prep.Prepare(context.Background(), fakerenderer.New())
				assert.Equal(t, expectedErr, err)
			})

			t.Run("short form", func(t *testing.T) {
				val := "2003-01-"
				prep := &resource.Preparer{
					Source:      map[string]interface{}{"time": val},
					Destination: new(testPreparerTarget),
				}

				_, ztErr := time.Parse(time.RFC3339, val)
				_, ltErr := time.ParseInLocation(longForm, val, zone)
				_, stErr := time.ParseInLocation(shortForm, val, zone)
				expectedErr := fmt.Errorf("could not convert time to time.Time any of:\n1. %v\n2. %v\n3. %v\n", ztErr, ltErr, stErr)
				_, err := prep.Prepare(context.Background(), fakerenderer.New())
				assert.Equal(t, expectedErr, err)
			})

			t.Run("long form-second out of range", func(t *testing.T) {
				val := "2003-01-02T01:02:61"
				prep := &resource.Preparer{
					Source:      map[string]interface{}{"time": val},
					Destination: new(testPreparerTarget),
				}

				_, ztErr := time.Parse(time.RFC3339, val)
				_, ltErr := time.ParseInLocation(longForm, val, zone)
				_, stErr := time.ParseInLocation(shortForm, val, zone)
				expectedErr := fmt.Errorf("could not convert time to time.Time any of:\n1. %v\n2. %v\n3. %v\n", ztErr, ltErr, stErr)
				_, err := prep.Prepare(context.Background(), fakerenderer.New())
				assert.Equal(t, expectedErr, err)
			})

			t.Run("zone provided-extra text", func(t *testing.T) {
				val := "2003-01-02T01:02:03-25:00:00"
				prep := &resource.Preparer{
					Source:      map[string]interface{}{"time": val},
					Destination: new(testPreparerTarget),
				}

				_, ztErr := time.Parse(time.RFC3339, val)
				_, ltErr := time.ParseInLocation(longForm, val, zone)
				_, stErr := time.ParseInLocation(shortForm, val, zone)
				expectedErr := fmt.Errorf("could not convert time to time.Time any of:\n1. %v\n2. %v\n3. %v\n", ztErr, ltErr, stErr)
				_, err := prep.Prepare(context.Background(), fakerenderer.New())
				assert.Equal(t, expectedErr, err)
			})
		})
	})

	// boolean values are special. We want to support a bunch of different cases
	// and forms of truth values, so we're going to test them all in a table
	// here.
	t.Run("bool", func(t *testing.T) {
		truthTable := []struct {
			val   interface{}
			truth bool
		}{
			// true - any casing of "true" or "t", or the boolean value
			{true, true},
			{"true", true},
			{"TRUE", true},
			{"t", true},
			{"T", true},

			// false
			{false, false},
			{"false", false},
			{"FALSE", false},
			{"f", false},
			{"F", false},
			{"bananas", false}, // or anything other string except as defined above
		}

		for _, pair := range truthTable {
			t.Run(fmt.Sprintf("%T-%v", pair.val, pair.val), func(t *testing.T) {
				target := newWithField(t, "bool", pair.val)
				assert.Equal(t, pair.truth, target.Bool)
			})

			t.Run(fmt.Sprintf("slice-of-%T-%v", pair.val, pair.val), func(t *testing.T) {
				target := newWithField(t, "bools", []interface{}{pair.val})
				assert.Equal(t, []bool{pair.truth}, target.Bools)
			})
		}
	})

	// next is our "anything" escape hatch of interface{}.
	t.Run("interface", func(t *testing.T) {
		target := newWithField(t, "anything", 1)
		assert.Equal(t, 1, target.Anything)
	})

	// numbers! We want to be able to parse either a numeric value or a string.
	numberTests := []struct {
		key   string
		value interface{}
		test  func(*testing.T, *testPreparerTarget)
	}{
		// ints
		{"int", 1, func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, 1, tpt.Int) }},
		{"int", "1", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, 1, tpt.Int) }},

		{"int8", int8(1), func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, int8(1), tpt.Int8) }},
		{"int8", "1", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, int8(1), tpt.Int8) }},

		{"int16", int16(1), func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, int16(1), tpt.Int16) }},
		{"int16", "1", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, int16(1), tpt.Int16) }},

		{"int32", int32(1), func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, int32(1), tpt.Int32) }},
		{"int32", "1", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, int32(1), tpt.Int32) }},

		{"int64", int64(1), func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, int64(1), tpt.Int64) }},
		{"int64", "1", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, int64(1), tpt.Int64) }},

		// uints
		{"uint", 1, func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, uint(1), tpt.Uint) }},
		{"uint", "1", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, uint(1), tpt.Uint) }},

		{"uint8", uint8(1), func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, uint8(1), tpt.Uint8) }},
		{"uint8", "1", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, uint8(1), tpt.Uint8) }},

		{"uint16", uint16(1), func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, uint16(1), tpt.Uint16) }},
		{"uint16", "1", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, uint16(1), tpt.Uint16) }},

		{"uint32", uint32(1), func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, uint32(1), tpt.Uint32) }},
		{"uint32", "1", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, uint32(1), tpt.Uint32) }},

		{"uint64", uint64(1), func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, uint64(1), tpt.Uint64) }},
		{"uint64", "1", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, uint64(1), tpt.Uint64) }},

		// floats
		{"float32", float32(1), func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, float32(1), tpt.Float32) }},
		{"float32", "1", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, float32(1), tpt.Float32) }},

		{"float64", float64(1), func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, float64(1), tpt.Float64) }},
		{"float64", "1", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, float64(1), tpt.Float64) }},

		// octal conversion
		{"octal", uint32(0777), func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, uint32(0777), tpt.Octal) }},
		{"octal", "0777", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, uint32(0777), tpt.Octal) }},
	}
	for _, test := range numberTests {
		t.Run(fmt.Sprintf("%s-%T", test.key, test.value), func(t *testing.T) {
			test.test(t, newWithField(t, test.key, test.value))
		})
	}

	// pointers
	t.Run("pointers", func(t *testing.T) {
		val := "test"
		target := newWithField(t, "pointer", val)
		if assert.NotNil(t, target.Pointer) {
			assert.Equal(t, *target.Pointer, val)
		}
	})

	// we do some very basic validations, let's test those too
	t.Run("valid_values", func(t *testing.T) {
		t.Run("valid", func(t *testing.T) {
			target := newWithField(t, "valid_values", "a")
			assert.Equal(t, "a", target.ValidValues)
		})

		t.Run("invalid", func(t *testing.T) {
			prep := &resource.Preparer{
				Source:      map[string]interface{}{"valid_values": "invalid"},
				Destination: new(testPreparerTarget),
			}

			_, err := prep.Prepare(context.Background(), fakerenderer.New())
			assert.EqualError(t, err, "value did not pass validation. Must be one of \"a\", was \"invalid\"")
		})
	})

	// type aliases are important for enum-like behavior
	t.Run("alias", func(t *testing.T) {
		target := newWithField(t, "alias", "a")
		assert.Equal(t, testAlias("a"), target.Alias)
	})

	// parameters can be required
	t.Run("required", func(t *testing.T) {
		t.Run("valid", func(t *testing.T) {
			prep := &resource.Preparer{
				Source:      map[string]interface{}{"required": "a"},
				Destination: new(testRequiredTarget),
			}

			_, err := prep.Prepare(context.Background(), fakerenderer.New())
			assert.NoError(t, err)
		})

		t.Run("invalid", func(t *testing.T) {
			prep := &resource.Preparer{
				Source:      map[string]interface{}{},
				Destination: new(testRequiredTarget),
			}

			_, err := prep.Prepare(context.Background(), fakerenderer.New())
			assert.EqualError(t, err, `"required" is required`)
		})
	})

	// parameters can be required to be nonempty
	t.Run("nonempty", func(t *testing.T) {
		t.Run("valid", func(t *testing.T) {
			prep := &resource.Preparer{
				Source:      map[string]interface{}{"nonempty": "a"},
				Destination: new(testNonemptyTarget),
			}

			_, err := prep.Prepare(context.Background(), fakerenderer.New())
			assert.NoError(t, err)
		})

		t.Run("invalid", func(t *testing.T) {
			prep := &resource.Preparer{
				Source:      map[string]interface{}{"nonempty": ""},
				Destination: new(testNonemptyTarget),
			}

			_, err := prep.Prepare(context.Background(), fakerenderer.New())
			assert.EqualError(t, err, `"nonempty" must be nonempty`)
		})
	})

	// two parameters can also be mutually exclusive
	t.Run("mutually_exclusive", func(t *testing.T) {
		t.Run("invalid", func(t *testing.T) {
			prep := &resource.Preparer{
				Source: map[string]interface{}{
					"a": 1,
					"b": 2,
				},
				Destination: new(testMutuallyExclusiveTarget),
			}

			_, err := prep.Prepare(context.Background(), fakerenderer.New())
			assert.EqualError(t, err, `only one of "a" or "b" can be set`)
		})
	})
}

// testAlias is a type alias... can we deserialize those?
type testAlias string

// testPreparerTarget is a big 'ol bucket for tested values. See comments in
// TestPreparerPrepare for how these are being used.
type testPreparerTarget struct {
	String         string                 `hcl:"string"`
	Strings        []string               `hcl:"strings"`
	StringMapKey   map[string]interface{} `hcl:"stringmapkey"`
	StringMapValue map[interface{}]string `hcl:"stringmapvalue"`

	Duration time.Duration `hcl:"duration"`

	Time time.Time `hcl:"time"`

	Bool         bool                 `hcl:"bool"`
	Bools        []bool               `hcl:"bools"`
	BoolMapKey   map[bool]interface{} `hcl:"boolmapkey"`
	BoolMapValue map[interface{}]bool `hcl:"boolmapvalue"`

	Anything interface{} `hcl:"anything"`

	// one of each numeric type
	Int     int     `hcl:"int"`
	Int8    int8    `hcl:"int8"`
	Int16   int16   `hcl:"int16"`
	Int32   int32   `hcl:"int32"`
	Int64   int64   `hcl:"int64"`
	Uint    uint    `hcl:"uint"`
	Uint8   uint8   `hcl:"uint8"`
	Uint16  uint16  `hcl:"uint16"`
	Uint32  uint32  `hcl:"uint32"`
	Uint64  uint64  `hcl:"uint64"`
	Float32 float32 `hcl:"float32"`
	Float64 float64 `hcl:"float64"`
	Octal   uint32  `hcl:"octal" base:"8"`

	// simple validation
	ValidValues string `hcl:"valid_values" valid_values:"a"`

	// aliasing
	Alias testAlias `hcl:"alias"`

	// pointers
	Pointer *string `hcl:"pointer"`
}

func (tpt *testPreparerTarget) Prepare(context.Context, resource.Renderer) (resource.Task, error) {
	return tpt, nil
}
func (tpt *testPreparerTarget) Check(context.Context, resource.Renderer) (resource.TaskStatus, error) {
	return nil, nil
}
func (tpt *testPreparerTarget) Apply(context.Context) (resource.TaskStatus, error) { return nil, nil }

// testRequiredTarget tests required fields. Those are invalid when empty, so
// we've got to include it separately
type testRequiredTarget struct {
	Required string `hcl:"required" required:"true"`
}

func (tpt *testRequiredTarget) Prepare(context.Context, resource.Renderer) (resource.Task, error) {
	return tpt, nil
}
func (tpt *testRequiredTarget) Check(context.Context, resource.Renderer) (resource.TaskStatus, error) {
	return nil, nil
}
func (tpt *testRequiredTarget) Apply(context.Context) (resource.TaskStatus, error) { return nil, nil }

// testNonemptyTarget tests nonempty fields. Those are invalid when empty, so
// we've got to include it separately
type testNonemptyTarget struct {
	Nonempty string `hcl:"nonempty" nonempty:"true"`
}

func (tpt *testNonemptyTarget) Prepare(context.Context, resource.Renderer) (resource.Task, error) {
	return tpt, nil
}
func (tpt *testNonemptyTarget) Check(context.Context, resource.Renderer) (resource.TaskStatus, error) {
	return nil, nil
}
func (tpt *testNonemptyTarget) Apply(context.Context) (resource.TaskStatus, error) { return nil, nil }

// testMutuallyExclusiveTarget tests mutually_exclusive fields. Those are
// invalid when empty, so we've got to include it separately
type testMutuallyExclusiveTarget struct {
	A string `hcl:"a" mutually_exclusive:"a,b"`
	B string `hcl:"b" mutually_exclusive:"a,b"`
}

func (tpt *testMutuallyExclusiveTarget) Prepare(context.Context, resource.Renderer) (resource.Task, error) {
	return tpt, nil
}
func (tpt *testMutuallyExclusiveTarget) Check(context.Context, resource.Renderer) (resource.TaskStatus, error) {
	return nil, nil
}
func (tpt *testMutuallyExclusiveTarget) Apply(context.Context) (resource.TaskStatus, error) {
	return nil, nil
}
