package schema

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

	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)

func TestResourceTimeout_ConfigDecode_badkey(t *testing.T) {
	cases := []struct {
		Name string
		// what the resource has defined in source
		ResourceDefaultTimeout *ResourceTimeout
		// configuration provider by user in tf file
		Config map[string]interface{}
		// what we expect the parsed ResourceTimeout to be
		Expected *ResourceTimeout
		// Should we have an error (key not defined in source)
		ShouldErr bool
	}{
		{
			Name:                   "Source does not define 'delete' key",
			ResourceDefaultTimeout: timeoutForValues(10, 0, 5, 0, 0),
			Config:                 expectedConfigForValues(2, 0, 0, 1, 0),
			Expected:               timeoutForValues(10, 0, 5, 0, 0),
			ShouldErr:              true,
		},
		{
			Name:                   "Config overrides create",
			ResourceDefaultTimeout: timeoutForValues(10, 0, 5, 0, 0),
			Config:                 expectedConfigForValues(2, 0, 7, 0, 0),
			Expected:               timeoutForValues(2, 0, 7, 0, 0),
			ShouldErr:              false,
		},
		{
			Name:                   "Config overrides create, default provided. Should still have zero values",
			ResourceDefaultTimeout: timeoutForValues(10, 0, 5, 0, 3),
			Config:                 expectedConfigForValues(2, 0, 7, 0, 0),
			Expected:               timeoutForValues(2, 0, 7, 0, 3),
			ShouldErr:              false,
		},
		{
			Name:                   "Use something besides 'minutes'",
			ResourceDefaultTimeout: timeoutForValues(10, 0, 5, 0, 3),
			Config: map[string]interface{}{
				"create": "2h",
			},
			Expected:  timeoutForValues(120, 0, 5, 0, 3),
			ShouldErr: false,
		},
	}

	for i, c := range cases {
		t.Run(fmt.Sprintf("%d-%s", i, c.Name), func(t *testing.T) {
			r := &Resource{
				Timeouts: c.ResourceDefaultTimeout,
			}

			conf := terraform.NewResourceConfigRaw(
				map[string]interface{}{
					"foo":             "bar",
					TimeoutsConfigKey: c.Config,
				},
			)

			timeout := &ResourceTimeout{}
			decodeErr := timeout.ConfigDecode(r, conf)
			if c.ShouldErr {
				if decodeErr == nil {
					t.Fatalf("ConfigDecode case (%d): Expected bad timeout key: %s", i, decodeErr)
				}
				// should error, err was not nil, continue
				return
			} else {
				if decodeErr != nil {
					// should not error, error was not nil, fatal
					t.Fatalf("decodeError was not nil: %s", decodeErr)
				}
			}

			if !reflect.DeepEqual(c.Expected, timeout) {
				t.Fatalf("ConfigDecode match error case (%d).\nExpected:\n%#v\nGot:\n%#v", i, c.Expected, timeout)
			}
		})
	}
}

func TestResourceTimeout_ConfigDecode(t *testing.T) {
	r := &Resource{
		Timeouts: &ResourceTimeout{
			Create: DefaultTimeout(10 * time.Minute),
			Update: DefaultTimeout(5 * time.Minute),
		},
	}

	c := terraform.NewResourceConfigRaw(
		map[string]interface{}{
			"foo": "bar",
			TimeoutsConfigKey: map[string]interface{}{
				"create": "2m",
				"update": "1m",
			},
		},
	)

	timeout := &ResourceTimeout{}
	err := timeout.ConfigDecode(r, c)
	if err != nil {
		t.Fatalf("Expected good timeout returned:, %s", err)
	}

	expected := &ResourceTimeout{
		Create: DefaultTimeout(2 * time.Minute),
		Update: DefaultTimeout(1 * time.Minute),
	}

	if !reflect.DeepEqual(timeout, expected) {
		t.Fatalf("bad timeout decode.\nExpected:\n%#v\nGot:\n%#v\n", expected, timeout)
	}
}

