// Copyright 2020 CUE Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package export_test

import (
	"testing"

	"golang.org/x/tools/txtar"

	"cuelang.org/go/cue"
	"cuelang.org/go/cue/ast"
	"cuelang.org/go/cue/cuecontext"
	"cuelang.org/go/cue/errors"
	"cuelang.org/go/cue/format"
	"cuelang.org/go/cue/parser"
	"cuelang.org/go/encoding/gocode/gocodec"
	"cuelang.org/go/internal/astinternal"
	"cuelang.org/go/internal/core/adt"
	"cuelang.org/go/internal/core/compile"
	"cuelang.org/go/internal/core/convert"
	"cuelang.org/go/internal/core/eval"
	"cuelang.org/go/internal/core/export"
	"cuelang.org/go/internal/core/runtime"
	"cuelang.org/go/internal/cuetdtest"
	"cuelang.org/go/internal/cuetxtar"
	"cuelang.org/go/internal/value"
)

func TestDefinition(t *testing.T) {
	t.Parallel()
	test := cuetxtar.TxTarTest{
		Root:   "./testdata/main",
		Name:   "definition",
		Matrix: cuetdtest.FullMatrix,
	}

	test.Run(t, func(t *cuetxtar.Test) {
		r := t.Runtime()
		a := t.Instance()

		v, errs := compile.Files(nil, r, "", a.Files...)
		if errs != nil {
			t.Fatal(errs)
		}
		v.Finalize(eval.NewContext(r, v))

		file, errs := export.Def(r, "", v)
		errors.Print(t, errs, nil)
		_, _ = t.Write(formatNode(t.T, file))
	})
}

func formatNode(t *testing.T, n ast.Node) []byte {
	t.Helper()

	b, err := format.Node(n)
	if err != nil {
		t.Fatal(err)
	}
	return b
}

// TestGenerated tests conversions of generated Go structs, which may be
// different from parsed or evaluated CUE, such as having Vertex values.
func TestGenerated(t *testing.T) {
	cuetdtest.FullMatrix.Do(t, func(t *testing.T, m *cuetdtest.M) {
		ctx := m.CueContext()

		testCases := []struct {
			in  func(ctx *adt.OpContext) (adt.Expr, error)
			out string
			p   *export.Profile
		}{{
			in: func(ctx *adt.OpContext) (adt.Expr, error) {
				in := &C{
					Terminals: []*A{{Name: "Name", Description: "Desc"}},
				}
				return convert.GoValueToValue(ctx, in, false), nil
			},
			out: `Terminals: [{Name: "Name", Description: "Desc"}]`,
		}, {
			in: func(ctx *adt.OpContext) (adt.Expr, error) {
				in := &C{
					Terminals: []*A{{Name: "Name", Description: "Desc"}},
				}
				return convert.GoTypeToExpr(ctx, in)
			},
			out: `*null|{Terminals?: *null|[...*null|{Name: string, Description: string}]}`,
		}, {
			in: func(ctx *adt.OpContext) (adt.Expr, error) {
				in := []*A{{Name: "Name", Description: "Desc"}}
				return convert.GoValueToValue(ctx, in, false), nil
			},
			out: `[{Name: "Name", Description: "Desc"}]`,
		}, {
			in: func(ctx *adt.OpContext) (adt.Expr, error) {
				in := []*A{{Name: "Name", Description: "Desc"}}
				return convert.GoTypeToExpr(ctx, in)
			},
			out: `*null|[...*null|{Name: string, Description: string}]`,
		}, {
			in: func(ctx *adt.OpContext) (adt.Expr, error) {
				expr, err := parser.ParseExpr("test", `{
				x: Guide.#Terminal
				Guide: {}
			}`)
				if err != nil {
					return nil, err
				}
				c, err := compile.Expr(nil, ctx, "_", expr)
				if err != nil {
					return nil, err
				}
				root := &adt.Vertex{}
				root.AddConjunct(c)
				root.Finalize(ctx)

				// Simulate Value.Unify of Lookup("x") and Lookup("Guide").
				n := &adt.Vertex{}
				n.AddConjunct(adt.MakeRootConjunct(nil, root.Arcs[0]))
				n.AddConjunct(adt.MakeRootConjunct(nil, root.Arcs[1]))
				n.Finalize(ctx)

				return n, nil
			},
			out: `<[l2// x: undefined field: #Terminal] _|_>`,
			p:   export.Final,
		}, {
			in: func(r *adt.OpContext) (adt.Expr, error) {
				v := ctx.CompileString(`
				#Provider: {
					ID: string
					notConcrete: bool
					a: int
					b: a + 1
				}`)

				spec := v.LookupPath(cue.ParsePath("#Provider"))
				spec2 := spec.FillPath(cue.ParsePath("ID"), "12345")
				root := v.FillPath(cue.ParsePath("providers.foo"), spec2)
				_, n := value.ToInternal(root)

				return n, nil
			},
			out: `#Provider: {ID: string, notConcrete: bool, a: int, b: a+1}, providers: {foo: {ID: "12345", notConcrete: bool, a: int, b: a+1}}`,
			p:   export.All,
		}, {
			// Issue #882
			in: func(r *adt.OpContext) (adt.Expr, error) {
				valA := ctx.CompileString(`
				#One: { version: string }
			`)

				valB := ctx.CompileString(`
				#One: _
				ones: {[string]: #One}
			`)
				v := valB.Unify(valA)
				_, n := value.ToInternal(v)
				return n, nil
			},
			out: `#One: {version: string}, ones: {[string]: #One}`,
			p:   export.All,
		}, {
			// Indicate closedness in an element that is closed and misses parent
			// context.
			// Issue #882
			in: func(r *adt.OpContext) (adt.Expr, error) {
				v := ctx.CompileString(`
					#A: b: c: string
				`)
				v = v.LookupPath(cue.ParsePath("#A.b"))

				_, n := value.ToInternal(v)
				return n, nil
			},
			out: `_#def, _#def: {c: string}`,
			p:   export.All,
		}, {
			// Don't wrap in def if the if the value is an embedded scalar.
			// Issue #977
			in: func(r *adt.OpContext) (adt.Expr, error) {
				v := ctx.CompileString(`
					#A: { "foo", #enum: 2 }
				`)
				v = v.LookupPath(cue.ParsePath("#A"))

				_, n := value.ToInternal(v)
				return n, nil
			},
			out: `"foo", #enum: 2`,
			p:   export.All,
		}, {
			// Issue #1131
			in: func(r *adt.OpContext) (adt.Expr, error) {
				m := make(map[string]interface{})
				v := ctx.Encode(m)
				_, x := value.ToInternal(v)
				return x, nil
			},
			out: ``, // empty file
		}, {
			in: func(r *adt.OpContext) (adt.Expr, error) {
				v := &adt.Vertex{}
				v.SetValue(r, &adt.StructMarker{})
				return v, nil
			},
			out: ``, // empty file
		}}
		for _, tc := range testCases {
			t.Run("", func(t *testing.T) {
				ctx := adt.NewContext((*runtime.Runtime)(ctx), &adt.Vertex{})
				v, err := tc.in(ctx)
				if err != nil {
					t.Fatal("failed test case: ", err)
				}

				p := tc.p
				if p == nil {
					p = export.Simplified
				}

				var n ast.Node
				switch x := v.(type) {
				case *adt.Vertex:
					n, err = p.Def(ctx, "", x)
				default:
					n, err = p.Expr(ctx, "", v)
				}
				if err != nil {
					t.Fatal("failed export: ", err)
				}
				got := astinternal.DebugStr(n)
				if got != tc.out {
					t.Errorf("got:  %s\nwant: %s", got, tc.out)
				}
			})
		}
	})
}

