// Copyright 2018 The 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 cue_test

import (
	"fmt"
	"testing"

	"cuelang.org/go/cue"
	"cuelang.org/go/cue/ast"
	"cuelang.org/go/cue/build"
	"cuelang.org/go/cue/cuecontext"
	"cuelang.org/go/cue/token"
	"cuelang.org/go/internal/core/debug"
	"cuelang.org/go/internal/value"
)

func TestFromExpr(t *testing.T) {
	testCases := []struct {
		expr ast.Expr
		out  string
	}{{
		expr: ast.NewString("Hello"),
		out:  `"Hello"`,
	}, {
		expr: ast.NewList(
			ast.NewString("Hello"),
			ast.NewString("World"),
		),
		out: `["Hello", "World"]`,
	}, {
		// Issue #2628; this would cause a panic in the evaluator.
		expr: ast.NewBinExpr(
			token.AND,
			ast.NewIdent("bool"),
			ast.NewBool(true),
		),
		out: `true`,
	}}
	for _, tc := range testCases {
		t.Run("", func(t *testing.T) {
			r := cuecontext.New()
			v := r.BuildExpr(tc.expr)
			if err := v.Err(); err != nil {
				t.Fatal(err)
			}
			if got := fmt.Sprint(v); got != tc.out {
				t.Errorf("\n got: %v; want %v", got, tc.out)
			}
		})
	}
}

func TestBuild(t *testing.T) {
	files := func(s ...string) []string { return s }
	insts := func(i ...*bimport) []*bimport { return i }
	pkg1 := &bimport{
		"pkg1",
		files(`
		package pkg1

		Object: "World"
		`),
	}
	pkg2 := &bimport{
		"mod.test/foo/pkg2:pkg",
		files(`
		package pkg

		Number: 12
		`),
	}
	pkg3 := &bimport{
		"mod.test/foo/v1:pkg3",
		files(`
		package pkg3

		List: [1,2,3]
		`),
	}

	testCases := []struct {
		instances []*bimport
		emit      string
	}{{
		insts(&bimport{"", files(`test: "ok"`)}),
		`{test:"ok"}`,
	}, {
		insts(&bimport{"",
			files(
				`package test

				import "math"

				"Pi: \(math.Pi)!"`)}),
		`"Pi: 3.14159265358979323846264338327950288419716939937510582097494459!"`,
	}, {
		insts(&bimport{"",
			files(
				`package test

				import math2 "math"

				"Pi: \(math2.Pi)!"`)}),
		`"Pi: 3.14159265358979323846264338327950288419716939937510582097494459!"`,
	}, {
		insts(pkg1, &bimport{"",
			files(
				`package test

				import "pkg1"

				"Hello \(pkg1.Object)!"`),
		}),
		`"Hello World!"`,
	}, {
		insts(pkg1, &bimport{"",
			files(
				`package test

				import "pkg1"

				"Hello \(pkg1.Object)!"`),
		}),
		`"Hello World!"`,
	}, {
		insts(pkg1, &bimport{"",
			files(
				`package test

				import pkg2 "pkg1"
				#pkg1: pkg2.Object

				"Hello \(#pkg1)!"`),
		}),
		`"Hello World!"`,
	}, {
		insts(pkg1, pkg2, &bimport{"",
			files(
				`package test

				import bar "pkg1"
				import baz "mod.test/foo/pkg2:pkg"

				pkg1: Object: 3
				"Hello \(pkg1.Object)!"`),
		}),
		`imported and not used: "pkg1" as bar (and 1 more errors)`,
	}, {
		insts(pkg2, &bimport{"",
			files(
				`package test

				import "mod.test/foo/pkg2:pkg"

				"Hello \(pkg2.Number)!"`),
		}),
		`imported and not used: "mod.test/foo/pkg2:pkg" (and 1 more errors)`,
		// `file0.cue:5:14: unresolved reference pkg2`,
	}, {
		insts(pkg2, &bimport{"",
			files(
				`package test

				import "mod.test/foo/pkg2:pkg"

				"Hello \(pkg.Number)!"`),
		}),
		`"Hello 12!"`,
	}, {
		insts(pkg3, &bimport{"",
			files(
				`package test

				import "mod.test/foo/v1:pkg3"

				"Hello \(pkg3.List[1])!"`),
		}),
		`"Hello 2!"`,
	}, {
		insts(pkg3, &bimport{"",
			files(
				`package test

				import "mod.test/foo/v1:pkg3"

				pkg3: 3

				"Hello \(pkg3.List[1])!"`),
		}),
		`pkg3 redeclared as imported package name
	previous declaration at file0.cue:5:5`,
	}}
	for _, tc := range testCases {
		t.Run("", func(t *testing.T) {
			insts := cue.Build(makeInstances(tc.instances))
			var got string
			if err := insts[0].Err; err != nil {
				got = err.Error()
			} else {
				cfg := &debug.Config{Compact: true}
				r, v := value.ToInternal(insts[0].Value())
				got = debug.NodeString(r, v, cfg)
			}
			if got != tc.emit {
				t.Errorf("\n got: %s\nwant: %s", got, tc.emit)
			}
		})
	}
}

type builder struct {
	ctxt    *build.Context
	imports map[string]*bimport
}

func (b *builder) load(pos token.Pos, path string) *build.Instance {
	bi := b.imports[path]
	if bi == nil {
		return nil
	}
	return b.build(bi)
}

type bimport struct {
	path  string // "" means top-level
	files []string
}

func makeInstances(insts []*bimport) (instances []*build.Instance) {
	b := builder{
		imports: map[string]*bimport{},
	}
	b.ctxt = build.NewContext(build.Loader(b.load))

	for _, bi := range insts {
		if bi.path != "" {
			b.imports[bi.path] = bi
		}
	}
	for _, bi := range insts {
		if bi.path == "" {
			instances = append(instances, b.build(bi))
		}
	}
	return
}

func (b *builder) build(bi *bimport) *build.Instance {
	path := bi.path
	if path == "" {
		path = "dir"
	}
	p := b.ctxt.NewInstance(path, nil)
	for i, f := range bi.files {
		_ = p.AddFile(fmt.Sprintf("file%d.cue", i), f)
	}
	_ = p.Complete()
	return p
}
