// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package command

import (
	"bytes"
	"fmt"
	"os"
	"path/filepath"
	"testing"

	"github.com/hashicorp/nomad/ci"
	"github.com/mitchellh/cli"
	"github.com/shoenig/test/must"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestFmtCommand(t *testing.T) {
	ci.Parallel(t)

	const inSuffix = ".in.hcl"
	const expectedSuffix = ".out.hcl"

	tests := []struct {
		name        string
		testFile    string
		flags       []string
		expectWrite bool
		expectCode  int
	}{
		{
			name:       "config with check",
			testFile:   "nomad",
			flags:      []string{"-check"},
			expectCode: 1,
		},
		{
			name:        "config without check",
			testFile:    "nomad",
			flags:       []string{},
			expectWrite: true,
			expectCode:  0,
		},
		{
			name:       "job with check",
			testFile:   "job",
			flags:      []string{"-check"},
			expectCode: 1,
		},
		{
			name:        "job without check",
			testFile:    "job",
			flags:       []string{},
			expectWrite: true,
			expectCode:  0,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			tc := tc
			ci.Parallel(t)

			tmpDir := t.TempDir()
			inFile := filepath.Join("testdata", "fmt", tc.testFile+inSuffix)
			expectedFile := filepath.Join("testdata", "fmt", tc.testFile+expectedSuffix)
			fmtFile := filepath.Join(tmpDir, tc.testFile+".hcl")

			expected, err := os.ReadFile(expectedFile)
			must.NoError(t, err)

			// copy the input file to the test tempdir so that we don't
			// overwrite the test input in source control
			input, err := os.ReadFile(inFile)
			must.NoError(t, err)
			must.NoError(t, os.WriteFile(fmtFile, input, 0644))

			ui := cli.NewMockUi()
			cmd := &FormatCommand{
				Meta: Meta{Ui: ui},
			}

			flags := append(tc.flags, fmtFile)

			code := cmd.Run(flags)
			must.Eq(t, tc.expectCode, code)

			// compare the maybe-overwritten file contents
			actual, err := os.ReadFile(fmtFile)
			must.NoError(t, err)

			if tc.expectWrite {
				must.Eq(t, string(expected), string(actual))
			} else {
				must.Eq(t, string(input), string(actual))
			}
		})
	}
}

func TestFmtCommand_FromStdin(t *testing.T) {
	ci.Parallel(t)

	tests := []struct {
		name       string
		flags      []string
		expectCode int
	}{
		{
			name:       "with check",
			flags:      []string{"-check", "-"},
			expectCode: 1,
		},
		{
			name:       "without check",
			flags:      []string{"-"},
			expectCode: 0,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {

			stdinFake := bytes.NewBuffer(fmtFixture.input)
			ui := cli.NewMockUi()
			cmd := &FormatCommand{
				Meta:  Meta{Ui: ui},
				stdin: stdinFake,
			}

			code := cmd.Run(tc.flags)
			must.Eq(t, tc.expectCode, code)
			must.StrContains(t, string(fmtFixture.golden), ui.OutputWriter.String())
		})
	}
}

func TestFmtCommand_FromWorkingDirectory(t *testing.T) {
	tmpDir := fmtFixtureWriteDir(t)

	cwd, err := os.Getwd()
	require.NoError(t, err)

	err = os.Chdir(tmpDir)
	require.NoError(t, err)
	defer os.Chdir(cwd)

	tests := []struct {
		name       string
		flags      []string
		expectCode int
	}{
		{
			name:       "with check",
			flags:      []string{"-check"},
			expectCode: 1,
		},
		{
			name:       "without check",
			flags:      []string{},
			expectCode: 0,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			ui := cli.NewMockUi()
			cmd := &FormatCommand{Meta: Meta{Ui: ui}}
			code := cmd.Run(tc.flags)
			must.Eq(t, tc.expectCode, code)
			must.Eq(t, fmt.Sprintf("%s\n", fmtFixture.filename), ui.OutputWriter.String())
		})
	}
}

func TestFmtCommand_FromDirectoryArgument(t *testing.T) {
	tmpDir := fmtFixtureWriteDir(t)

	tests := []struct {
		name       string
		flags      []string
		expectCode int
	}{
		{
			name:       "with check",
			flags:      []string{"-check", tmpDir},
			expectCode: 1,
		},
		{
			name:       "without check",
			flags:      []string{tmpDir},
			expectCode: 0,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			ui := cli.NewMockUi()
			cmd := &FormatCommand{Meta: Meta{Ui: ui}}

			code := cmd.Run(tc.flags)
			must.Eq(t, tc.expectCode, code)
			must.Eq(t,
				fmt.Sprintf("%s\n", filepath.Join(tmpDir, fmtFixture.filename)),
				ui.OutputWriter.String())
		})
	}
}

func TestFmtCommand_FromFileArgument(t *testing.T) {
	tmpDir := fmtFixtureWriteDir(t)
	path := filepath.Join(tmpDir, fmtFixture.filename)

	tests := []struct {
		name       string
		flags      []string
		expectCode int
	}{
		{
			name:       "with check",
			flags:      []string{"-check", path},
			expectCode: 1,
		},
		{
			name:       "without check",
			flags:      []string{path},
			expectCode: 0,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			ui := cli.NewMockUi()
			cmd := &FormatCommand{Meta: Meta{Ui: ui}}

			code := cmd.Run(tc.flags)
			must.Eq(t, tc.expectCode, code)
			must.Eq(t, fmt.Sprintf("%s\n", path), ui.OutputWriter.String())
		})
	}
}

func TestFmtCommand_FileDoesNotExist(t *testing.T) {
	ci.Parallel(t)

	ui := cli.NewMockUi()
	cmd := &FormatCommand{
		Meta: Meta{Ui: ui},
	}

	code := cmd.Run([]string{"file/does/not/exist.hcl"})
	assert.Equal(t, 1, code)
}

func TestFmtCommand_InvalidSyntax(t *testing.T) {
	ci.Parallel(t)

	stdinFake := bytes.NewBufferString(`client {enabled true }`)

	ui := cli.NewMockUi()
	cmd := &FormatCommand{
		Meta:  Meta{Ui: ui},
		stdin: stdinFake,
	}

	code := cmd.Run([]string{"-"})
	assert.Equal(t, 1, code)
}

func fmtFixtureWriteDir(t *testing.T) string {
	dir := t.TempDir()

	err := os.WriteFile(filepath.Join(dir, fmtFixture.filename), fmtFixture.input, 0644)
	require.NoError(t, err)

	return dir
}

var fmtFixture = struct {
	filename string
	input    []byte
	golden   []byte
}{
	filename: "nomad.hcl",
	input:    []byte("client   {enabled = true}"),
	golden:   []byte("client { enabled = true }\n\n"),
}
