package test

import (
	"errors"
	"fmt"
	"regexp"
	"strings"
	"testing"

	"github.com/hashicorp/terraform/addrs"
	"github.com/hashicorp/terraform/helper/resource"
	"github.com/hashicorp/terraform/terraform"
)

func TestResourceNestedSet_basic(t *testing.T) {
	resource.UnitTest(t, resource.TestCase{
		Providers:    testAccProviders,
		CheckDestroy: testAccCheckResourceDestroy,
		Steps: []resource.TestStep{
			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
	single {
		value = "bar"
	}
}
				`),
			},
		},
	})
}

func TestResourceNestedSet_basicImport(t *testing.T) {
	resource.UnitTest(t, resource.TestCase{
		Providers:    testAccProviders,
		CheckDestroy: testAccCheckResourceDestroy,
		Steps: []resource.TestStep{
			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
	single {
		value = "bar"
	}
}
				`),
			},
			resource.TestStep{
				ImportState:  true,
				ResourceName: "test_resource_nested_set.foo",
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
	single {
		value = "bar"
	}
}
				`),
				ImportStateCheck: func(ss []*terraform.InstanceState) error {
					for _, s := range ss {
						if s.Attributes["multi.#"] != "0" ||
							s.Attributes["single.#"] != "0" ||
							s.Attributes["type_list.#"] != "0" ||
							s.Attributes["with_list.#"] != "0" {
							return fmt.Errorf("missing blocks in imported state:\n%s", s)
						}
					}
					return nil
				},
			},
		},
	})
}

// The set should not be generated because of it's computed value
func TestResourceNestedSet_noSet(t *testing.T) {
	checkFunc := func(s *terraform.State) error {
		root := s.ModuleByPath(addrs.RootModuleInstance)
		res := root.Resources["test_resource_nested_set.foo"]
		for k, v := range res.Primary.Attributes {
			if strings.HasPrefix(k, "single") && k != "single.#" {
				return fmt.Errorf("unexpected set value: %s:%s", k, v)
			}
		}
		return nil
	}
	resource.UnitTest(t, resource.TestCase{
		Providers:    testAccProviders,
		CheckDestroy: testAccCheckResourceDestroy,
		Steps: []resource.TestStep{
			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
}
				`),
				Check: checkFunc,
			},
		},
	})
}