func TestResourceTimeout_legacyConfigDecode(t *testing.T) {
	r := &Resource{
		Timeouts: &ResourceTimeout{
			Create: DefaultTimeout(10 * time.Minute),
			Update: DefaultTimeout(5 * time.Minute),
		},
	}

	c := terraform.NewResourceConfigRaw(
		map[string]interface{}{
			"foo": "bar",
			TimeoutsConfigKey: []interface{}{
				map[string]interface{}{
					"create": "2m",
					"update": "1m",
				},
			},
		},
	)

	timeout := &ResourceTimeout{}
	err := timeout.ConfigDecode(r, c)
	if err != nil {
		t.Fatalf("Expected good timeout returned:, %s", err)
	}

	expected := &ResourceTimeout{
		Create: DefaultTimeout(2 * time.Minute),
		Update: DefaultTimeout(1 * time.Minute),
	}

	if !reflect.DeepEqual(timeout, expected) {
		t.Fatalf("bad timeout decode.\nExpected:\n%#v\nGot:\n%#v\n", expected, timeout)
	}
}

func TestResourceTimeout_DiffEncode_basic(t *testing.T) {
	cases := []struct {
		Timeout  *ResourceTimeout
		Expected map[string]interface{}
		// Not immediately clear when an error would hit
		ShouldErr bool
	}{
		// Two fields
		{
			Timeout:   timeoutForValues(10, 0, 5, 0, 0),
			Expected:  map[string]interface{}{TimeoutKey: expectedForValues(10, 0, 5, 0, 0)},
			ShouldErr: false,
		},
		// Two fields, one is Default
		{
			Timeout:   timeoutForValues(10, 0, 0, 0, 7),
			Expected:  map[string]interface{}{TimeoutKey: expectedForValues(10, 0, 0, 0, 7)},
			ShouldErr: false,
		},
		// All fields
		{
			Timeout:   timeoutForValues(10, 3, 4, 1, 7),
			Expected:  map[string]interface{}{TimeoutKey: expectedForValues(10, 3, 4, 1, 7)},
			ShouldErr: false,
		},
		// No fields
		{
			Timeout:   &ResourceTimeout{},
			Expected:  nil,
			ShouldErr: false,
		},
	}

	for _, c := range cases {
		state := &terraform.InstanceDiff{}
		err := c.Timeout.DiffEncode(state)
		if err != nil && !c.ShouldErr {
			t.Fatalf("Error, expected:\n%#v\n got:\n%#v\n", c.Expected, state.Meta)
		}

		// should maybe just compare [TimeoutKey] but for now we're assuming only
		// that in Meta
		if !reflect.DeepEqual(state.Meta, c.Expected) {
			t.Fatalf("Encode not equal, expected:\n%#v\n\ngot:\n%#v\n", c.Expected, state.Meta)
		}
	}
	// same test cases but for InstanceState
	for _, c := range cases {
		state := &terraform.InstanceState{}
		err := c.Timeout.StateEncode(state)
		if err != nil && !c.ShouldErr {
			t.Fatalf("Error, expected:\n%#v\n got:\n%#v\n", c.Expected, state.Meta)
		}

		// should maybe just compare [TimeoutKey] but for now we're assuming only
		// that in Meta
		if !reflect.DeepEqual(state.Meta, c.Expected) {
			t.Fatalf("Encode not equal, expected:\n%#v\n\ngot:\n%#v\n", c.Expected, state.Meta)
		}
	}
}

func TestResourceTimeout_MetaDecode_basic(t *testing.T) {
	cases := []struct {
		State    *terraform.InstanceDiff
		Expected *ResourceTimeout
		// Not immediately clear when an error would hit
		ShouldErr bool
	}{
		// Two fields
		{
			State:     &terraform.InstanceDiff{Meta: map[string]interface{}{TimeoutKey: expectedForValues(10, 0, 5, 0, 0)}},
			Expected:  timeoutForValues(10, 0, 5, 0, 0),
			ShouldErr: false,
		},
		// Two fields, one is Default
		{
			State:     &terraform.InstanceDiff{Meta: map[string]interface{}{TimeoutKey: expectedForValues(10, 0, 0, 0, 7)}},
			Expected:  timeoutForValues(10, 7, 7, 7, 7),
			ShouldErr: false,
		},
		// All fields
		{
			State:     &terraform.InstanceDiff{Meta: map[string]interface{}{TimeoutKey: expectedForValues(10, 3, 4, 1, 7)}},
			Expected:  timeoutForValues(10, 3, 4, 1, 7),
			ShouldErr: false,
		},
		// No fields
		{
			State:     &terraform.InstanceDiff{},
			Expected:  &ResourceTimeout{},
			ShouldErr: false,
		},
	}

	for _, c := range cases {
		rt := &ResourceTimeout{}
		err := rt.DiffDecode(c.State)
		if err != nil && !c.ShouldErr {
			t.Fatalf("Error, expected:\n%#v\n got:\n%#v\n", c.Expected, rt)
		}

		// should maybe just compare [TimeoutKey] but for now we're assuming only
		// that in Meta
		if !reflect.DeepEqual(rt, c.Expected) {
			t.Fatalf("Encode not equal, expected:\n%#v\n\ngot:\n%#v\n", c.Expected, rt)
		}
	}
}

