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

package hclwrite

import (
	"fmt"
	"testing"

	"reflect"

	"github.com/davecgh/go-spew/spew"
	"github.com/hashicorp/hcl/v2/hclsyntax"
)

func TestFormat(t *testing.T) {
	tests := []struct {
		input string
		want  string
	}{
		{
			``,
			``,
		},
		{
			`a=1`,
			`a = 1`,
		},
		{
			`a=b.c`,
			`a = b.c`,
		},
		{
			`a=b[c]`,
			`a = b[c]`,
		},
		{
			`a=b()[c]`,
			`a = b()[c]`,
		},
		{
			`a=["hello"][0]`,
			`a = ["hello"][0]`,
		},
		{
			`( a+2 )`,
			`(a + 2)`,
		},
		{
			`( a*2 )`,
			`(a * 2)`,
		},
		{
			`( a+-2 )`,
			`(a + -2)`,
		},
		{
			`( a*-2 )`,
			`(a * -2)`,
		},
		{
			`(-2+1)`,
			`(-2 + 1)`,
		},
		{
			`foo(1, -2,a*b, b,c)`,
			`foo(1, -2, a * b, b, c)`,
		},
		{
			`foo(a,b...)`,
			`foo(a, b...)`,
		},
		{
			`! true`,
			`!true`,
		},
		{
			`a="hello ${ name }"`,
			`a = "hello ${name}"`,
		},
		{
			`a="hello ${~ name ~}"`,
			`a = "hello ${~name~}"`,
		},
		{
			`a="${b}${c}${ d } ${e}"`,
			`a = "${b}${c}${d} ${e}"`,
		},
		{
			`"%{if true}${var.foo}%{endif}"`,
			`"%{if true}${var.foo}%{endif}"`,
		},
		{
			`b{}`,
			`b {}`,
		},
		{
			`
"${
hello
}"
`,
			`
"${
  hello
}"
`,
		},
		{
			`
foo(
1,
- 2,
a*b,
b,
c,
)
`,
			`
foo(
  1,
  -2,
  a * b,
  b,
  c,
)
`,
		},
		{
			`a?b:c`,
			`a ? b : c`,
		},
		{
			`[ [ ] ]`,
			`[[]]`,
		},
		{
			`[for x in y : x]`,
			`[for x in y : x]`,
		},
		{
			`[for x in [y] : x]`,
			`[for x in [y] : x]`,
		},
		{
			`
[
[
a
]
]
`,
			`
[
  [
    a
  ]
]
`,
		},
		{
			`
[[
a
]]
`,
			`
[[
  a
]]
`,
		},
		{
			`
[[
[
a
]
]]
`,
			`
[[
  [
    a
  ]
]]
`,
		},
		{
			// degenerate case with asymmetrical brackets
			`
[[
[
a
]]
]
`,
			`
[[
  [
    a
  ]]
]
`,
		},
		{
			`
b {
a = 1
}
`,
			`
b {
  a = 1
}
`,
		},
		{
			`
b {a = 1}
`,
			`
b { a = 1 }
`,
		},
		{
			`
a = 1
bungle = 2
`,
			`
a      = 1
bungle = 2
`,
		},
		{
			`
a = 1

bungle = 2
`,
			`
a = 1

bungle = 2
`,
		},
		{
			`
a = 1 # foo
bungle = 2
`,
			`
a      = 1 # foo
bungle = 2
`,
		},
		{
			`
a = 1 # foo
bungle = "bonce" # baz
`,
			`
a      = 1       # foo
bungle = "bonce" # baz
`,
		},
		{
			`
# here we go
a = 1 # foo
bungle = "bonce" # baz
`,
			`
# here we go
a      = 1       # foo
bungle = "bonce" # baz
`,
		},
		{
			`
foo {} # here we go
a = 1 # foo
bungle = "bonce" # baz
`,
			`
foo {}           # here we go
a      = 1       # foo
bungle = "bonce" # baz
`,
		},
		{
			`
a = 1 # foo
bungle = "bonce" # baz
zebra = "striped" # baz
`,
			`
a      = 1         # foo
bungle = "bonce"   # baz
zebra  = "striped" # baz
`,
		},
		{
			`
a = 1 # foo
bungle = (
    "bonce"
) # baz
zebra = "striped" # baz
`,
			`
a = 1 # foo
bungle = (
  "bonce"
)                 # baz
zebra = "striped" # baz
`,
		},
		{
			`
a="apple"# foo
bungle=(# woo parens
"bonce"
)# baz
zebra="striped"# baz
`,
			`
a = "apple" # foo
bungle = (  # woo parens
  "bonce"
)                 # baz
zebra = "striped" # baz
`,
		},
		{
			`
𝒜 = 1 # foo
bungle = "🇬🇧" # baz
zebra = "striped" # baz
`,
			`
𝒜      = 1         # foo
bungle = "🇬🇧"       # baz
zebra  = "striped" # baz
`,
		},
		{
			`
foo {
# ...
}
`,
			`
foo {
  # ...
}
`,
		},
		{
			`
foo = {
# ...
}
`,
			`
foo = {
  # ...
}
`,
		},
		{
			`
foo = [
# ...
]
`,
			`
foo = [
  # ...
]
`,
		},
		{
			`
foo = [{
# ...
}]
`,
			`
foo = [{
  # ...
}]
`,
		},
		{
			`
foo {
bar {
# ...
}
}
`,
			`
foo {
  bar {
    # ...
  }
}
`,
		},
		{
			`
foo {
bar = {
# ...
}
}
`,
			`
foo {
  bar = {
    # ...
  }
}
`,
		},
		{
			`
foo {
bar = [
# ...
]
}
`,
			`
foo {
  bar = [
    # ...
  ]
}
`,
		},
		{
			`
foo {
bar = <<EOT
Foo bar baz
EOT
}
`,
			`
foo {
  bar = <<EOT
Foo bar baz
EOT
}
`,
		},
		{
			`
foo {
bar = <<-EOT
Foo bar baz
EOT
}
`,
			`
foo {
  bar = <<-EOT
Foo bar baz
EOT
}
`,
		},
		{
			`
foo {
bar = <<-EOT
  Foo bar baz
EOT
}
`,
			`
foo {
  bar = <<-EOT
  Foo bar baz
EOT
}
`,
		},
		{
			`
foo {
bar = <<-EOT
  blahblahblah = x
EOT
}
`,
			`
foo {
  bar = <<-EOT
  blahblahblah = x
EOT
}
`,
		},
		{
			`
foo {
bar = <<-EOT
  ${{ blahblahblah = x }}
EOT
}
`,
			`
foo {
  bar = <<-EOT
  ${ { blahblahblah = x } }
EOT
}
`,
		},
		{
			`
foo {
  bar = <<-EOT
  ${a}${b}${ c } ${d}
EOT
}
`,
			`
foo {
  bar = <<-EOT
  ${a}${b}${c} ${d}
EOT
}
`,
		},
		{
			`
foo {
bar = <<EOT
Foo bar baz
EOT
}

baz {
default="string"
}
`,
			`
foo {
  bar = <<EOT
Foo bar baz
EOT
}

baz {
  default = "string"
}
`,
		},
		{
			`
foo {
bar = <<EOT
Foo bar baz
EOT
baz = <<EOT
Foo bar baz
EOT
}

bar {
foo = "bar"
}
`,
			`
foo {
  bar = <<EOT
Foo bar baz
EOT
  baz = <<EOT
Foo bar baz
EOT
}

bar {
  foo = "bar"
}
`,
		},
		{
			`
module "foo" {
foo = <<EOF
5
EOF
}

module "x" {
a = "b"
abcde = "456"
}`,
			`
module "foo" {
  foo = <<EOF
5
EOF
}

module "x" {
  a     = "b"
  abcde = "456"
}`,
		},
		{
			`attr = provider::framework::example()`,
			`attr = provider::framework::example()`,
		},
		{
			`attr = provider :: framework :: example()`,
			`attr = provider::framework::example()`,
		},
		{
			`attr = provider ::framework:: example()`,
			`attr = provider::framework::example()`,
		},
		{
			// This is invalid syntax so formatting it with spaces
			// does not have any meaning other than to make the fact more visible
			`attr = provider::+example()`,
			`attr = provider:: + example()`,
		},
	}

	for i, test := range tests {
		t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
			tokens := lexConfig([]byte(test.input))
			format(tokens)
			t.Logf("tokens %s\n", spew.Sdump(tokens))
			got := string(tokens.Bytes())

			if got != test.want {
				t.Errorf("wrong result\ninput:\n%s\ngot:\n%s\nwant:\n%s", test.input, got, test.want)
			}
		})
	}

}

