package json

import (
	"fmt"
	"reflect"
	"testing"

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

func TestBodyPartialContent(t *testing.T) {
	tests := []struct {
		src       string
		schema    *hcl.BodySchema
		want      *hcl.BodyContent
		diagCount int
	}{
		{
			`{}`,
			&hcl.BodySchema{},
			&hcl.BodyContent{
				Attributes: map[string]*hcl.Attribute{},
				MissingItemRange: hcl.Range{
					Filename: "test.json",
					Start:    hcl.Pos{Line: 1, Column: 2, Byte: 1},
					End:      hcl.Pos{Line: 1, Column: 3, Byte: 2},
				},
			},
			0,
		},
		{
			`{"//": "comment that should be ignored"}`,
			&hcl.BodySchema{},
			&hcl.BodyContent{
				Attributes: map[string]*hcl.Attribute{},
				MissingItemRange: hcl.Range{
					Filename: "test.json",
					Start:    hcl.Pos{Line: 1, Column: 40, Byte: 39},
					End:      hcl.Pos{Line: 1, Column: 41, Byte: 40},
				},
			},
			0,
		},
		{
			`{"name":"Ermintrude"}`,
			&hcl.BodySchema{
				Attributes: []hcl.AttributeSchema{
					{
						Name: "name",
					},
				},
			},
			&hcl.BodyContent{
				Attributes: map[string]*hcl.Attribute{
					"name": &hcl.Attribute{
						Name: "name",
						Expr: &expression{
							src: &stringVal{
								Value: "Ermintrude",
								SrcRange: hcl.Range{
									Filename: "test.json",
									Start: hcl.Pos{
										Byte:   8,
										Line:   1,
										Column: 9,
									},
									End: hcl.Pos{
										Byte:   20,
										Line:   1,
										Column: 21,
									},
								},
							},
						},
						Range: hcl.Range{
							Filename: "test.json",
							Start: hcl.Pos{
								Byte:   1,
								Line:   1,
								Column: 2,
							},
							End: hcl.Pos{
								Byte:   20,
								Line:   1,
								Column: 21,
							},
						},
						NameRange: hcl.Range{
							Filename: "test.json",
							Start: hcl.Pos{
								Byte:   1,
								Line:   1,
								Column: 2,
							},
							End: hcl.Pos{
								Byte:   7,
								Line:   1,
								Column: 8,
							},
						},
					},
				},
				MissingItemRange: hcl.Range{
					Filename: "test.json",
					Start:    hcl.Pos{Line: 1, Column: 21, Byte: 20},
					End:      hcl.Pos{Line: 1, Column: 22, Byte: 21},
				},
			},
			0,
		},
		{
			`{"name":"Ermintrude"}`,
			&hcl.BodySchema{
				Attributes: []hcl.AttributeSchema{
					{
						Name:     "name",
						Required: true,
					},
					{
						Name:     "age",
						Required: true,
					},
				},
			},
			&hcl.BodyContent{
				Attributes: map[string]*hcl.Attribute{
					"name": &hcl.Attribute{
						Name: "name",
						Expr: &expression{
							src: &stringVal{
								Value: "Ermintrude",
								SrcRange: hcl.Range{
									Filename: "test.json",
									Start: hcl.Pos{
										Byte:   8,
										Line:   1,
										Column: 9,
									},
									End: hcl.Pos{
										Byte:   20,
										Line:   1,
										Column: 21,
									},
								},
							},
						},
						Range: hcl.Range{
							Filename: "test.json",
							Start: hcl.Pos{
								Byte:   1,
								Line:   1,
								Column: 2,
							},
							End: hcl.Pos{
								Byte:   20,
								Line:   1,
								Column: 21,
							},
						},
						NameRange: hcl.Range{
							Filename: "test.json",
							Start: hcl.Pos{
								Byte:   1,
								Line:   1,
								Column: 2,
							},
							End: hcl.Pos{
								Byte:   7,
								Line:   1,
								Column: 8,
							},
						},
					},
				},
				MissingItemRange: hcl.Range{
					Filename: "test.json",
					Start:    hcl.Pos{Line: 1, Column: 21, Byte: 20},
					End:      hcl.Pos{Line: 1, Column: 22, Byte: 21},
				},
			},
			1,
		},
		{
			`{"resource":{}}`,
			&hcl.BodySchema{
				Blocks: []hcl.BlockHeaderSchema{
					{
						Type: "resource",
					},
				},
			},
			&hcl.BodyContent{
				Attributes: map[string]*hcl.Attribute{},
				Blocks: hcl.Blocks{
					{
						Type:   "resource",
						Labels: []string{},
						Body: &body{
							obj: &objectVal{
								Attrs: map[string]*objectAttr{},
								SrcRange: hcl.Range{
									Filename: "test.json",
									Start: hcl.Pos{
										Byte:   12,
										Line:   1,
										Column: 13,
									},
									End: hcl.Pos{
										Byte:   14,
										Line:   1,
										Column: 15,
									},
								},
								OpenRange: hcl.Range{
									Filename: "test.json",
									Start: hcl.Pos{
										Byte:   12,
										Line:   1,
										Column: 13,
									},
									End: hcl.Pos{
										Byte:   13,
										Line:   1,
										Column: 14,
									},
								},
								CloseRange: hcl.Range{
									Filename: "test.json",
									Start: hcl.Pos{
										Byte:   13,
										Line:   1,
										Column: 14,
									},
									End: hcl.Pos{
										Byte:   14,
										Line:   1,
										Column: 15,
									},
								},
							},
						},

						DefRange: hcl.Range{
							Filename: "test.json",
							Start: hcl.Pos{
								Byte:   12,
								Line:   1,
								Column: 13,
							},
							End: hcl.Pos{
								Byte:   13,
								Line:   1,
								Column: 14,
							},
						},
						TypeRange: hcl.Range{
							Filename: "test.json",
							Start: hcl.Pos{
								Byte:   1,
								Line:   1,
								Column: 2,
							},
							End: hcl.Pos{
								Byte:   11,
								Line:   1,
								Column: 12,
							},
						},
						LabelRanges: []hcl.Range{},
					},
				},
				MissingItemRange: hcl.Range{
					Filename: "test.json",
					Start:    hcl.Pos{Line: 1, Column: 15, Byte: 14},
					End:      hcl.Pos{Line: 1, Column: 16, Byte: 15},
				},
			},
			0,
		},
		{
			`{"resource":[{},{}]}`,
			&hcl.BodySchema{
				Blocks: []hcl.BlockHeaderSchema{
					{
						Type: "resource",
					},
				},
			},
			&hcl.BodyContent{
				Attributes: map[string]*hcl.Attribute{},
				Blocks: hcl.Blocks{
					{
						Type:   "resource",
						Labels: []string{},
						Body: &body{
							obj: &objectVal{
								Attrs: map[string]*objectAttr{},
								SrcRange: hcl.Range{
									Filename: "test.json",
									Start: hcl.Pos{
										Byte:   13,
										Line:   1,
										Column: 14,
									},
									End: hcl.Pos{
										Byte:   15,
										Line:   1,
										Column: 16,
									},
								},
								OpenRange: hcl.Range{
									Filename: "test.json",
									Start: hcl.Pos{
										Byte:   13,
										Line:   1,
										Column: 14,
									},
									End: hcl.Pos{
										Byte:   14,
										Line:   1,
										Column: 15,
									},
								},
								CloseRange: hcl.Range{
									Filename: "test.json",
									Start: hcl.Pos{
										Byte:   14,
										Line:   1,
										Column: 15,
									},
									End: hcl.Pos{
										Byte:   15,
										Line:   1,
										Column: 16,
									},
								},
							},
						},

						DefRange: hcl.Range{
							Filename: "test.json",
							Start: hcl.Pos{
								Byte:   12,
								Line:   1,
								Column: 13,
							},
							End: hcl.Pos{
								Byte:   13,
								Line:   1,
								Column: 14,
							},
						},
						TypeRange: hcl.Range{
							Filename: "test.json",
							Start: hcl.Pos{
								Byte:   1,
								Line:   1,
								Column: 2,
							},
							End: hcl.Pos{
								Byte:   11,
								Line:   1,
								Column: 12,
							},
						},
						LabelRanges: []hcl.Range{},
					},
					{
						Type:   "resource",
						Labels: []string{},
						Body: &body{
							obj: &objectVal{
								Attrs: map[string]*objectAttr{},
								SrcRange: hcl.Range{
									Filename: "test.json",
									Start: hcl.Pos{
										Byte:   16,
										Line:   1,
										Column: 17,
									},
									End: hcl.Pos{
										Byte:   18,
										Line:   1,
										Column: 19,
									},
								},
								OpenRange: hcl.Range{
									Filename: "test.json",
									Start: hcl.Pos{
										Byte:   16,
										Line:   1,
										Column: 17,
									},
									End: hcl.Pos{
										Byte:   17,
										Line:   1,
										Column: 18,
									},
								},
								CloseRange: hcl.Range{
									Filename: "test.json",
									Start: hcl.Pos{
										Byte:   17,
										Line:   1,
										Column: 18,
									},
									End: hcl.Pos{
										Byte:   18,
										Line:   1,
										Column: 19,
									},
								},
							},
						},

						DefRange: hcl.Range{
							Filename: "test.json",
							Start: hcl.Pos{
								Byte:   12,
								Line:   1,
								Column: 13,
							},
							End: hcl.Pos{
								Byte:   13,
								Line:   1,
								Column: 14,
							},
						},
						TypeRange: hcl.Range{
							Filename: "test.json",
							Start: hcl.Pos{
								Byte:   1,
								Line:   1,
								Column: 2,
							},
							End: hcl.Pos{
								Byte:   11,
								Line:   1,
								Column: 12,
							},
						},
						LabelRanges: []hcl.Range{},
					},
				},
				MissingItemRange: hcl.Range{
					Filename: "test.json",
					Start:    hcl.Pos{Line: 1, Column: 20, Byte: 19},
					End:      hcl.Pos{Line: 1, Column: 21, Byte: 20},
				},
			},
			0,
		},
		{
			`{"resource":{"foo_instance":{"bar":{}}}}`,
			&hcl.BodySchema{
				Blocks: []hcl.BlockHeaderSchema{
					{
						Type:       "resource",
						LabelNames: []string{"type", "name"},
					},
				},
			},
			&hcl.BodyContent{
				Attributes: map[string]*hcl.Attribute{},
				Blocks: hcl.Blocks{
					{
						Type:   "resource",
						Labels: []string{"foo_instance", "bar"},
						Body: &body{
							obj: &objectVal{
								Attrs: map[string]*objectAttr{},
								SrcRange: hcl.Range{
									Filename: "test.json",
									Start: hcl.Pos{
										Byte:   35,
										Line:   1,
										Column: 36,
									},
									End: hcl.Pos{
										Byte:   37,
										Line:   1,
										Column: 38,
									},
								},
								OpenRange: hcl.Range{
									Filename: "test.json",
									Start: hcl.Pos{
										Byte:   35,
										Line:   1,
										Column: 36,
									},
									End: hcl.Pos{
										Byte:   36,
										Line:   1,
										Column: 37,
									},
								},
								CloseRange: hcl.Range{
									Filename: "test.json",
									Start: hcl.Pos{
										Byte:   36,
										Line:   1,
										Column: 37,
									},
									End: hcl.Pos{
										Byte:   37,
										Line:   1,
										Column: 38,
									},
								},
							},
						},

						DefRange: hcl.Range{
							Filename: "test.json",
							Start: hcl.Pos{
								Byte:   35,
								Line:   1,
								Column: 36,
							},
							End: hcl.Pos{
								Byte:   36,
								Line:   1,
								Column: 37,
							},
						},
						TypeRange: hcl.Range{
							Filename: "test.json",
							Start: hcl.Pos{
								Byte:   1,
								Line:   1,
								Column: 2,
							},
							End: hcl.Pos{
								Byte:   11,
								Line:   1,
								Column: 12,
							},
						},
						LabelRanges: []hcl.Range{
							{
								Filename: "test.json",
								Start: hcl.Pos{
									Byte:   13,
									Line:   1,
									Column: 14,
								},
								End: hcl.Pos{
									Byte:   27,
									Line:   1,
									Column: 28,
								},
							},
							{
								Filename: "test.json",
								Start: hcl.Pos{
									Byte:   29,
									Line:   1,
									Column: 30,
								},
								End: hcl.Pos{
									Byte:   34,
									Line:   1,
									Column: 35,
								},
							},
						},
					},
				},
				MissingItemRange: hcl.Range{
					Filename: "test.json",
					Start:    hcl.Pos{Line: 1, Column: 40, Byte: 39},
					End:      hcl.Pos{Line: 1, Column: 41, Byte: 40},
				},
			},
			0,
		},
		{
			`{"name":"Ermintrude"}`,
			&hcl.BodySchema{
				Blocks: []hcl.BlockHeaderSchema{
					{
						Type: "name",
					},
				},
			},
			&hcl.BodyContent{
				Attributes: map[string]*hcl.Attribute{},
				MissingItemRange: hcl.Range{
					Filename: "test.json",
					Start:    hcl.Pos{Line: 1, Column: 21, Byte: 20},
					End:      hcl.Pos{Line: 1, Column: 22, Byte: 21},
				},
			},
			1,
		},
	}

	for i, test := range tests {
		t.Run(fmt.Sprintf("%02d-%s", i, test.src), func(t *testing.T) {
			file, diags := Parse([]byte(test.src), "test.json")
			if len(diags) != 0 {
				t.Fatalf("Parse produced diagnostics: %s", diags)
			}
			got, _, diags := file.Body.PartialContent(test.schema)
			if len(diags) != test.diagCount {
				t.Errorf("Wrong number of diagnostics %d; want %d", len(diags), test.diagCount)
				for _, diag := range diags {
					t.Logf(" - %s", diag)
				}
			}

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

func TestBodyContent(t *testing.T) {
	// We test most of the functionality already in TestBodyPartialContent, so
	// this test focuses on the handling of extraneous attributes.
	tests := []struct {
		src       string
		schema    *hcl.BodySchema
		diagCount int
	}{
		{
			`{"unknown": true}`,
			&hcl.BodySchema{},
			1,
		},
		{
			`{"//": "comment that should be ignored"}`,
			&hcl.BodySchema{},
			0,
		},
		{
			`{"unknow": true}`,
			&hcl.BodySchema{
				Attributes: []hcl.AttributeSchema{
					{
						Name: "unknown",
					},
				},
			},
			1,
		},
		{
			`{"unknow": true, "unnown": true}`,
			&hcl.BodySchema{
				Attributes: []hcl.AttributeSchema{
					{
						Name: "unknown",
					},
				},
			},
			2,
		},
	}

	for i, test := range tests {
		t.Run(fmt.Sprintf("%02d-%s", i, test.src), func(t *testing.T) {
			file, diags := Parse([]byte(test.src), "test.json")
			if len(diags) != 0 {
				t.Fatalf("Parse produced diagnostics: %s", diags)
			}
			_, diags = file.Body.Content(test.schema)
			if len(diags) != test.diagCount {
				t.Errorf("Wrong number of diagnostics %d; want %d", len(diags), test.diagCount)
				for _, diag := range diags {
					t.Logf(" - %s", diag)
				}
			}
		})
	}
}

func TestJustAttributes(t *testing.T) {
	// We test most of the functionality already in TestBodyPartialContent, so
	// this test focuses on the handling of extraneous attributes.
	tests := []struct {
		src  string
		want hcl.Attributes
	}{
		{
			`{}`,
			map[string]*hcl.Attribute{},
		},
		{
			`{"foo": true}`,
			map[string]*hcl.Attribute{
				"foo": {
					Name: "foo",
					Expr: &expression{
						src: &booleanVal{
							Value: true,
							SrcRange: hcl.Range{
								Filename: "test.json",
								Start:    hcl.Pos{Byte: 8, Line: 1, Column: 9},
								End:      hcl.Pos{Byte: 12, Line: 1, Column: 13},
							},
						},
					},
					Range: hcl.Range{
						Filename: "test.json",
						Start:    hcl.Pos{Byte: 1, Line: 1, Column: 2},
						End:      hcl.Pos{Byte: 12, Line: 1, Column: 13},
					},
					NameRange: hcl.Range{
						Filename: "test.json",
						Start:    hcl.Pos{Byte: 1, Line: 1, Column: 2},
						End:      hcl.Pos{Byte: 6, Line: 1, Column: 7},
					},
				},
			},
		},
		{
			`{"//": "comment that should be ignored"}`,
			map[string]*hcl.Attribute{},
		},
	}

	for i, test := range tests {
		t.Run(fmt.Sprintf("%02d-%s", i, test.src), func(t *testing.T) {
			file, diags := Parse([]byte(test.src), "test.json")
			if len(diags) != 0 {
				t.Fatalf("Parse produced diagnostics: %s", diags)
			}
			got, diags := file.Body.JustAttributes()
			if len(diags) != 0 {
				t.Errorf("Wrong number of diagnostics %d; want %d", len(diags), 0)
				for _, diag := range diags {
					t.Logf(" - %s", diag)
				}
			}
			if !reflect.DeepEqual(got, test.want) {
				t.Errorf("wrong result\ngot:  %s\nwant: %s", spew.Sdump(got), spew.Sdump(test.want))
			}
		})
	}
}

func TestExpressionAsTraversal(t *testing.T) {
	e := &expression{
		src: &stringVal{
			Value: "foo.bar[0]",
		},
	}
	traversal := e.AsTraversal()
	if len(traversal) != 3 {
		t.Fatalf("incorrect traversal %#v; want length 3", traversal)
	}
}

func TestStaticExpressionList(t *testing.T) {
	e := &expression{
		src: &arrayVal{
			Values: []node{
				&stringVal{
					Value: "hello",
				},
			},
		},
	}
	exprs := e.ExprList()
	if len(exprs) != 1 {
		t.Fatalf("incorrect exprs %#v; want length 1", exprs)
	}
	if exprs[0].(*expression).src != e.src.(*arrayVal).Values[0] {
		t.Fatalf("wrong first expression node")
	}
}