type A struct {
	Name        string
	Description string
}

type B struct {
	Image string
}

type C struct {
	Terminals []*A
}

// For debugging purposes. Do not delete.
func TestX(t *testing.T) {
	t.Skip()

	in := `
-- in.cue --
	`

	archive := txtar.Parse([]byte(in))
	a := cuetxtar.Load(archive, t.TempDir())
	if err := a[0].Err; err != nil {
		t.Fatal(err)
	}

	// x := a[0].Files[0]
	// astutil.Sanitize(x)

	ctx := cuecontext.New()
	r := (*runtime.Runtime)(ctx)
	v, errs := compile.Files(nil, r, "", a[0].Files...)
	if errs != nil {
		t.Fatal(errs)
	}
	v.Finalize(eval.NewContext(r, v))

	file, errs := export.Def(r, "main", v)
	if errs != nil {
		t.Fatal(errs)
	}

	t.Error(string(formatNode(t, file)))
}

func TestFromGo(t *testing.T) {
	type Struct struct {
		A string
		B string
	}

	m := make(map[string]Struct)
	m["hello"] = Struct{
		A: "a",
		B: "b",
	}
	ctx := cuecontext.New()
	codec := gocodec.New(ctx, nil)
	v, err := codec.Decode(m)
	if err != nil {
		panic(err)
	}

	syn, _ := format.Node(v.Syntax())
	if got := string(syn); got != `{
	hello: {
		A: "a"
		B: "b"
	}
}` {
		t.Errorf("incorrect ordering: %s\n", got)
	}
}

func TestFromAPI(t *testing.T) {
	testCases := []struct {
		expr ast.Expr
		out  string
	}{{
		expr: ast.NewCall(ast.NewIdent("close"), ast.NewStruct()),
		out:  `close({})`,
	}, {
		expr: ast.NewCall(ast.NewIdent("close"), ast.NewStruct(
			"a", ast.NewString("foo"),
		)),
		out: `close({a: "foo"})`,
	}, {
		expr: ast.NewCall(ast.NewIdent("close"), ast.NewStruct(
			ast.Embed(ast.NewStruct("a", ast.NewString("foo"))),
		)),
		out: `close({a: "foo"})`,
	}}
	// Issue #1204
	for _, tc := range testCases {
		cuetdtest.FullMatrix.Run(t, "", func(t *testing.T, m *cuetdtest.M) {
			ctx := m.CueContext()

			v := ctx.BuildExpr(tc.expr)

			r, x := value.ToInternal(v)
			file, err := export.Def(r, "foo", x)

			if err != nil {
				t.Fatal(err)
			}

			got := astinternal.DebugStr(file)
			if got != tc.out {
				t.Errorf("got:  %s\nwant: %s", got, tc.out)
			}

		})
	}
}
