// Copyright 2019 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 convert_test

// TODO: generate tests from Go's json encoder.

import (
	"encoding"
	"math/big"
	"testing"
	"time"

	"github.com/cockroachdb/apd/v3"
	"github.com/google/go-cmp/cmp"

	"cuelang.org/go/cue/errors"
	"cuelang.org/go/internal/core/adt"
	"cuelang.org/go/internal/core/convert"
	"cuelang.org/go/internal/core/debug"
	"cuelang.org/go/internal/core/runtime"

	_ "cuelang.org/go/pkg"
)

func mkBigInt(a int64) (v apd.Decimal) { v.SetInt64(a); return }

type textMarshaller struct {
	b string
}

func (t *textMarshaller) MarshalText() (b []byte, err error) {
	return []byte(t.b), nil
}

var _ encoding.TextMarshaler = &textMarshaller{}

func TestConvert(t *testing.T) {
	type key struct {
		a int
	}
	type stringType string
	i34 := big.NewInt(34)
	d35 := mkBigInt(35)
	n36 := mkBigInt(-36)
	f37 := big.NewFloat(37.0000)
	testCases := []struct {
		goVal interface{}
		want  string
	}{{
		nil, "(_){ _ }",
	}, {
		true, "(bool){ true }",
	}, {
		false, "(bool){ false }",
	}, {
		errors.New("oh noes"), "(_|_){\n  // [eval] oh noes\n}",
	}, {
		"foo", `(string){ "foo" }`,
	}, {
		"\x80", `(string){ "�" }`,
	}, {
		3, "(int){ 3 }",
	}, {
		uint(3), "(int){ 3 }",
	}, {
		uint8(3), "(int){ 3 }",
	}, {
		uint16(3), "(int){ 3 }",
	}, {
		uint32(3), "(int){ 3 }",
	}, {
		uint64(3), "(int){ 3 }",
	}, {
		int8(-3), "(int){ -3 }",
	}, {
		int16(-3), "(int){ -3 }",
	}, {
		int32(-3), "(int){ -3 }",
	}, {
		int64(-3), "(int){ -3 }",
	}, {
		float64(3), "(float){ 3 }",
	}, {
		float64(3.1), "(float){ 3.1 }",
	}, {
		float32(3.1), "(float){ 3.1 }",
	}, {
		uintptr(3), "(int){ 3 }",
	}, {
		&i34, "(int){ 34 }",
	}, {
		&f37, "(float){ 37 }",
	}, {
		&d35, "(int){ 35 }",
	}, {
		&n36, "(int){ -36 }",
	}, {
		[]int{1, 2, 3, 4}, `(#list){
  0: (int){ 1 }
  1: (int){ 2 }
  2: (int){ 3 }
  3: (int){ 4 }
}`,
	}, {
		struct {
			A int
			B *int
		}{3, nil},
		"(struct){\n  A: (int){ 3 }\n}",
	}, {
		[]interface{}{}, "(#list){\n}",
	}, {
		[]interface{}{nil}, "(#list){\n  0: (_){ _ }\n}",
	}, {
		map[string]interface{}{"a": 1, "x": nil}, `(struct){
  a: (int){ 1 }
  x: (_){ _ }
}`,
	}, {
		map[string][]int{
			"a": {1},
			"b": {3, 4},
		}, `(struct){
  a: (#list){
    0: (int){ 1 }
  }
  b: (#list){
    0: (int){ 3 }
    1: (int){ 4 }
  }
}`,
	}, {
		map[bool]int{}, "(_|_){\n  // [eval] unsupported Go type for map key (bool)\n}",
	}, {
		map[struct{}]int{{}: 2}, "(_|_){\n  // [eval] unsupported Go type for map key (struct {})\n}",
	}, {
		map[int]int{1: 2}, `(struct){
  "1": (int){ 2 }
}`,
	}, {
		struct {
			a int
			b int
		}{3, 4},
		"(struct){\n}",
	}, {
		struct {
			A int
			B int `json:"-"`
			C int `json:",omitempty"`
		}{3, 4, 0},
		`(struct){
  A: (int){ 3 }
}`,
	}, {
		struct {
			A int
			B int
		}{3, 4},
		`(struct){
  A: (int){ 3 }
  B: (int){ 4 }
}`,
	}, {
		struct {
			A int `json:"a"`
			B int `yaml:"b"`
		}{3, 4},
		`(struct){
  a: (int){ 3 }
  b: (int){ 4 }
}`,
	}, {
		struct {
			A int `json:"" yaml:"" protobuf:"aa"`
			B int `yaml:"cc" json:"bb" protobuf:"aa"`
		}{3, 4},
		`(struct){
  aa: (int){ 3 }
  bb: (int){ 4 }
}`,
	}, {
		&struct{ A int }{3}, `(struct){
  A: (int){ 3 }
}`,
	}, {
		(*struct{ A int })(nil), "(_){ _ }",
	}, {
		time.Date(2019, 4, 1, 0, 0, 0, 0, time.UTC), `(string){ "2019-04-01T00:00:00Z" }`,
	}, {
		func() interface{} {
			type T struct {
				B int
			}
			type S struct {
				A string
				T
			}
			return S{}
		}(),
		`(struct){
  A: (string){ "" }
  B: (int){ 0 }
}`,
	},
		{map[key]string{{a: 1}: "foo"},
			"(_|_){\n  // [eval] unsupported Go type for map key (convert_test.key)\n}"},
		{map[*textMarshaller]string{{b: "bar"}: "foo"},
			"(struct){\n  \"&{bar}\": (string){ \"foo\" }\n}"},
		{map[int]string{1: "foo"},
			"(struct){\n  \"1\": (string){ \"foo\" }\n}"},
		{map[string]encoding.TextMarshaler{"foo": nil},
			"(struct){\n  foo: (_){ _ }\n}"},
		{map[string]encoding.TextMarshaler{"foo": &textMarshaller{b: "bar"}},
			"(struct){\n  foo: (string){ \"bar\" }\n}"},
		{make(chan int),
			"(_|_){\n  // [eval] unsupported Go type (chan int)\n}"},
		{[]interface{}{func() {}},
			"(_|_){\n  // [eval] unsupported Go type (func())\n}"},
		{[]encoding.TextMarshaler{nil},
			"(#list){\n  0: (_){ _ }\n}"},
		{[]encoding.TextMarshaler{&textMarshaller{b: "bar"}},
			"(#list){\n  0: (string){ \"bar\" }\n}"},
		{stringType("\x80"), `(string){ "�" }`},
	}
	r := runtime.New()
	for _, tc := range testCases {
		ctx := adt.NewContext(r, &adt.Vertex{})
		t.Run("", func(t *testing.T) {
			v := convert.FromGoValue(ctx, tc.goVal, true)
			n, ok := v.(*adt.Vertex)
			if !ok {
				n = &adt.Vertex{BaseValue: v}
			}
			got := debug.NodeString(ctx, n, nil)
			if got != tc.want {
				t.Error(cmp.Diff(got, tc.want))
			}
		})
	}
}

