package module

import (
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestLoadModuleWithOptions(t *testing.T) {
	assert := assert.New(t)

	options, _ := NewOptions().With(&Options{
		Path: filepath.Join("testdata", "full-example"),
	})
	module, err := LoadWithOptions(options)

	assert.Nil(err)
	assert.Equal(true, module.HasHeader())
	assert.Equal(true, module.HasInputs())
	assert.Equal(true, module.HasOutputs())
	assert.Equal(true, module.HasProviders())
	assert.Equal(true, module.HasRequirements())
}

func TestLoadModule(t *testing.T) {
	tests := []struct {
		name    string
		path    string
		wantErr bool
	}{
		{
			name:    "load module from path",
			path:    "full-example",
			wantErr: false,
		},
		{
			name:    "load module from path",
			path:    "non-exist",
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert := assert.New(t)
			_, err := loadModule(filepath.Join("testdata", tt.path))
			if tt.wantErr {
				assert.NotNil(err)
			} else {
				assert.Nil(err)
			}
		})
	}
}

func TestGetFileFormat(t *testing.T) {
	tests := []struct {
		name     string
		filename string
		expected string
	}{
		{
			name:     "get file format",
			filename: "main.tf",
			expected: ".tf",
		},
		{
			name:     "get file format",
			filename: "main.file.tf",
			expected: ".tf",
		},
		{
			name:     "get file format",
			filename: "main_file.tf",
			expected: ".tf",
		},
		{
			name:     "get file format",
			filename: "main.file_tf",
			expected: ".file_tf",
		},
		{
			name:     "get file format",
			filename: "main_file_tf",
			expected: "",
		},
		{
			name:     "get file format",
			filename: "",
			expected: "",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert := assert.New(t)
			actual := getFileFormat(tt.filename)
			assert.Equal(tt.expected, actual)
		})
	}
}

func TestIsFileFormatSupported(t *testing.T) {
	tests := []struct {
		name     string
		filename string
		expected bool
		wantErr  bool
		errText  string
	}{
		{
			name:     "is file format supported",
			filename: "main.adoc",
			expected: true,
			wantErr:  false,
			errText:  "",
		},
		{
			name:     "is file format supported",
			filename: "main.md",
			expected: true,
			wantErr:  false,
			errText:  "",
		},
		{
			name:     "is file format supported",
			filename: "main.tf",
			expected: true,
			wantErr:  false,
			errText:  "",
		},
		{
			name:     "is file format supported",
			filename: "main.txt",
			expected: true,
			wantErr:  false,
			errText:  "",
		},
		{
			name:     "is file format supported",
			filename: "main.doc",
			expected: false,
			wantErr:  true,
			errText:  "only .adoc, .md, .tf and .txt formats are supported to read header from",
		},
		{
			name:     "is file format supported",
			filename: "",
			expected: false,
			wantErr:  true,
			errText:  "--header-from value is missing",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert := assert.New(t)
			actual, err := isFileFormatSupported(tt.filename)
			if tt.wantErr {
				assert.NotNil(err)
				assert.Equal(tt.errText, err.Error())
			} else {
				assert.Nil(err)
				assert.Equal(tt.expected, actual)
			}
		})
	}
}