func TestLinesForFormat(t *testing.T) {
	tests := []struct {
		tokens Tokens
		want   []formatLine
	}{
		{
			Tokens{
				&Token{Type: hclsyntax.TokenEOF},
			},
			[]formatLine{
				{
					lead: Tokens{},
				},
			},
		},
		{
			Tokens{
				&Token{Type: hclsyntax.TokenIdent},
				&Token{Type: hclsyntax.TokenEOF},
			},
			[]formatLine{
				{
					lead: Tokens{
						&Token{Type: hclsyntax.TokenIdent},
					},
				},
			},
		},
		{
			Tokens{
				&Token{Type: hclsyntax.TokenIdent},
				&Token{Type: hclsyntax.TokenNewline},
				&Token{Type: hclsyntax.TokenNumberLit},
				&Token{Type: hclsyntax.TokenEOF},
			},
			[]formatLine{
				{
					lead: Tokens{
						&Token{Type: hclsyntax.TokenIdent},
						&Token{Type: hclsyntax.TokenNewline},
					},
				},
				{
					lead: Tokens{
						&Token{Type: hclsyntax.TokenNumberLit},
					},
				},
			},
		},
		{
			Tokens{
				&Token{Type: hclsyntax.TokenIdent},
				&Token{Type: hclsyntax.TokenComment, Bytes: []byte("#foo\n")},
				&Token{Type: hclsyntax.TokenNumberLit},
				&Token{Type: hclsyntax.TokenEOF},
			},
			[]formatLine{
				{
					lead: Tokens{
						&Token{Type: hclsyntax.TokenIdent},
					},
					comment: Tokens{
						&Token{Type: hclsyntax.TokenComment, Bytes: []byte("#foo\n")},
					},
				},
				{
					lead: Tokens{
						&Token{Type: hclsyntax.TokenNumberLit},
					},
				},
			},
		},
		{
			Tokens{
				&Token{Type: hclsyntax.TokenIdent},
				&Token{Type: hclsyntax.TokenEqual},
				&Token{Type: hclsyntax.TokenNumberLit},
				&Token{Type: hclsyntax.TokenEOF},
			},
			[]formatLine{
				{
					lead: Tokens{
						&Token{Type: hclsyntax.TokenIdent},
					},
					assign: Tokens{
						&Token{Type: hclsyntax.TokenEqual},
						&Token{Type: hclsyntax.TokenNumberLit},
					},
				},
			},
		},
		{
			Tokens{
				&Token{Type: hclsyntax.TokenIdent},
				&Token{Type: hclsyntax.TokenEqual},
				&Token{Type: hclsyntax.TokenNumberLit},
				&Token{Type: hclsyntax.TokenComment, Bytes: []byte("#foo\n")},
				&Token{Type: hclsyntax.TokenEOF},
			},
			[]formatLine{
				{
					lead: Tokens{
						&Token{Type: hclsyntax.TokenIdent},
					},
					assign: Tokens{
						&Token{Type: hclsyntax.TokenEqual},
						&Token{Type: hclsyntax.TokenNumberLit},
					},
					comment: Tokens{
						&Token{Type: hclsyntax.TokenComment, Bytes: []byte("#foo\n")},
					},
				},
				{
					lead: Tokens{},
				},
			},
		},
		{
			Tokens{
				// A comment goes into a comment cell only if it is after
				// some non-comment tokens, since whole-line comments must
				// stay flush with the indent level.
				&Token{Type: hclsyntax.TokenComment, Bytes: []byte("#foo\n")},
				&Token{Type: hclsyntax.TokenEOF},
			},
			[]formatLine{
				{
					lead: Tokens{
						&Token{Type: hclsyntax.TokenComment, Bytes: []byte("#foo\n")},
					},
				},
				{
					lead: Tokens{},
				},
			},
		},
	}

	for i, test := range tests {
		t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
			got := linesForFormat(test.tokens)

			if !reflect.DeepEqual(got, test.want) {
				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.want)
			}
		})
	}
}