// the empty type_list must be passed to the provider with 1 nil element
func TestResourceNestedSet_emptyBlock(t *testing.T) {
	resource.UnitTest(t, resource.TestCase{
		Providers:    testAccProviders,
		CheckDestroy: testAccCheckResourceDestroy,
		Steps: []resource.TestStep{
			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
	type_list {
	}
}
				`),
				Check: resource.ComposeTestCheckFunc(
					resource.TestCheckResourceAttr("test_resource_nested_set.foo", "type_list.#", "1"),
				),
			},
		},
	})
}

func TestResourceNestedSet_emptyNestedListBlock(t *testing.T) {
	checkFunc := func(s *terraform.State) error {
		root := s.ModuleByPath(addrs.RootModuleInstance)
		res := root.Resources["test_resource_nested_set.foo"]
		found := false
		for k := range res.Primary.Attributes {
			if !regexp.MustCompile(`^with_list\.\d+\.list_block\.`).MatchString(k) {
				continue
			}
			found = true
		}
		if !found {
			return fmt.Errorf("with_list.X.list_block not found")
		}
		return nil
	}
	resource.UnitTest(t, resource.TestCase{
		Providers:    testAccProviders,
		CheckDestroy: testAccCheckResourceDestroy,
		Steps: []resource.TestStep{
			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
	with_list {
		required = "ok"
		list_block {
		}
	}
}
				`),
				Check: checkFunc,
			},
		},
	})
}
func TestResourceNestedSet_emptyNestedList(t *testing.T) {
	checkFunc := func(s *terraform.State) error {
		root := s.ModuleByPath(addrs.RootModuleInstance)
		res := root.Resources["test_resource_nested_set.foo"]
		found := false
		for k, v := range res.Primary.Attributes {
			if regexp.MustCompile(`^with_list\.\d+\.list\.#$`).MatchString(k) {
				found = true
				if v != "0" {
					return fmt.Errorf("expected empty list: %s, got %s", k, v)
				}
				break
			}
		}
		if !found {
			return fmt.Errorf("with_list.X.nested_list not found")
		}
		return nil
	}
	resource.UnitTest(t, resource.TestCase{
		Providers:    testAccProviders,
		CheckDestroy: testAccCheckResourceDestroy,
		Steps: []resource.TestStep{
			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
	with_list {
		required = "ok"
		list = []
	}
}
				`),
				Check: checkFunc,
			},
		},
	})
}

func TestResourceNestedSet_addRemove(t *testing.T) {
	var id string
	checkFunc := func(s *terraform.State) error {
		root := s.ModuleByPath(addrs.RootModuleInstance)
		res := root.Resources["test_resource_nested_set.foo"]
		if res.Primary.ID == id {
			return errors.New("expected new resource")
		}
		id = res.Primary.ID
		return nil
	}
	resource.UnitTest(t, resource.TestCase{
		Providers:    testAccProviders,
		CheckDestroy: testAccCheckResourceDestroy,
		Steps: []resource.TestStep{
			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
}
				`),
				Check: checkFunc,
			},
			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
	single {
		value = "bar"
	}
}
				`),
				Check: resource.ComposeTestCheckFunc(
					checkFunc,
					resource.TestCheckResourceAttr(
						"test_resource_nested_set.foo", "single.#", "1",
					),
					// the hash of single seems to change here, so we're not
					// going to test for "value" directly
					// FIXME: figure out why the set hash changes
				),
			},
			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
}
				`),
				Check: resource.ComposeTestCheckFunc(
					resource.TestCheckResourceAttr(
						"test_resource_nested_set.foo", "single.#", "0",
					),
					checkFunc,
				),
			},
			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
	single {
		value = "bar"
	}
}
				`),
				Check: checkFunc,
			},
			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
	single {
		value = "bar"
		optional = "baz"
	}
}
				`),
				Check: checkFunc,
			},

			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
}
			   	`),
				Check: checkFunc,
			},
		},
	})
}
func TestResourceNestedSet_multiAddRemove(t *testing.T) {
	checkFunc := func(s *terraform.State) error {
		return nil
	}
	resource.UnitTest(t, resource.TestCase{
		Providers:    testAccProviders,
		CheckDestroy: testAccCheckResourceDestroy,
		Steps: []resource.TestStep{
			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
}
				`),
				Check: checkFunc,
			},
			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
	multi {
		optional = "bar"
	}
}
				`),
				Check: checkFunc,
			},

			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
}
								`),
				Check: checkFunc,
			},

			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
	multi {
		set {
			required = "val"
		}
	}
}
				`),
				Check: checkFunc,
			},

			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
	multi {
		set {
			required = "new"
		}
	}
}
				`),
				Check: checkFunc,
			},

			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
	multi {
		set {
			required = "new"
			optional_int = 3
		}
	}
}
				`),
				Check: checkFunc,
			},

			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
	single {
		value = "bar"
		optional = "baz"
	}
	multi {
		set {
			required = "new"
			optional_int = 3
		}
	}
}
			`),
				Check: checkFunc,
			},

			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
	optional = true
	single {
		value = "bar"
		optional = "baz"
	}
	multi {
		set {
			required = "new"
			optional_int = 3
		}
	}
}
			`),
				Check: checkFunc,
			},
		},
	})
}

func TestResourceNestedSet_forceNewEmptyString(t *testing.T) {
	var id string
	step := 0
	checkFunc := func(s *terraform.State) error {
		root := s.ModuleByPath(addrs.RootModuleInstance)
		res := root.Resources["test_resource_nested_set.foo"]
		defer func() {
			step++
			id = res.Primary.ID
		}()

		if step == 2 && res.Primary.ID == id {
			// setting an empty string currently does not trigger ForceNew, but
			// it should in the future.
			return nil
		}

		if res.Primary.ID == id {
			return errors.New("expected new resource")
		}

		return nil
	}
	resource.UnitTest(t, resource.TestCase{
		Providers:    testAccProviders,
		CheckDestroy: testAccCheckResourceDestroy,
		Steps: []resource.TestStep{
			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
	multi {
		set {
			required = "val"
		}
	}
}
				`),
				Check: checkFunc,
			},

			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
	multi {
		set {
			required = ""
		}
	}
}
				`),
				Check: checkFunc,
			},

			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
	force_new = ""
}
				`),
				Check: checkFunc,
			},
		},
	})
}