func TestLoadHeader(t *testing.T) {
	tests := []struct {
		name     string
		path     string
		header   string
		expected string
		wantErr  bool
		errText  string
	}{
		{
			name:     "load module header from path",
			path:     "full-example",
			header:   "main.tf",
			expected: "Example of 'foo_bar' module in `foo_bar.tf`.\n\n- list item 1\n- list item 2\n\nEven inline **formatting** in _here_ is possible.\nand some [link](https://domain.com/)",
			wantErr:  false,
			errText:  "",
		},
		{
			name:     "load module header from path",
			path:     "full-example",
			header:   "doc.tf",
			expected: "Custom Header:\n\nExample of 'foo_bar' module in `foo_bar.tf`.\n\n- list item 1\n- list item 2",
			wantErr:  false,
			errText:  "",
		},
		{
			name:     "load module header from path",
			path:     "full-example",
			header:   "doc.md",
			expected: "# Custom Header\n\nExample of 'foo_bar' module in `foo_bar.tf`.\n\n- list item 1\n- list item 2\n",
			wantErr:  false,
			errText:  "",
		},
		{
			name:     "load module header from path",
			path:     "full-example",
			header:   "doc.adoc",
			expected: "= Custom Header\n\nExample of 'foo_bar' module in `foo_bar.tf`.\n\n- list item 1\n- list item 2\n",
			wantErr:  false,
			errText:  "",
		},
		{
			name:     "load module header from path",
			path:     "full-example",
			header:   "doc.txt",
			expected: "# Custom Header\n\nExample of 'foo_bar' module in `foo_bar.tf`.\n\n- list item 1\n- list item 2\n",
			wantErr:  false,
			errText:  "",
		},
		{
			name:     "load module header from path",
			path:     "no-inputs",
			header:   "main.tf",
			expected: "",
			wantErr:  false,
			errText:  "",
		},
		{
			name:     "load module header from path",
			path:     "full-example",
			header:   "non-existent.tf",
			expected: "",
			wantErr:  true,
			errText:  "stat testdata/full-example/non-existent.tf: no such file or directory",
		},
		{
			name:     "load module header from path",
			path:     "full-example",
			header:   "wrong-formate.docx",
			expected: "",
			wantErr:  true,
			errText:  "only .adoc, .md, .tf and .txt formats are supported to read header from",
		},
		{
			name:     "load module header from path",
			path:     "full-example",
			header:   "",
			expected: "",
			wantErr:  true,
			errText:  "--header-from value is missing",
		},
		{
			name:     "load module header from path",
			path:     "empty-header",
			header:   "",
			expected: "",
			wantErr:  true,
			errText:  "--header-from value is missing",
		},
		{
			name:     "load module header from path",
			path:     "non-exist",
			header:   "",
			expected: "",
			wantErr:  true,
			errText:  "--header-from value is missing",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert := assert.New(t)
			options := &Options{Path: filepath.Join("testdata", tt.path), HeaderFromFile: tt.header, ShowHeader: true}
			actual, err := loadHeader(options)
			if tt.wantErr {
				assert.NotNil(err)
				assert.Equal(tt.errText, err.Error())
			} else {
				assert.Nil(err)
				assert.Equal(tt.expected, actual)
			}
		})
	}
}

func TestLoadInputs(t *testing.T) {
	type expected struct {
		inputs    int
		requireds int
		optionals int
	}
	tests := []struct {
		name     string
		path     string
		expected expected
	}{
		{
			name: "load module inputs from path",
			path: "full-example",
			expected: expected{
				inputs:    7,
				requireds: 2,
				optionals: 5,
			},
		},
		{
			name: "load module inputs from path",
			path: "no-required-inputs",
			expected: expected{
				inputs:    6,
				requireds: 0,
				optionals: 6,
			},
		},
		{
			name: "load module inputs from path",
			path: "no-optional-inputs",
			expected: expected{
				inputs:    6,
				requireds: 6,
				optionals: 0,
			},
		},
		{
			name: "load module inputs from path",
			path: "no-inputs",
			expected: expected{
				inputs:    0,
				requireds: 0,
				optionals: 0,
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert := assert.New(t)
			module, _ := loadModule(filepath.Join("testdata", tt.path))
			inputs, requireds, optionals := loadInputs(module)

			assert.Equal(tt.expected.inputs, len(inputs))
			assert.Equal(tt.expected.requireds, len(requireds))
			assert.Equal(tt.expected.optionals, len(optionals))
		})
	}
}

func TestLoadInputsLineEnding(t *testing.T) {
	tests := []struct {
		name     string
		path     string
		expected string
	}{
		{
			name:     "load module inputs from file with lf line ending",
			path:     "inputs-lf",
			expected: "The quick brown fox jumps\nover the lazy dog\n",
		},
		{
			name:     "load module inputs from file with crlf line ending",
			path:     "inputs-crlf",
			expected: "The quick brown fox jumps\nover the lazy dog\n",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert := assert.New(t)
			module, _ := loadModule(filepath.Join("testdata", tt.path))
			inputs, _, _ := loadInputs(module)

			assert.Equal(1, len(inputs))
			assert.Equal(tt.expected, string(inputs[0].Description))
		})
	}
}