func TestX(t *testing.T) {
	t.Skip()

	x := []string{}

	r := runtime.New()
	ctx := adt.NewContext(r, &adt.Vertex{})

	v := convert.FromGoValue(ctx, x, false)
	// if err != nil {
	// 	t.Fatal(err)
	// }
	got := debug.NodeString(ctx, v, nil)
	t.Error(got)
}

func TestConvertType(t *testing.T) {
	testCases := []struct {
		goTyp       interface{}
		want        string
		expectError bool
	}{{
		goTyp: struct {
			A int      `cue:">=0&<100"`
			B *big.Int `cue:">=0"`
			C *big.Int
			D big.Int
			F *big.Float
		}{},
		// TODO: indicate that B is explicitly an int only.
		want: `{
  A: (((int & >=-9223372036854775808) & <=9223372036854775807) & (>=0 & <100))
  B: (int & >=0)
  C?: int
  D: int
  F?: number
}`,
	}, {
		goTyp: &struct {
			A int16 `cue:">=0&<100"`
			B error `json:"b"`
			C string
			D bool
			F float64
			L []byte
			T time.Time
			G func()
		}{},
		want: `(*null|{
  A: (((int & >=-32768) & <=32767) & (>=0 & <100))
  b: null
  C: string
  D: bool
  F: number
  L?: (*null|bytes)
  T: _
})`,
	}, {
		goTyp: struct {
			A int `cue:"<"` // invalid
		}{},
		want:        "_|_(invalid tag \"<\" for field \"A\": expected operand, found 'EOF' (and 1 more errors))",
		expectError: true,
	}, {
		goTyp: struct {
			A int `json:"-"` // skip
			D *apd.Decimal
			P ***apd.Decimal
			I interface{ Foo() }
			T string `cue:""` // allowed
			h int
		}{},
		want: `{
  D?: number
  P?: (*null|number)
  I?: _
  T: (string & _)
}`,
	}, {
		goTyp: struct {
			A int8 `cue:"C-B"`
			B int8 `cue:"C-A,opt"`
			C int8 `cue:"A+B"`
		}{},
		// TODO: should B be marked as optional?
		want: `{
  A: (((int & >=-128) & <=127) & (〈0;C〉 - 〈0;B〉))
  B?: (((int & >=-128) & <=127) & (〈0;C〉 - 〈0;A〉))
  C: (((int & >=-128) & <=127) & (〈0;A〉 + 〈0;B〉))
}`,
	}, {
		goTyp: []string{},
		want: `(*null|[
  ...string,
])`,
	}, {
		goTyp: [4]string{},
		want: `〈import;list〉.Repeat([
  string,
], 4)`,
	}, {
		goTyp:       []func(){},
		want:        "_|_(unsupported Go type (func()))",
		expectError: true,
	}, {
		goTyp: map[string]struct{ A map[string]uint }{},
		want: `(*null|{
  [string]: {
    A?: (*null|{
      [string]: ((int & >=0) & <=18446744073709551615)
    })
  }
})`,
	}, {
		goTyp:       map[float32]int{},
		want:        `_|_(unsupported Go type for map key (float32))`,
		expectError: true,
	}, {
		goTyp:       map[int]map[float32]int{},
		want:        `_|_(unsupported Go type for map key (float32))`,
		expectError: true,
	}, {
		goTyp:       map[int]func(){},
		want:        `_|_(unsupported Go type (func()))`,
		expectError: true,
	}, {
		goTyp:       time.Now, // a function
		want:        "_|_(unsupported Go type (func() time.Time))",
		expectError: true,
	}, {
		goTyp: struct {
			Foobar string `cue:"\"foo,bar\",opt"`
		}{},
		want: `{
  Foobar?: (string & "foo,bar")
}`,
	}, {
		goTyp: struct {
			Foobar string `cue:"\"foo,opt,bar\""`
		}{},
		want: `{
  Foobar: (string & "foo,opt,bar")
}`,
	}}

	r := runtime.New()

	for _, tc := range testCases {
		t.Run("", func(t *testing.T) {
			ctx := adt.NewContext(r, &adt.Vertex{})
			v, err := convert.FromGoType(ctx, tc.goTyp)
			got := debug.NodeString(ctx, v, nil)
			if got != tc.want {
				t.Errorf("\n got %q;\nwant %q", got, tc.want)
			}
			if tc.expectError && err == nil {
				t.Errorf("\n expected an error but didn't get one")
			} else if !tc.expectError && err != nil {
				t.Errorf("\n got unexpected error: %v", err)
			}
			if err == nil && !tc.expectError {
				val, _ := ctx.Evaluate(&adt.Environment{}, v)
				if bot, ok := val.(*adt.Bottom); ok {
					t.Errorf("\n unexpected error when evaluating result of conversion: %v", bot)
				}
			}
		})
	}
}
