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

package json

import (
	"math/big"
	"testing"

	"github.com/go-test/deep"
	"github.com/hashicorp/hcl/v2"
)

func init() {
	deep.MaxDepth = 999
}

func TestParse(t *testing.T) {
	tests := []struct {
		Input     string
		Want      node
		DiagCount int
	}{
		// Simple, single-token constructs
		{
			`true`,
			&booleanVal{
				Value: true,
				SrcRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 5, Byte: 4},
				},
			},
			0,
		},
		{
			`false`,
			&booleanVal{
				Value: false,
				SrcRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 6, Byte: 5},
				},
			},
			0,
		},
		{
			`null`,
			&nullVal{
				SrcRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 5, Byte: 4},
				},
			},
			0,
		},
		{
			`undefined`,
			invalidVal{hcl.Range{
				Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
				End:   hcl.Pos{Line: 1, Column: 10, Byte: 9},
			}},
			1,
		},
		{
			`flase`,
			invalidVal{hcl.Range{
				Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
				End:   hcl.Pos{Line: 1, Column: 6, Byte: 5},
			}},
			1,
		},
		{
			`"hello"`,
			&stringVal{
				Value: "hello",
				SrcRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 8, Byte: 7},
				},
			},
			0,
		},
		{
			`"hello\nworld"`,
			&stringVal{
				Value: "hello\nworld",
				SrcRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 15, Byte: 14},
				},
			},
			0,
		},
		{
			`"hello \"world\""`,
			&stringVal{
				Value: `hello "world"`,
				SrcRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 18, Byte: 17},
				},
			},
			0,
		},
		{
			`"hello \\"`,
			&stringVal{
				Value: "hello \\",
				SrcRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 11, Byte: 10},
				},
			},
			0,
		},
		{
			`"hello`,
			invalidVal{hcl.Range{
				Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
				End:   hcl.Pos{Line: 1, Column: 7, Byte: 6},
			}},
			1,
		},
		{
			`"he\llo"`,
			invalidVal{hcl.Range{
				Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
				End:   hcl.Pos{Line: 1, Column: 9, Byte: 8},
			}},
			1,
		},
		{
			`1`,
			&numberVal{
				Value: mustBigFloat("1"),
				SrcRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
				},
			},
			0,
		},
		{
			`1.2`,
			&numberVal{
				Value: mustBigFloat("1.2"),
				SrcRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 4, Byte: 3},
				},
			},
			0,
		},
		{
			`-1`,
			&numberVal{
				Value: mustBigFloat("-1"),
				SrcRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 3, Byte: 2},
				},
			},
			0,
		},
		{
			`1.2e5`,
			&numberVal{
				Value: mustBigFloat("120000"),
				SrcRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 6, Byte: 5},
				},
			},
			0,
		},
		{
			`1.2e+5`,
			&numberVal{
				Value: mustBigFloat("120000"),
				SrcRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 7, Byte: 6},
				},
			},
			0,
		},
		{
			`1.2e-5`,
			&numberVal{
				Value: mustBigFloat("1.2e-5"),
				SrcRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 7, Byte: 6},
				},
			},
			0,
		},
		{
			`.1`,
			invalidVal{hcl.Range{
				Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
				End:   hcl.Pos{Line: 1, Column: 3, Byte: 2},
			}},
			1,
		},
		{
			`+2`,
			invalidVal{hcl.Range{
				Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
				End:   hcl.Pos{Line: 1, Column: 3, Byte: 2},
			}},
			1,
		},
		{
			`1 2`,
			&numberVal{
				Value: mustBigFloat("1"),
				SrcRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
				},
			},
			1,
		},

		// Objects
		{
			`{"hello": true}`,
			&objectVal{
				Attrs: []*objectAttr{
					{
						Name: "hello",
						Value: &booleanVal{
							Value: true,
							SrcRange: hcl.Range{
								Start: hcl.Pos{Line: 1, Column: 11, Byte: 10},
								End:   hcl.Pos{Line: 1, Column: 15, Byte: 14},
							},
						},
						NameRange: hcl.Range{
							Start: hcl.Pos{Line: 1, Column: 2, Byte: 1},
							End:   hcl.Pos{Line: 1, Column: 9, Byte: 8},
						},
					},
				},
				SrcRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 16, Byte: 15},
				},
				OpenRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
				},
				CloseRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 15, Byte: 14},
					End:   hcl.Pos{Line: 1, Column: 16, Byte: 15},
				},
			},
			0,
		},
		{
			`{"hello": true, "bye": false}`,
			&objectVal{
				Attrs: []*objectAttr{
					{
						Name: "hello",
						Value: &booleanVal{
							Value: true,
							SrcRange: hcl.Range{
								Start: hcl.Pos{Line: 1, Column: 11, Byte: 10},
								End:   hcl.Pos{Line: 1, Column: 15, Byte: 14},
							},
						},
						NameRange: hcl.Range{
							Start: hcl.Pos{Line: 1, Column: 2, Byte: 1},
							End:   hcl.Pos{Line: 1, Column: 9, Byte: 8},
						},
					},
					{
						Name: "bye",
						Value: &booleanVal{
							Value: false,
							SrcRange: hcl.Range{
								Start: hcl.Pos{Line: 1, Column: 24, Byte: 23},
								End:   hcl.Pos{Line: 1, Column: 29, Byte: 28},
							},
						},
						NameRange: hcl.Range{
							Start: hcl.Pos{Line: 1, Column: 17, Byte: 16},
							End:   hcl.Pos{Line: 1, Column: 22, Byte: 21},
						},
					},
				},
				SrcRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 30, Byte: 29},
				},
				OpenRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
				},
				CloseRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 29, Byte: 28},
					End:   hcl.Pos{Line: 1, Column: 30, Byte: 29},
				},
			},
			0,
		},
		{
			`{}`,
			&objectVal{
				Attrs: []*objectAttr{},
				SrcRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 3, Byte: 2},
				},
				OpenRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
				},
				CloseRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 2, Byte: 1},
					End:   hcl.Pos{Line: 1, Column: 3, Byte: 2},
				},
			},
			0,
		},
		{
			`{"hello":true`,
			invalidVal{hcl.Range{
				Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
				End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
			}},
			1,
		},
		{
			`{"hello":true]`,
			invalidVal{hcl.Range{
				Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
				End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
			}},
			1,
		},
		{
			`{"hello":true,}`,
			invalidVal{hcl.Range{
				Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
				End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
			}},
			1,
		},
		{
			`{true:false}`,
			invalidVal{hcl.Range{
				Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
				End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
			}},
			1,
		},
		{
			`{"hello": true, "hello": true}`,
			&objectVal{
				Attrs: []*objectAttr{
					{
						Name: "hello",
						Value: &booleanVal{
							Value: true,
							SrcRange: hcl.Range{
								Start: hcl.Pos{Line: 1, Column: 11, Byte: 10},
								End:   hcl.Pos{Line: 1, Column: 15, Byte: 14},
							},
						},
						NameRange: hcl.Range{
							Start: hcl.Pos{Line: 1, Column: 2, Byte: 1},
							End:   hcl.Pos{Line: 1, Column: 9, Byte: 8},
						},
					},
					{
						Name: "hello",
						Value: &booleanVal{
							Value: true,
							SrcRange: hcl.Range{
								Start: hcl.Pos{Line: 1, Column: 26, Byte: 25},
								End:   hcl.Pos{Line: 1, Column: 30, Byte: 29},
							},
						},
						NameRange: hcl.Range{
							Start: hcl.Pos{Line: 1, Column: 17, Byte: 16},
							End:   hcl.Pos{Line: 1, Column: 24, Byte: 23},
						},
					},
				},
				SrcRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 31, Byte: 30},
				},
				OpenRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
				},
				CloseRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 30, Byte: 29},
					End:   hcl.Pos{Line: 1, Column: 31, Byte: 30},
				},
			},
			0,
		},
		{
			`{"hello": true, "hello": true, "hello", true}`,
			invalidVal{hcl.Range{
				Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
				End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
			}},
			1, // comma used where colon is expected
		},
		{
			`{"hello", "world"}`,
			invalidVal{hcl.Range{
				Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
				End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
			}},
			1,
		},
		{
			`[]`,
			&arrayVal{
				Values: []node{},
				SrcRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 3, Byte: 2},
				},
				OpenRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
				},
			},
			0,
		},
		{
			`[true]`,
			&arrayVal{
				Values: []node{
					&booleanVal{
						Value: true,
						SrcRange: hcl.Range{
							Start: hcl.Pos{Line: 1, Column: 2, Byte: 1},
							End:   hcl.Pos{Line: 1, Column: 6, Byte: 5},
						},
					},
				},
				SrcRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 7, Byte: 6},
				},
				OpenRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
				},
			},
			0,
		},
		{
			`[true, false]`,
			&arrayVal{
				Values: []node{
					&booleanVal{
						Value: true,
						SrcRange: hcl.Range{
							Start: hcl.Pos{Line: 1, Column: 2, Byte: 1},
							End:   hcl.Pos{Line: 1, Column: 6, Byte: 5},
						},
					},
					&booleanVal{
						Value: false,
						SrcRange: hcl.Range{
							Start: hcl.Pos{Line: 1, Column: 8, Byte: 7},
							End:   hcl.Pos{Line: 1, Column: 13, Byte: 12},
						},
					},
				},
				SrcRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 14, Byte: 13},
				},
				OpenRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
				},
			},
			0,
		},
		{
			`[[]]`,
			&arrayVal{
				Values: []node{
					&arrayVal{
						Values: []node{},
						SrcRange: hcl.Range{
							Start: hcl.Pos{Line: 1, Column: 2, Byte: 1},
							End:   hcl.Pos{Line: 1, Column: 4, Byte: 3},
						},
						OpenRange: hcl.Range{
							Start: hcl.Pos{Line: 1, Column: 2, Byte: 1},
							End:   hcl.Pos{Line: 1, Column: 3, Byte: 2},
						},
					},
				},
				SrcRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 5, Byte: 4},
				},
				OpenRange: hcl.Range{
					Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
					End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
				},
			},
			0,
		},
		{
			`[`,
			invalidVal{hcl.Range{
				Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
				End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
			}},
			2,
		},
		{
			`[true`,
			invalidVal{hcl.Range{
				Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
				End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
			}},
			1,
		},
		{
			`]`,
			invalidVal{hcl.Range{
				Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
				End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
			}},
			1,
		},
		{
			`[true,]`,
			invalidVal{hcl.Range{
				Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
				End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
			}},
			1,
		},
		{
			`[[],]`,
			invalidVal{hcl.Range{
				Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
				End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
			}},
			1,
		},
		{
			`["hello":true]`,
			invalidVal{hcl.Range{
				Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
				End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
			}},
			1,
		},
		{
			`[true}`,
			invalidVal{hcl.Range{
				Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
				End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
			}},
			1,
		},
		{
			`{"wrong"=true}`,
			invalidVal{hcl.Range{
				Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
				End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
			}},
			1,
		},
		{
			`{"wrong" = true}`,
			invalidVal{hcl.Range{
				Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
				End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
			}},
			1,
		},
		{
			`{"wrong" true}`,
			invalidVal{hcl.Range{
				Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
				End:   hcl.Pos{Line: 1, Column: 2, Byte: 1},
			}},
			1,
		},
	}

	for _, test := range tests {
		t.Run(test.Input, func(t *testing.T) {
			got, diag := parseFileContent([]byte(test.Input), "", hcl.Pos{Byte: 0, Line: 1, Column: 1})

			if len(diag) != test.DiagCount {
				t.Errorf("got %d diagnostics; want %d", len(diag), test.DiagCount)
				for _, d := range diag {
					t.Logf("  - %s", d.Error())
				}
			}

			if diff := deep.Equal(got, test.Want); diff != nil {
				for _, problem := range diff {
					t.Error(problem)
				}
			}
		})
	}
}

func TestParseWithPos(t *testing.T) {
	tests := []struct {
		Input     string
		StartPos  hcl.Pos
		Want      node
		DiagCount int
	}{
		// Simple, single-token constructs
		{
			`true`,
			hcl.Pos{Byte: 0, Line: 3, Column: 10},
			&booleanVal{
				Value: true,
				SrcRange: hcl.Range{
					Start: hcl.Pos{Line: 3, Column: 10, Byte: 0},
					End:   hcl.Pos{Line: 3, Column: 14, Byte: 4},
				},
			},
			0,
		},
	}

	for _, test := range tests {
		t.Run(test.Input, func(t *testing.T) {
			got, diag := parseFileContent([]byte(test.Input), "", test.StartPos)

			if len(diag) != test.DiagCount {
				t.Errorf("got %d diagnostics; want %d", len(diag), test.DiagCount)
				for _, d := range diag {
					t.Logf("  - %s", d.Error())
				}
			}

			if diff := deep.Equal(got, test.Want); diff != nil {
				for _, problem := range diff {
					t.Error(problem)
				}
			}
		})
	}
}

func mustBigFloat(s string) *big.Float {
	f, _, err := (&big.Float{}).Parse(s, 10)
	if err != nil {
		panic(err)
	}
	return f
}