func TestLoadOutputs(t *testing.T) {
	type expected struct {
		outputs int
	}
	tests := []struct {
		name     string
		path     string
		expected expected
	}{
		{
			name: "load module outputs from path",
			path: "full-example",
			expected: expected{
				outputs: 3,
			},
		},
		{
			name: "load module outputs from path",
			path: "no-outputs",
			expected: expected{
				outputs: 0,
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert := assert.New(t)
			options := NewOptions()
			module, _ := loadModule(filepath.Join("testdata", tt.path))
			outputs, err := loadOutputs(module, options)

			assert.Nil(err)
			assert.Equal(tt.expected.outputs, len(outputs))

			for _, v := range outputs {
				assert.Equal(false, v.ShowValue)
			}
		})
	}
}

func TestLoadOutputsValues(t *testing.T) {
	type expected struct {
		outputs int
	}
	tests := []struct {
		name       string
		path       string
		outputPath string
		expected   expected
		wantErr    bool
	}{
		{
			name:       "load module outputs with values from path",
			path:       "full-example",
			outputPath: "output-values.json",
			expected: expected{
				outputs: 3,
			},
			wantErr: false,
		},
		{
			name:       "load module outputs with values from path",
			path:       "full-example",
			outputPath: "no-file.json",
			expected:   expected{},
			wantErr:    true,
		},
		{
			name:       "load module outputs with values from path",
			path:       "no-outputs",
			outputPath: "no-file.json",
			expected:   expected{},
			wantErr:    true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert := assert.New(t)
			options, _ := NewOptions().With(&Options{
				OutputValues:     true,
				OutputValuesPath: filepath.Join("testdata", tt.path, tt.outputPath),
			})
			module, _ := loadModule(filepath.Join("testdata", tt.path))
			outputs, err := loadOutputs(module, options)

			if tt.wantErr {
				assert.NotNil(err)
			} else {
				assert.Nil(err)
				assert.Equal(tt.expected.outputs, len(outputs))

				for _, v := range outputs {
					assert.Equal(true, v.ShowValue)
				}
			}
		})
	}
}

func TestLoadProviders(t *testing.T) {
	type expected struct {
		providers int
	}
	tests := []struct {
		name     string
		path     string
		expected expected
	}{
		{
			name: "load module providers from path",
			path: "full-example",
			expected: expected{
				providers: 3,
			},
		},
		{
			name: "load module providers from path",
			path: "no-providers",
			expected: expected{
				providers: 0,
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert := assert.New(t)
			module, _ := loadModule(filepath.Join("testdata", tt.path))
			providers := loadProviders(module)

			assert.Equal(tt.expected.providers, len(providers))
		})
	}
}

func TestLoadComments(t *testing.T) {
	tests := []struct {
		name       string
		path       string
		fileName   string
		lineNumber int
		expected   string
	}{
		{
			name:       "load resource comment from file",
			path:       "full-example",
			fileName:   "variables.tf",
			lineNumber: 2,
			expected:   "D description",
		},
		{
			name:       "load resource comment from file",
			path:       "full-example",
			fileName:   "variables.tf",
			lineNumber: 16,
			expected:   "A Description in multiple lines",
		},
		{
			name:       "load resource comment from file with wrong line number",
			path:       "full-example",
			fileName:   "variables.tf",
			lineNumber: 100,
			expected:   "",
		},
		{
			name:       "load resource comment from non existing file",
			path:       "full-example",
			fileName:   "non-exist.tf",
			lineNumber: 5,
			expected:   "",
		},
		{
			name:       "load resource comment from non existing file",
			path:       "non-exist",
			fileName:   "variables.tf",
			lineNumber: 5,
			expected:   "",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert := assert.New(t)
			actual := loadComments(filepath.Join("testdata", tt.path, tt.fileName), tt.lineNumber)
			assert.Equal(tt.expected, actual)
		})
	}
}

