// 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 cuego

import (
	"reflect"
	"testing"
)

type Sum struct {
	A int `cue:"C-B" json:",omitempty"`
	B int `cue:"C-A" json:",omitempty"`
	C int `cue:"A+B & >=5" json:",omitempty"`
}

func checkErr(t *testing.T, got error, want string) {
	t.Helper()
	if (got == nil) != (want == "") {
		t.Errorf("error: got %v; want %v", got, want)
	}
}
func TestValidate(t *testing.T) {
	fail := "some error"
	testCases := []struct {
		name        string
		value       interface{}
		constraints string
		err         string
	}{{
		name:  "Sum",
		value: Sum{A: 1, B: 4, C: 5},
	}, {
		name:  "*Sum",
		value: &Sum{A: 1, B: 4, C: 5},
	}, {
		name:  "*Sum: incorrect sum",
		value: &Sum{A: 1, B: 4, C: 6},
		err:   fail,
	}, {
		name:  "*Sum: field C is too low",
		value: &Sum{A: 1, B: 3, C: 4},
		err:   fail,
	}, {
		name:  "*Sum: nil value",
		value: (*Sum)(nil),
		// }, {
		// // TODO: figure out whether this constraint should constrain it
		// // to a struct or not.
		// name:        "*Sum: nil disallowed by constraint",
		// value:       (*Sum)(nil),
		// constraints: "!=null",
		// err:         fail,
	}, {
		// Not a typical constraint, but it is possible.
		name:        "string list",
		value:       []string{"a", "b", "c"},
		constraints: `[_, "b", ...]`,
	}, {
		// Not a typical constraint, but it is possible.
		name:        "string list incompatible lengths",
		value:       []string{"a", "b", "c"},
		constraints: `4*[string]`,
		err:         fail,
	}}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			c := &Context{}
			if tc.constraints != "" {
				err := c.Constrain(tc.value, tc.constraints)
				if err != nil {
					t.Fatal(err)
				}
			}
			err := c.Validate(tc.value)
			checkErr(t, err, tc.err)
		})
	}
}

func TestUpdate(t *testing.T) {
	type updated struct {
		A []*int `cue:"[...int|*1]"` // arbitrary length slice with values 1
		B []int  `cue:"3*[int|*1]"`  // slice of length 3, defaults to [1,1,1]

		// TODO: better errors if the user forgets to quote.
		M map[string]int `cue:",opt"`
	}
	type sump struct {
		A *int `cue:"C-B"`
		B *int `cue:"C-A"`
		C *int `cue:"A+B"`
	}
	one := 1
	two := 2
	fail := "some error"
	_ = fail
	_ = one
	testCases := []struct {
		name        string
		value       interface{}
		result      interface{}
		constraints string
		err         string
	}{{
		name:   "*Sum",
		value:  &Sum{A: 1, B: 4, C: 5},
		result: &Sum{A: 1, B: 4, C: 5},
	}, {
		name:   "*Sum",
		value:  &Sum{A: 1, B: 4},
		result: &Sum{A: 1, B: 4, C: 5},
	}, {
		name:   "*sump",
		value:  &sump{A: &one, B: &one},
		result: &sump{A: &one, B: &one, C: &two},
	}, {
		name:   "*Sum: backwards",
		value:  &Sum{B: 4, C: 8},
		result: &Sum{A: 4, B: 4, C: 8},
	}, {
		name:   "*Sum: sum too low",
		value:  &Sum{A: 1, B: 3},
		result: &Sum{A: 1, B: 3}, // Value should not be updated
		err:    fail,
	}, {
		name:   "*Sum: sum underspecified",
		value:  &Sum{A: 1},
		result: &Sum{A: 1}, // Value should not be updated
		err:    fail,
	}, {
		name:   "Sum: cannot modify",
		value:  Sum{A: 3, B: 4, C: 7},
		result: Sum{A: 3, B: 4, C: 7},
		err:    fail,
	}, {
		name:   "*Sum: cannot update nil value",
		value:  (*Sum)(nil),
		result: (*Sum)(nil),
		err:    fail,
	}, {
		name:   "cannot modify slice",
		value:  []string{"a", "b", "c"},
		result: []string{"a", "b", "c"},
		err:    fail,
	}, {
		name: "composite values update",
		// allocate a slice with uninitialized values and let Update fill
		// out default values.
		value: &updated{A: make([]*int, 3)},
		result: &updated{
			A: []*int{&one, &one, &one},
			B: []int{1, 1, 1},
			M: map[string]int(nil),
		},
	}, {
		name:   "composite values update with unsatisfied map constraints",
		value:  &updated{},
		result: &updated{},

		constraints: ` { M: {foo: bar, bar: foo} } `,
		err:         fail, // incomplete values
	}, {
		name:        "composite values update with map constraints",
		value:       &updated{M: map[string]int{"foo": 1}},
		constraints: ` { M: {foo: bar, bar: foo} } `,
		result: &updated{
			// TODO: would be better if this is nil, but will not matter for
			// JSON output: if omitempty is false, an empty list will be
			// printed regardless, and if it is true, it will be omitted
			// regardless.
			A: []*int{},
			B: []int{1, 1, 1},
			M: map[string]int{"bar": 1, "foo": 1},
		},
	}}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			c := &Context{}
			if tc.constraints != "" {
				err := c.Constrain(tc.value, tc.constraints)
				if err != nil {
					t.Fatal(err)
				}
			}
			err := c.Complete(tc.value)
			checkErr(t, err, tc.err)
			if !reflect.DeepEqual(tc.value, tc.result) {
				t.Errorf("value:\n got: %#v;\nwant: %#v", tc.value, tc.result)
			}
		})
	}
}