func TestResourceNestedSet_setWithList(t *testing.T) {
	checkFunc := func(s *terraform.State) error {
		return nil
	}
	resource.UnitTest(t, resource.TestCase{
		Providers:    testAccProviders,
		CheckDestroy: testAccCheckResourceDestroy,
		Steps: []resource.TestStep{
			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
	with_list {
		required = "bar"
		list = ["initial value"]
	}
}
				`),
				Check: checkFunc,
			},
			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
	with_list {
		required = "bar"
		list = ["second value"]
	}
}
				`),
				Check: checkFunc,
			},
		},
	})
}

// This is the same as forceNewEmptyString, but we start with the empty value,
// instead of changing it.
func TestResourceNestedSet_nestedSetEmptyString(t *testing.T) {
	resource.UnitTest(t, resource.TestCase{
		Providers:    testAccProviders,
		CheckDestroy: testAccCheckResourceDestroy,
		Steps: []resource.TestStep{
			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
	multi {
		set {
			required = ""
		}
	}
}
				`),
				Check: resource.ComposeTestCheckFunc(
					resource.TestCheckResourceAttr(
						"test_resource_nested_set.foo", "multi.529860700.set.4196279896.required", "",
					),
				),
			},
		},
	})
}

func TestResourceNestedSet_emptySet(t *testing.T) {
	resource.UnitTest(t, resource.TestCase{
		Providers:    testAccProviders,
		CheckDestroy: testAccCheckResourceDestroy,
		Steps: []resource.TestStep{
			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
	multi {
	}
}
				`),
				Check: resource.ComposeTestCheckFunc(
					resource.TestCheckResourceAttr(
						"test_resource_nested_set.foo", "multi.#", "1",
					),
				),
			},
		},
	})
}

func TestResourceNestedSet_multipleUnknownSetElements(t *testing.T) {
	checkFunc := func(s *terraform.State) error {
		return nil
	}
	resource.UnitTest(t, resource.TestCase{
		Providers:    testAccProviders,
		CheckDestroy: testAccCheckResourceDestroy,
		Steps: []resource.TestStep{
			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "a" {
}

resource "test_resource_nested_set" "b" {
}

resource "test_resource_nested_set" "c" {
	multi {
		optional = test_resource_nested_set.a.id
	}
	multi {
		optional = test_resource_nested_set.b.id
	}
}
				`),
				Check: checkFunc,
			},
		},
	})
}

func TestResourceNestedSet_interpolationChanges(t *testing.T) {
	resource.UnitTest(t, resource.TestCase{
		Providers:    testAccProviders,
		CheckDestroy: testAccCheckResourceDestroy,
		Steps: []resource.TestStep{
			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "foo" {
	single {
		value = "x"
	}
}
resource "test_resource_nested_set" "bar" {
	single {
		value = test_resource_nested_set.foo.id
	}
}
				`),
				Check: resource.ComposeTestCheckFunc(
					resource.TestCheckResourceAttr(
						"test_resource_nested_set.foo", "single.#", "1",
					),
					resource.TestCheckResourceAttr(
						"test_resource_nested_set.bar", "single.#", "1",
					),
				),
			},
			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource_nested_set" "baz" {
	single {
		value = "x"
	}
}
resource "test_resource_nested_set" "bar" {
	single {
		value = test_resource_nested_set.baz.id
	}
}
				`),
				Check: resource.ComposeTestCheckFunc(
					resource.TestCheckResourceAttr(
						"test_resource_nested_set.baz", "single.#", "1",
					),
					resource.TestCheckResourceAttr(
						"test_resource_nested_set.bar", "single.#", "1",
					),
				),
			},
		},
	})
}

func TestResourceNestedSet_dynamicSetBlock(t *testing.T) {
	resource.UnitTest(t, resource.TestCase{
		Providers:    testAccProviders,
		CheckDestroy: testAccCheckResourceDestroy,
		Steps: []resource.TestStep{
			resource.TestStep{
				Config: strings.TrimSpace(`
resource "test_resource" "a" {
	required = "ok"
	required_map = {
		a = "b"
	}
}

resource "test_resource_nested_set" "foo" {
  dynamic "with_list" {
    iterator = thing
	for_each = test_resource.a.computed_list
    content {
      required = thing.value
	  list = [thing.key]
    }
  }
}
				`),
				Check: resource.ComposeTestCheckFunc(),
			},
		},
	})
}