func TestSortItems(t *testing.T) {
	type expected struct {
		inputs    []string
		required  []string
		optional  []string
		outputs   []string
		providers []string
	}
	tests := []struct {
		name     string
		path     string
		sort     *SortBy
		expected expected
	}{
		{
			name: "sort module items",
			path: "full-example",
			sort: &SortBy{Name: false, Required: false, Type: false},
			expected: expected{
				inputs:    []string{"D", "B", "E", "A", "C", "F", "G"},
				required:  []string{"A", "F"},
				optional:  []string{"D", "B", "E", "C", "G"},
				outputs:   []string{"C", "A", "B"},
				providers: []string{"tls", "aws", "null"},
			},
		},
		{
			name: "sort module items",
			path: "full-example",
			sort: &SortBy{Name: true, Required: false, Type: false},
			expected: expected{
				inputs:    []string{"A", "B", "C", "D", "E", "F", "G"},
				required:  []string{"A", "F"},
				optional:  []string{"B", "C", "D", "E", "G"},
				outputs:   []string{"A", "B", "C"},
				providers: []string{"aws", "null", "tls"},
			},
		},
		{
			name: "sort module items",
			path: "full-example",
			sort: &SortBy{Name: false, Required: true, Type: false},
			expected: expected{
				inputs:    []string{"D", "B", "E", "A", "C", "F", "G"},
				required:  []string{"A", "F"},
				optional:  []string{"D", "B", "E", "C", "G"},
				outputs:   []string{"C", "A", "B"},
				providers: []string{"tls", "aws", "null"},
			},
		},
		{
			name: "sort module items",
			path: "full-example",
			sort: &SortBy{Name: false, Required: false, Type: true},
			expected: expected{
				inputs:    []string{"A", "F", "G", "B", "C", "D", "E"},
				required:  []string{"A", "F"},
				optional:  []string{"G", "B", "C", "D", "E"},
				outputs:   []string{"A", "B", "C"},
				providers: []string{"aws", "null", "tls"},
			},
		},
		{
			name: "sort module items",
			path: "full-example",
			sort: &SortBy{Name: true, Required: true, Type: false},
			expected: expected{
				inputs:    []string{"A", "F", "B", "C", "D", "E", "G"},
				required:  []string{"A", "F"},
				optional:  []string{"B", "C", "D", "E", "G"},
				outputs:   []string{"A", "B", "C"},
				providers: []string{"aws", "null", "tls"},
			},
		},
		{
			name: "sort module items",
			path: "full-example",
			sort: &SortBy{Name: true, Required: false, Type: true},
			expected: expected{
				inputs:    []string{"A", "F", "G", "B", "C", "D", "E"},
				required:  []string{"A", "F"},
				optional:  []string{"G", "B", "C", "D", "E"},
				outputs:   []string{"A", "B", "C"},
				providers: []string{"aws", "null", "tls"},
			},
		},
		{
			name: "sort module items",
			path: "full-example",
			sort: &SortBy{Name: false, Required: true, Type: true},
			expected: expected{
				inputs:    []string{"A", "F", "G", "B", "C", "D", "E"},
				required:  []string{"A", "F"},
				optional:  []string{"G", "B", "C", "D", "E"},
				outputs:   []string{"A", "B", "C"},
				providers: []string{"aws", "null", "tls"},
			},
		},
		{
			name: "sort module items",
			path: "full-example",
			sort: &SortBy{Name: true, Required: true, Type: true},
			expected: expected{
				inputs:    []string{"A", "F", "G", "B", "C", "D", "E"},
				required:  []string{"A", "F"},
				optional:  []string{"G", "B", "C", "D", "E"},
				outputs:   []string{"A", "B", "C"},
				providers: []string{"aws", "null", "tls"},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert := assert.New(t)
			path := filepath.Join("testdata", tt.path)
			options, _ := NewOptions().With(&Options{
				Path:   path,
				SortBy: tt.sort,
			})
			tfmodule, _ := loadModule(path)
			module, err := loadModuleItems(tfmodule, options)

			assert.Nil(err)
			sortItems(module, tt.sort)

			for i, v := range module.Inputs {
				assert.Equal(tt.expected.inputs[i], v.Name)
			}
			for i, v := range module.RequiredInputs {
				assert.Equal(tt.expected.required[i], v.Name)
			}
			for i, v := range module.OptionalInputs {
				assert.Equal(tt.expected.optional[i], v.Name)
			}
			for i, v := range module.Outputs {
				assert.Equal(tt.expected.outputs[i], v.Name)
			}
			for i, v := range module.Providers {
				assert.Equal(tt.expected.providers[i], v.Name)
			}
		})
	}
}
