package vals

import (
	"math/big"
	"reflect"
	"testing"

	"src.elv.sh/pkg/eval/errs"
	"src.elv.sh/pkg/tt"
)

type someType struct {
	Foo string
}

func TestScanToGo_ConcreteTypeDst(t *testing.T) {
	// A wrapper around ScanToGo, to make it easier to test. Instead of
	// supplying a pointer to the destination, an initial value to the
	// destination is supplied and the result is returned.
	scanToGo := func(src any, dstInit any) (any, error) {
		ptr := reflect.New(TypeOf(dstInit))
		err := ScanToGo(src, ptr.Interface())
		return ptr.Elem().Interface(), err
	}

	tt.Test(t, tt.Fn(scanToGo).Named("scanToGo"),
		// int
		Args("12", 0).Rets(12),
		Args("0x12", 0).Rets(0x12),
		Args(12.0, 0).Rets(0, errMustBeInteger),
		Args(0.5, 0).Rets(0, errMustBeInteger),
		Args(someType{}, 0).Rets(tt.Any, errMustBeInteger),
		Args("x", 0).Rets(tt.Any, cannotParseAs{"integer", "x"}),

		// float64
		Args(23, 0.0).Rets(23.0),
		Args(big.NewRat(1, 2), 0.0).Rets(0.5),
		Args(1.2, 0.0).Rets(1.2),
		Args("23", 0.0).Rets(23.0),
		Args("0x23", 0.0).Rets(float64(0x23)),
		Args(someType{}, 0.0).Rets(tt.Any, errMustBeNumber),
		Args("x", 0.0).Rets(tt.Any, cannotParseAs{"number", "x"}),

		// rune
		Args("x", ' ').Rets('x'),
		Args(someType{}, ' ').Rets(tt.Any, errMustBeString),
		Args("\xc3\x28", ' ').Rets(tt.Any, errMustBeValidUTF8), // Invalid UTF8
		Args("ab", ' ').Rets(tt.Any, errMustHaveSingleRune),

		// Other types don't undergo any conversion, as long as the types match
		Args("foo", "").Rets("foo"),
		Args(someType{"foo"}, someType{}).Rets(someType{"foo"}),
		Args(nil, nil).Rets(nil),
		Args("x", someType{}).Rets(tt.Any, WrongType{"!!vals.someType", "string"}),
	)
}

func TestScanToGo_NumDst(t *testing.T) {
	scanToGo := func(src any) (Num, error) {
		var n Num
		err := ScanToGo(src, &n)
		return n, err
	}

	tt.Test(t, tt.Fn(scanToGo).Named("scanToGo"),
		// Strings are automatically converted
		Args("12").Rets(12),
		Args(z).Rets(bigInt(z)),
		Args("1/2").Rets(big.NewRat(1, 2)),
		Args("12.0").Rets(12.0),
		// Already numbers
		Args(12).Rets(12),
		Args(bigInt(z)).Rets(bigInt(z)),
		Args(big.NewRat(1, 2)).Rets(big.NewRat(1, 2)),
		Args(12.0).Rets(12.0),

		Args("bad").Rets(tt.Any, cannotParseAs{"number", "bad"}),
		Args(EmptyList).Rets(tt.Any, errMustBeNumber),
	)
}

func TestScanToGo_InterfaceDst(t *testing.T) {
	scanToGo := func(src any) (any, error) {
		var l List
		err := ScanToGo(src, &l)
		return l, err
	}

	tt.Test(t, tt.Fn(scanToGo).Named("scanToGo"),
		Args(EmptyList).Rets(EmptyList),

		Args("foo").Rets(tt.Any, WrongType{"!!vector.Vector", "string"}),
	)
}

func TestScanToGo_CallableDstAdmitsNil(t *testing.T) {
	type mockCallable interface {
		Call()
	}
	scanToGo := func(src any) (any, error) {
		var c mockCallable
		err := ScanToGo(src, &c)
		return c, err
	}

	tt.Test(t, tt.Fn(scanToGo).Named("scanToGo"),
		Args(nil).Rets(mockCallable(nil)),
	)
}

func TestScanToGo_ErrorsWithNonPointerDst(t *testing.T) {
	err := ScanToGo("", 1)
	if err == nil {
		t.Errorf("did not return error")
	}
}

