package hcldec

import (
	"fmt"
	"reflect"
	"testing"

	"github.com/apparentlymart/go-dump/dump"
	"github.com/zclconf/go-cty/cty"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hclsyntax"
)

// Verify that all of our spec types implement the necessary interfaces
var _ Spec = ObjectSpec(nil)
var _ Spec = TupleSpec(nil)
var _ Spec = (*AttrSpec)(nil)
var _ Spec = (*LiteralSpec)(nil)
var _ Spec = (*ExprSpec)(nil)
var _ Spec = (*BlockSpec)(nil)
var _ Spec = (*BlockListSpec)(nil)
var _ Spec = (*BlockSetSpec)(nil)
var _ Spec = (*BlockMapSpec)(nil)
var _ Spec = (*BlockAttrsSpec)(nil)
var _ Spec = (*BlockLabelSpec)(nil)
var _ Spec = (*DefaultSpec)(nil)
var _ Spec = (*TransformExprSpec)(nil)
var _ Spec = (*TransformFuncSpec)(nil)
var _ Spec = (*ValidateSpec)(nil)

var _ attrSpec = (*AttrSpec)(nil)
var _ attrSpec = (*DefaultSpec)(nil)

var _ blockSpec = (*BlockSpec)(nil)
var _ blockSpec = (*BlockListSpec)(nil)
var _ blockSpec = (*BlockSetSpec)(nil)
var _ blockSpec = (*BlockMapSpec)(nil)
var _ blockSpec = (*BlockAttrsSpec)(nil)
var _ blockSpec = (*DefaultSpec)(nil)

var _ specNeedingVariables = (*AttrSpec)(nil)
var _ specNeedingVariables = (*BlockSpec)(nil)
var _ specNeedingVariables = (*BlockListSpec)(nil)
var _ specNeedingVariables = (*BlockSetSpec)(nil)
var _ specNeedingVariables = (*BlockMapSpec)(nil)
var _ specNeedingVariables = (*BlockAttrsSpec)(nil)

func TestDefaultSpec(t *testing.T) {
	config := `
foo = fooval
bar = barval
`
	f, diags := hclsyntax.ParseConfig([]byte(config), "", hcl.Pos{Line: 1, Column: 1})
	if diags.HasErrors() {
		t.Fatal(diags.Error())
	}

	t.Run("primary set", func(t *testing.T) {
		spec := &DefaultSpec{
			Primary: &AttrSpec{
				Name: "foo",
				Type: cty.String,
			},
			Default: &AttrSpec{
				Name: "bar",
				Type: cty.String,
			},
		}

		gotVars := Variables(f.Body, spec)
		wantVars := []hcl.Traversal{
			{
				hcl.TraverseRoot{
					Name: "fooval",
					SrcRange: hcl.Range{
						Filename: "",
						Start:    hcl.Pos{Line: 2, Column: 7, Byte: 7},
						End:      hcl.Pos{Line: 2, Column: 13, Byte: 13},
					},
				},
			},
			{
				hcl.TraverseRoot{
					Name: "barval",
					SrcRange: hcl.Range{
						Filename: "",
						Start:    hcl.Pos{Line: 3, Column: 7, Byte: 20},
						End:      hcl.Pos{Line: 3, Column: 13, Byte: 26},
					},
				},
			},
		}
		if !reflect.DeepEqual(gotVars, wantVars) {
			t.Errorf("wrong Variables result\ngot: %s\nwant: %s", dump.Value(gotVars), dump.Value(wantVars))
		}

		ctx := &hcl.EvalContext{
			Variables: map[string]cty.Value{
				"fooval": cty.StringVal("foo value"),
				"barval": cty.StringVal("bar value"),
			},
		}

		got, err := Decode(f.Body, spec, ctx)
		if err != nil {
			t.Fatal(err)
		}
		want := cty.StringVal("foo value")
		if !got.RawEquals(want) {
			t.Errorf("wrong Decode result\ngot:  %#v\nwant: %#v", got, want)
		}
	})

	t.Run("primary not set", func(t *testing.T) {
		spec := &DefaultSpec{
			Primary: &AttrSpec{
				Name: "foo",
				Type: cty.String,
			},
			Default: &AttrSpec{
				Name: "bar",
				Type: cty.String,
			},
		}

		ctx := &hcl.EvalContext{
			Variables: map[string]cty.Value{
				"fooval": cty.NullVal(cty.String),
				"barval": cty.StringVal("bar value"),
			},
		}

		got, err := Decode(f.Body, spec, ctx)
		if err != nil {
			t.Fatal(err)
		}
		want := cty.StringVal("bar value")
		if !got.RawEquals(want) {
			t.Errorf("wrong Decode result\ngot:  %#v\nwant: %#v", got, want)
		}
	})
}

func TestValidateFuncSpec(t *testing.T) {
	config := `
foo = "invalid"
`
	f, diags := hclsyntax.ParseConfig([]byte(config), "", hcl.Pos{Line: 1, Column: 1})
	if diags.HasErrors() {
		t.Fatal(diags.Error())
	}

	expectRange := map[string]*hcl.Range{
		"without_range": nil,
		"with_range": &hcl.Range{
			Filename: "foobar",
			Start:    hcl.Pos{Line: 99, Column: 99},
			End:      hcl.Pos{Line: 999, Column: 999},
		},
	}

	for name := range expectRange {
		t.Run(name, func(t *testing.T) {
			spec := &ValidateSpec{
				Wrapped: &AttrSpec{
					Name: "foo",
					Type: cty.String,
				},
				Func: func(value cty.Value) hcl.Diagnostics {
					if value.AsString() != "invalid" {
						return hcl.Diagnostics{
							&hcl.Diagnostic{
								Severity: hcl.DiagError,
								Summary:  "incorrect value",
								Detail:   fmt.Sprintf("invalid value passed in: %s", value.GoString()),
							},
						}
					}

					return hcl.Diagnostics{
						&hcl.Diagnostic{
							Severity: hcl.DiagWarning,
							Summary:  "OK",
							Detail:   "validation called correctly",
							Subject:  expectRange[name],
						},
					}
				},
			}

			_, diags = Decode(f.Body, spec, nil)
			if len(diags) != 1 ||
				diags[0].Severity != hcl.DiagWarning ||
				diags[0].Summary != "OK" ||
				diags[0].Detail != "validation called correctly" {
				t.Fatalf("unexpected diagnostics: %s", diags.Error())
			}

			if expectRange[name] == nil && diags[0].Subject == nil {
				t.Fatal("returned diagnostic subject missing")
			}

			if expectRange[name] != nil && !reflect.DeepEqual(expectRange[name], diags[0].Subject) {
				t.Fatalf("expected range %s, got range %s", expectRange[name], diags[0].Subject)
			}
		})
	}
}
