package command

import (
	"fmt"
	"regexp"
	"testing"
	"time"

	"github.com/mitchellh/cli"
	"github.com/mitchellh/colorstring"
	"github.com/zclconf/go-cty/cty"

	"github.com/hashicorp/terraform/addrs"
	"github.com/hashicorp/terraform/plans"
	"github.com/hashicorp/terraform/states"
	"github.com/hashicorp/terraform/terraform"
)

func TestUiHookPreApply_periodicTimer(t *testing.T) {
	ui := cli.NewMockUi()
	h := &UiHook{
		Colorize: &colorstring.Colorize{
			Colors:  colorstring.DefaultColors,
			Disable: true,
			Reset:   true,
		},
		Ui:              ui,
		PeriodicUiTimer: 1 * time.Second,
	}
	h.init()
	h.resources = map[string]uiResourceState{
		"data.aws_availability_zones.available": uiResourceState{
			Op:    uiResourceDestroy,
			Start: time.Now(),
		},
	}

	addr := addrs.Resource{
		Mode: addrs.DataResourceMode,
		Type: "aws_availability_zones",
		Name: "available",
	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)

	priorState := cty.ObjectVal(map[string]cty.Value{
		"id": cty.StringVal("2017-03-05 10:56:59.298784526 +0000 UTC"),
		"names": cty.ListVal([]cty.Value{
			cty.StringVal("us-east-1a"),
			cty.StringVal("us-east-1b"),
			cty.StringVal("us-east-1c"),
			cty.StringVal("us-east-1d"),
		}),
	})
	plannedNewState := cty.NullVal(cty.Object(map[string]cty.Type{
		"id":    cty.String,
		"names": cty.List(cty.String),
	}))

	action, err := h.PreApply(addr, states.CurrentGen, plans.Delete, priorState, plannedNewState)
	if err != nil {
		t.Fatal(err)
	}
	if action != terraform.HookActionContinue {
		t.Fatalf("Expected hook to continue, given: %#v", action)
	}

	time.Sleep(3100 * time.Millisecond)

	// stop the background writer
	uiState := h.resources[addr.String()]
	close(uiState.DoneCh)
	<-uiState.done

	expectedOutput := `data.aws_availability_zones.available: Destroying... [id=2017-03-05 10:56:59.298784526 +0000 UTC]
data.aws_availability_zones.available: Still destroying... [id=2017-03-05 10:56:59.298784526 +0000 UTC, 1s elapsed]
data.aws_availability_zones.available: Still destroying... [id=2017-03-05 10:56:59.298784526 +0000 UTC, 2s elapsed]
data.aws_availability_zones.available: Still destroying... [id=2017-03-05 10:56:59.298784526 +0000 UTC, 3s elapsed]
`
	output := ui.OutputWriter.String()
	if output != expectedOutput {
		t.Fatalf("Output didn't match.\nExpected: %q\nGiven: %q", expectedOutput, output)
	}

	expectedErrOutput := ""
	errOutput := ui.ErrorWriter.String()
	if errOutput != expectedErrOutput {
		t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput)
	}
}

func TestUiHookPreApply_destroy(t *testing.T) {
	ui := cli.NewMockUi()
	h := &UiHook{
		Colorize: &colorstring.Colorize{
			Colors:  colorstring.DefaultColors,
			Disable: true,
			Reset:   true,
		},
		Ui: ui,
	}
	h.init()
	h.resources = map[string]uiResourceState{
		"data.aws_availability_zones.available": uiResourceState{
			Op:    uiResourceDestroy,
			Start: time.Now(),
		},
	}

	addr := addrs.Resource{
		Mode: addrs.DataResourceMode,
		Type: "aws_availability_zones",
		Name: "available",
	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)

	priorState := cty.ObjectVal(map[string]cty.Value{
		"id": cty.StringVal("2017-03-05 10:56:59.298784526 +0000 UTC"),
		"names": cty.ListVal([]cty.Value{
			cty.StringVal("us-east-1a"),
			cty.StringVal("us-east-1b"),
			cty.StringVal("us-east-1c"),
			cty.StringVal("us-east-1d"),
		}),
	})
	plannedNewState := cty.NullVal(cty.Object(map[string]cty.Type{
		"id":    cty.String,
		"names": cty.List(cty.String),
	}))

	action, err := h.PreApply(addr, states.CurrentGen, plans.Delete, priorState, plannedNewState)
	if err != nil {
		t.Fatal(err)
	}
	if action != terraform.HookActionContinue {
		t.Fatalf("Expected hook to continue, given: %#v", action)
	}

	// stop the background writer
	uiState := h.resources[addr.String()]
	close(uiState.DoneCh)
	<-uiState.done

	expectedOutput := "data.aws_availability_zones.available: Destroying... [id=2017-03-05 10:56:59.298784526 +0000 UTC]\n"
	output := ui.OutputWriter.String()
	if output != expectedOutput {
		t.Fatalf("Output didn't match.\nExpected: %q\nGiven: %q", expectedOutput, output)
	}

	expectedErrOutput := ""
	errOutput := ui.ErrorWriter.String()
	if errOutput != expectedErrOutput {
		t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput)
	}
}