func TestScanListToGo(t *testing.T) {
	// A wrapper around ScanListToGo, to make it easier to test.
	scanListToGo := func(src List, dstInit any) (any, error) {
		ptr := reflect.New(TypeOf(dstInit))
		ptr.Elem().Set(reflect.ValueOf(dstInit))
		err := ScanListToGo(src, ptr.Interface())
		return ptr.Elem().Interface(), err
	}

	tt.Test(t, tt.Fn(scanListToGo).Named("scanListToGo"),
		Args(MakeList("1", "2"), []int{}).Rets([]int{1, 2}),
		Args(MakeList("1", "2"), []string{}).Rets([]string{"1", "2"}),

		Args(MakeList("1", "a"), []int{}).Rets([]int{}, cannotParseAs{"integer", "a"}),
	)
}

func TestScanListElementsToGo(t *testing.T) {
	// A wrapper around ScanListElementsToGo, to make it easier to test.
	scanListElementsToGo := func(src List, inits ...any) ([]any, error) {
		ptrs := make([]any, len(inits))
		for i, init := range inits {
			if o, ok := init.(optional); ok {
				// Wrapping the init value with Optional translates to wrapping
				// the pointer with Optional.
				ptrs[i] = Optional(reflect.New(TypeOf(o.ptr)).Interface())
			} else {
				ptrs[i] = reflect.New(TypeOf(init)).Interface()
			}
		}
		err := ScanListElementsToGo(src, ptrs...)
		vals := make([]any, len(ptrs))
		for i, ptr := range ptrs {
			if o, ok := ptr.(optional); ok {
				vals[i] = reflect.ValueOf(o.ptr).Elem().Interface()
			} else {
				vals[i] = reflect.ValueOf(ptr).Elem().Interface()
			}
		}
		return vals, err
	}

	tt.Test(t, tt.Fn(scanListElementsToGo).Named("scanListElementsToGo"),
		Args(MakeList("1", "2"), 0, 0).Rets([]any{1, 2}),
		Args(MakeList("1", "2"), "", "").Rets([]any{"1", "2"}),
		Args(MakeList("1", "2"), 0, Optional(0)).Rets([]any{1, 2}),
		Args(MakeList("1"), 0, Optional(0)).Rets([]any{1, 0}),

		Args(MakeList("a"), 0).Rets([]any{0},
			cannotParseAs{"integer", "a"}),
		Args(MakeList("1"), 0, 0).Rets([]any{0, 0},
			errs.ArityMismatch{What: "list elements",
				ValidLow: 2, ValidHigh: 2, Actual: 1}),
		Args(MakeList("1"), 0, 0, Optional(0)).Rets([]any{0, 0, 0},
			errs.ArityMismatch{What: "list elements",
				ValidLow: 2, ValidHigh: 3, Actual: 1}),
	)
}

type aStruct struct {
	Foo int
	bar any
}

// Equal is required by cmp.Diff, since aStruct contains unexported fields.
func (a aStruct) Equal(b aStruct) bool { return a == b }

func TestScanMapToGo(t *testing.T) {
	// A wrapper around ScanMapToGo, to make it easier to test.
	scanMapToGo := func(src Map, dstInit any) (any, error) {
		ptr := reflect.New(TypeOf(dstInit))
		ptr.Elem().Set(reflect.ValueOf(dstInit))
		err := ScanMapToGo(src, ptr.Interface())
		return ptr.Elem().Interface(), err
	}

	tt.Test(t, tt.Fn(scanMapToGo).Named("scanMapToGo"),
		Args(MakeMap("foo", "1"), aStruct{}).Rets(aStruct{Foo: 1}),
		// More fields is OK
		Args(MakeMap("foo", "1", "bar", "x"), aStruct{}).Rets(aStruct{Foo: 1}),
		// Fewer fields is OK
		Args(MakeMap(), aStruct{}).Rets(aStruct{}),
		// Unexported fields are ignored
		Args(MakeMap("bar", 20), aStruct{bar: 10}).Rets(aStruct{bar: 10}),

		// Conversion error
		Args(MakeMap("foo", "a"), aStruct{}).
			Rets(aStruct{}, cannotParseAs{"integer", "a"}),
	)
}

func TestFromGo(t *testing.T) {
	tt.Test(t, FromGo,
		// BigInt -> int, when in range
		Args(bigInt(z)).Rets(bigInt(z)),
		Args(big.NewInt(100)).Rets(100),
		// BigRat -> BigInt or int, when denominator is 1
		Args(bigRat(z1+"/"+z)).Rets(bigRat(z1+"/"+z)),
		Args(bigRat(z+"/1")).Rets(bigInt(z)),
		Args(bigRat("2/1")).Rets(2),
		// rune -> string
		Args('x').Rets("x"),

		// Other types don't undergo any conversion
		Args(nil).Rets(nil),
		Args(someType{"foo"}).Rets(someType{"foo"}),
	)
}
