package command

import (
	"os"
	"strings"
	"testing"

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

	"github.com/hashicorp/terraform/configs/configschema"
	"github.com/hashicorp/terraform/terraform"
)

func setupTest(fixturepath string, args ...string) (*cli.MockUi, int) {
	ui := new(cli.MockUi)
	p := testProvider()
	p.GetSchemaReturn = &terraform.ProviderSchema{
		ResourceTypes: map[string]*configschema.Block{
			"test_instance": {
				Attributes: map[string]*configschema.Attribute{
					"ami": {Type: cty.String, Optional: true},
				},
				BlockTypes: map[string]*configschema.NestedBlock{
					"network_interface": {
						Nesting: configschema.NestingList,
						Block: configschema.Block{
							Attributes: map[string]*configschema.Attribute{
								"device_index": {Type: cty.String, Optional: true},
								"description":  {Type: cty.String, Optional: true},
							},
						},
					},
				},
			},
		},
	}
	c := &ValidateCommand{
		Meta: Meta{
			testingOverrides: metaOverridesForProvider(p),
			Ui:               ui,
		},
	}

	args = append(args, testFixturePath(fixturepath))

	code := c.Run(args)
	return ui, code
}

func TestValidateCommand(t *testing.T) {
	if ui, code := setupTest("validate-valid"); code != 0 {
		t.Fatalf("unexpected non-successful exit code %d\n\n%s", code, ui.ErrorWriter.String())
	}
}

func TestValidateCommandWithTfvarsFile(t *testing.T) {
	// Create a temporary working directory that is empty because this test
	// requires scanning the current working directory by validate command.
	td := tempDir(t)
	testCopyDir(t, testFixturePath("validate-valid/with-tfvars-file"), td)
	defer os.RemoveAll(td)
	defer testChdir(t, td)()

	ui := new(cli.MockUi)
	c := &ValidateCommand{
		Meta: Meta{
			testingOverrides: metaOverridesForProvider(testProvider()),
			Ui:               ui,
		},
	}

	args := []string{}
	if code := c.Run(args); code != 0 {
		t.Fatalf("bad %d\n\n%s", code, ui.ErrorWriter.String())
	}
}

func TestValidateFailingCommand(t *testing.T) {
	if ui, code := setupTest("validate-invalid"); code != 1 {
		t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
	}
}

func TestValidateFailingCommandMissingQuote(t *testing.T) {
	// FIXME: Re-enable once we've updated core for new data structures
	t.Skip("test temporarily disabled until deep validate supports new config structures")

	ui, code := setupTest("validate-invalid/missing_quote")

	if code != 1 {
		t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
	}
	if !strings.HasSuffix(strings.TrimSpace(ui.ErrorWriter.String()), "IDENT test") {
		t.Fatalf("Should have failed: %d\n\n'%s'", code, ui.ErrorWriter.String())
	}
}

func TestValidateFailingCommandMissingVariable(t *testing.T) {
	// FIXME: Re-enable once we've updated core for new data structures
	t.Skip("test temporarily disabled until deep validate supports new config structures")

	ui, code := setupTest("validate-invalid/missing_var")
	if code != 1 {
		t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
	}
	if !strings.HasSuffix(strings.TrimSpace(ui.ErrorWriter.String()), "config: unknown variable referenced: 'description'; define it with a 'variable' block") {
		t.Fatalf("Should have failed: %d\n\n'%s'", code, ui.ErrorWriter.String())
	}
}

func TestSameProviderMutipleTimesShouldFail(t *testing.T) {
	ui, code := setupTest("validate-invalid/multiple_providers")
	if code != 1 {
		t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
	}
	wantError := "Error: Duplicate provider configuration"
	if !strings.Contains(ui.ErrorWriter.String(), wantError) {
		t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
	}
}

func TestSameModuleMultipleTimesShouldFail(t *testing.T) {
	ui, code := setupTest("validate-invalid/multiple_modules")
	if code != 1 {
		t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
	}
	wantError := "Error: Duplicate module call"
	if !strings.Contains(ui.ErrorWriter.String(), wantError) {
		t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
	}
}

func TestSameResourceMultipleTimesShouldFail(t *testing.T) {
	ui, code := setupTest("validate-invalid/multiple_resources")
	if code != 1 {
		t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
	}
	wantError := `Error: Duplicate resource "aws_instance" configuration`
	if !strings.Contains(ui.ErrorWriter.String(), wantError) {
		t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
	}
}

func TestOutputWithoutValueShouldFail(t *testing.T) {
	ui, code := setupTest("validate-invalid/outputs")
	if code != 1 {
		t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
	}
	wantError := `The argument "value" is required, but no definition was found.`
	if !strings.Contains(ui.ErrorWriter.String(), wantError) {
		t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
	}
	wantError = `An argument named "values" is not expected here. Did you mean "value"?`
	if !strings.Contains(ui.ErrorWriter.String(), wantError) {
		t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
	}
}

func TestModuleWithIncorrectNameShouldFail(t *testing.T) {
	ui, code := setupTest("validate-invalid/incorrectmodulename")
	if code != 1 {
		t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
	}

	wantError := `Error: Invalid module instance name`
	if !strings.Contains(ui.ErrorWriter.String(), wantError) {
		t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
	}
	wantError = `Error: Variables not allowed`
	if !strings.Contains(ui.ErrorWriter.String(), wantError) {
		t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
	}
}

func TestWronglyUsedInterpolationShouldFail(t *testing.T) {
	ui, code := setupTest("validate-invalid/interpolation")
	if code != 1 {
		t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String())
	}

	wantError := `Error: Variables not allowed`
	if !strings.Contains(ui.ErrorWriter.String(), wantError) {
		t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
	}
	wantError = `A single static variable reference is required`
	if !strings.Contains(ui.ErrorWriter.String(), wantError) {
		t.Fatalf("Missing error string %q\n\n'%s'", wantError, ui.ErrorWriter.String())
	}
}

func TestMissingDefinedVar(t *testing.T) {
	ui, code := setupTest("validate-invalid/missing_defined_var")
	// This is allowed because validate tests only that variables are referenced
	// correctly, not that they all have defined values.
	if code != 0 {
		t.Fatalf("Should have passed: %d\n\n%s", code, ui.ErrorWriter.String())
	}
}