func timeoutForValues(create, read, update, del, def int) *ResourceTimeout {
	rt := ResourceTimeout{}

	if create != 0 {
		rt.Create = DefaultTimeout(time.Duration(create) * time.Minute)
	}
	if read != 0 {
		rt.Read = DefaultTimeout(time.Duration(read) * time.Minute)
	}
	if update != 0 {
		rt.Update = DefaultTimeout(time.Duration(update) * time.Minute)
	}
	if del != 0 {
		rt.Delete = DefaultTimeout(time.Duration(del) * time.Minute)
	}

	if def != 0 {
		rt.Default = DefaultTimeout(time.Duration(def) * time.Minute)
	}

	return &rt
}

// Generates a ResourceTimeout struct that should reflect the
// d.Timeout("key") results
func expectedTimeoutForValues(create, read, update, del, def int) *ResourceTimeout {
	rt := ResourceTimeout{}

	defaultValues := []*int{&create, &read, &update, &del, &def}
	for _, v := range defaultValues {
		if *v == 0 {
			*v = 20
		}
	}

	if create != 0 {
		rt.Create = DefaultTimeout(time.Duration(create) * time.Minute)
	}
	if read != 0 {
		rt.Read = DefaultTimeout(time.Duration(read) * time.Minute)
	}
	if update != 0 {
		rt.Update = DefaultTimeout(time.Duration(update) * time.Minute)
	}
	if del != 0 {
		rt.Delete = DefaultTimeout(time.Duration(del) * time.Minute)
	}

	if def != 0 {
		rt.Default = DefaultTimeout(time.Duration(def) * time.Minute)
	}

	return &rt
}

func expectedForValues(create, read, update, del, def int) map[string]interface{} {
	ex := make(map[string]interface{})

	if create != 0 {
		ex["create"] = DefaultTimeout(time.Duration(create) * time.Minute).Nanoseconds()
	}
	if read != 0 {
		ex["read"] = DefaultTimeout(time.Duration(read) * time.Minute).Nanoseconds()
	}
	if update != 0 {
		ex["update"] = DefaultTimeout(time.Duration(update) * time.Minute).Nanoseconds()
	}
	if del != 0 {
		ex["delete"] = DefaultTimeout(time.Duration(del) * time.Minute).Nanoseconds()
	}

	if def != 0 {
		defNano := DefaultTimeout(time.Duration(def) * time.Minute).Nanoseconds()
		ex["default"] = defNano

		for _, k := range timeoutKeys() {
			if _, ok := ex[k]; !ok {
				ex[k] = defNano
			}
		}
	}

	return ex
}

func expectedConfigForValues(createMin, readMin, updateMin, deleteMin, def int) map[string]interface{} {
	ex := make(map[string]interface{}, 0)

	if createMin != 0 {
		ex["create"] = fmt.Sprintf("%dm", createMin)
	}
	if readMin != 0 {
		ex["read"] = fmt.Sprintf("%dm", readMin)
	}
	if updateMin != 0 {
		ex["update"] = fmt.Sprintf("%dm", updateMin)
	}
	if deleteMin != 0 {
		ex["delete"] = fmt.Sprintf("%dm", deleteMin)
	}

	if def != 0 {
		ex["default"] = fmt.Sprintf("%dm", def)
	}
	return ex
}