func TestUiHookPostApply_emptyState(t *testing.T) {
	ui := cli.NewMockUi()
	h := &UiHook{
		Colorize: &colorstring.Colorize{
			Colors:  colorstring.DefaultColors,
			Disable: true,
			Reset:   true,
		},
		Ui: ui,
	}
	h.init()
	h.resources = map[string]uiResourceState{
		"data.google_compute_zones.available": uiResourceState{
			Op:    uiResourceDestroy,
			Start: time.Now(),
		},
	}

	addr := addrs.Resource{
		Mode: addrs.DataResourceMode,
		Type: "google_compute_zones",
		Name: "available",
	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)

	newState := cty.NullVal(cty.Object(map[string]cty.Type{
		"id":    cty.String,
		"names": cty.List(cty.String),
	}))

	action, err := h.PostApply(addr, states.CurrentGen, newState, nil)
	if err != nil {
		t.Fatal(err)
	}
	if action != terraform.HookActionContinue {
		t.Fatalf("Expected hook to continue, given: %#v", action)
	}

	expectedRegexp := "^data.google_compute_zones.available: Destruction complete after -?[a-z0-9µ.]+\n$"
	output := ui.OutputWriter.String()
	if matched, _ := regexp.MatchString(expectedRegexp, output); !matched {
		t.Fatalf("Output didn't match regexp.\nExpected: %q\nGiven: %q", expectedRegexp, output)
	}

	expectedErrOutput := ""
	errOutput := ui.ErrorWriter.String()
	if errOutput != expectedErrOutput {
		t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput)
	}
}

func TestTruncateId(t *testing.T) {
	testCases := []struct {
		Input    string
		Expected string
		MaxLen   int
	}{
		{
			Input:    "Hello world",
			Expected: "H...d",
			MaxLen:   3,
		},
		{
			Input:    "Hello world",
			Expected: "H...d",
			MaxLen:   5,
		},
		{
			Input:    "Hello world",
			Expected: "He...d",
			MaxLen:   6,
		},
		{
			Input:    "Hello world",
			Expected: "He...ld",
			MaxLen:   7,
		},
		{
			Input:    "Hello world",
			Expected: "Hel...ld",
			MaxLen:   8,
		},
		{
			Input:    "Hello world",
			Expected: "Hel...rld",
			MaxLen:   9,
		},
		{
			Input:    "Hello world",
			Expected: "Hell...rld",
			MaxLen:   10,
		},
		{
			Input:    "Hello world",
			Expected: "Hello world",
			MaxLen:   11,
		},
		{
			Input:    "Hello world",
			Expected: "Hello world",
			MaxLen:   12,
		},
		{
			Input:    "あいうえおかきくけこさ",
			Expected: "あ...さ",
			MaxLen:   3,
		},
		{
			Input:    "あいうえおかきくけこさ",
			Expected: "あ...さ",
			MaxLen:   5,
		},
		{
			Input:    "あいうえおかきくけこさ",
			Expected: "あい...さ",
			MaxLen:   6,
		},
		{
			Input:    "あいうえおかきくけこさ",
			Expected: "あい...こさ",
			MaxLen:   7,
		},
		{
			Input:    "あいうえおかきくけこさ",
			Expected: "あいう...こさ",
			MaxLen:   8,
		},
		{
			Input:    "あいうえおかきくけこさ",
			Expected: "あいう...けこさ",
			MaxLen:   9,
		},
		{
			Input:    "あいうえおかきくけこさ",
			Expected: "あいうえ...けこさ",
			MaxLen:   10,
		},
		{
			Input:    "あいうえおかきくけこさ",
			Expected: "あいうえおかきくけこさ",
			MaxLen:   11,
		},
		{
			Input:    "あいうえおかきくけこさ",
			Expected: "あいうえおかきくけこさ",
			MaxLen:   12,
		},
	}
	for i, tc := range testCases {
		testName := fmt.Sprintf("%d", i)
		t.Run(testName, func(t *testing.T) {
			out := truncateId(tc.Input, tc.MaxLen)
			if out != tc.Expected {
				t.Fatalf("Expected %q to be shortened to %d as %q (given: %q)",
					tc.Input, tc.MaxLen, tc.Expected, out)
			}
		})
	}
}
