// Copyright 2013 The Go Authors. All rights reserved.
// Modified work copyright 2018 Alex Browne. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package constant

import (
	"fmt"
	"strings"
	"testing"

	"github.com/albrow/fo/token"
)

// TODO(gri) expand this test framework

var opTests = []string{
	// unary operations
	`+ 0 = 0`,
	`+ ? = ?`,
	`- 1 = -1`,
	`- ? = ?`,
	`^ 0 = -1`,
	`^ ? = ?`,

	`! true = false`,
	`! false = true`,
	`! ? = ?`,

	// etc.

	// binary operations
	`"" + "" = ""`,
	`"foo" + "" = "foo"`,
	`"" + "bar" = "bar"`,
	`"foo" + "bar" = "foobar"`,

	`0 + 0 = 0`,
	`0 + 0.1 = 0.1`,
	`0 + 0.1i = 0.1i`,
	`0.1 + 0.9 = 1`,
	`1e100 + 1e100 = 2e100`,
	`? + 0 = ?`,
	`0 + ? = ?`,

	`0 - 0 = 0`,
	`0 - 0.1 = -0.1`,
	`0 - 0.1i = -0.1i`,
	`1e100 - 1e100 = 0`,
	`? - 0 = ?`,
	`0 - ? = ?`,

	`0 * 0 = 0`,
	`1 * 0.1 = 0.1`,
	`1 * 0.1i = 0.1i`,
	`1i * 1i = -1`,
	`? * 0 = ?`,
	`0 * ? = ?`,

	`0 / 0 = "division_by_zero"`,
	`10 / 2 = 5`,
	`5 / 3 = 5/3`,
	`5i / 3i = 5/3`,
	`? / 0 = ?`,
	`0 / ? = ?`,

	`0 % 0 = "runtime_error:_integer_divide_by_zero"`, // TODO(gri) should be the same as for /
	`10 % 3 = 1`,
	`? % 0 = ?`,
	`0 % ? = ?`,

	`0 & 0 = 0`,
	`12345 & 0 = 0`,
	`0xff & 0xf = 0xf`,
	`? & 0 = ?`,
	`0 & ? = ?`,

	`0 | 0 = 0`,
	`12345 | 0 = 12345`,
	`0xb | 0xa0 = 0xab`,
	`? | 0 = ?`,
	`0 | ? = ?`,

	`0 ^ 0 = 0`,
	`1 ^ -1 = -2`,
	`? ^ 0 = ?`,
	`0 ^ ? = ?`,

	`0 &^ 0 = 0`,
	`0xf &^ 1 = 0xe`,
	`1 &^ 0xf = 0`,
	// etc.

	// shifts
	`0 << 0 = 0`,
	`1 << 10 = 1024`,
	`0 >> 0 = 0`,
	`1024 >> 10 == 1`,
	`? << 0 == ?`,
	`? >> 10 == ?`,
	// etc.

	// comparisons
	`false == false = true`,
	`false == true = false`,
	`true == false = false`,
	`true == true = true`,

	`false != false = false`,
	`false != true = true`,
	`true != false = true`,
	`true != true = false`,

	`"foo" == "bar" = false`,
	`"foo" != "bar" = true`,
	`"foo" < "bar" = false`,
	`"foo" <= "bar" = false`,
	`"foo" > "bar" = true`,
	`"foo" >= "bar" = true`,

	`0 == 0 = true`,
	`0 != 0 = false`,
	`0 < 10 = true`,
	`10 <= 10 = true`,
	`0 > 10 = false`,
	`10 >= 10 = true`,

	`1/123456789 == 1/123456789 == true`,
	`1/123456789 != 1/123456789 == false`,
	`1/123456789 < 1/123456788 == true`,
	`1/123456788 <= 1/123456789 == false`,
	`0.11 > 0.11 = false`,
	`0.11 >= 0.11 = true`,

	`? == 0 = false`,
	`? != 0 = false`,
	`? < 10 = false`,
	`? <= 10 = false`,
	`? > 10 = false`,
	`? >= 10 = false`,

	`0 == ? = false`,
	`0 != ? = false`,
	`0 < ? = false`,
	`10 <= ? = false`,
	`0 > ? = false`,
	`10 >= ? = false`,

	// etc.
}

func TestOps(t *testing.T) {
	for _, test := range opTests {
		a := strings.Split(test, " ")
		i := 0 // operator index

		var x, x0 Value
		switch len(a) {
		case 4:
			// unary operation
		case 5:
			// binary operation
			x, x0 = val(a[0]), val(a[0])
			i = 1
		default:
			t.Errorf("invalid test case: %s", test)
			continue
		}

		op, ok := optab[a[i]]
		if !ok {
			panic("missing optab entry for " + a[i])
		}

		y, y0 := val(a[i+1]), val(a[i+1])

		got := doOp(x, op, y)
		want := val(a[i+3])
		if !eql(got, want) {
			t.Errorf("%s: got %s; want %s", test, got, want)
			continue
		}

		if x0 != nil && !eql(x, x0) {
			t.Errorf("%s: x changed to %s", test, x)
			continue
		}

		if !eql(y, y0) {
			t.Errorf("%s: y changed to %s", test, y)
			continue
		}
	}
}

func eql(x, y Value) bool {
	_, ux := x.(unknownVal)
	_, uy := y.(unknownVal)
	if ux || uy {
		return ux == uy
	}
	return Compare(x, token.EQL, y)
}

// ----------------------------------------------------------------------------
// String tests

var xxx = strings.Repeat("x", 68)
var issue14262 = `"بموجب الشروط التالية نسب المصنف — يجب عليك أن تنسب العمل بالطريقة التي تحددها المؤلف أو المرخص (ولكن ليس بأي حال من الأحوال أن توحي وتقترح بتحول أو استخدامك للعمل).  المشاركة على قدم المساواة — إذا كنت يعدل ، والتغيير ، أو الاستفادة من هذا العمل ، قد ينتج عن توزيع العمل إلا في ظل تشابه او تطابق فى واحد لهذا الترخيص."`

var stringTests = []struct {
	input, short, exact string
}{
	// Unknown
	{"", "unknown", "unknown"},
	{"0x", "unknown", "unknown"},
	{"'", "unknown", "unknown"},
	{"1f0", "unknown", "unknown"},
	{"unknown", "unknown", "unknown"},

	// Bool
	{"true", "true", "true"},
	{"false", "false", "false"},

	// String
	{`""`, `""`, `""`},
	{`"foo"`, `"foo"`, `"foo"`},
	{`"` + xxx + `xx"`, `"` + xxx + `xx"`, `"` + xxx + `xx"`},
	{`"` + xxx + `xxx"`, `"` + xxx + `...`, `"` + xxx + `xxx"`},
	{`"` + xxx + xxx + `xxx"`, `"` + xxx + `...`, `"` + xxx + xxx + `xxx"`},
	{issue14262, `"بموجب الشروط التالية نسب المصنف — يجب عليك أن تنسب العمل بالطريقة ال...`, issue14262},

	// Int
	{"0", "0", "0"},
	{"-1", "-1", "-1"},
	{"12345", "12345", "12345"},
	{"-12345678901234567890", "-12345678901234567890", "-12345678901234567890"},
	{"12345678901234567890", "12345678901234567890", "12345678901234567890"},

	// Float
	{"0.", "0", "0"},
	{"-0.0", "0", "0"},
	{"10.0", "10", "10"},
	{"2.1", "2.1", "21/10"},
	{"-2.1", "-2.1", "-21/10"},
	{"1e9999", "1e+9999", "0x.f8d4a9da224650a8cb2959e10d985ad92adbd44c62917e608b1f24c0e1b76b6f61edffeb15c135a4b601637315f7662f325f82325422b244286a07663c9415d2p+33216"},
	{"1e-9999", "1e-9999", "0x.83b01ba6d8c0425eec1b21e96f7742d63c2653ed0a024cf8a2f9686df578d7b07d7a83d84df6a2ec70a921d1f6cd5574893a7eda4d28ee719e13a5dce2700759p-33215"},
	{"2.71828182845904523536028747135266249775724709369995957496696763", "2.71828", "271828182845904523536028747135266249775724709369995957496696763/100000000000000000000000000000000000000000000000000000000000000"},
	{"0e9999999999", "0", "0"},   // issue #16176
	{"-6e-1886451601", "0", "0"}, // issue #20228

	// Complex
	{"0i", "(0 + 0i)", "(0 + 0i)"},
	{"-0i", "(0 + 0i)", "(0 + 0i)"},
	{"10i", "(0 + 10i)", "(0 + 10i)"},
	{"-10i", "(0 + -10i)", "(0 + -10i)"},
	{"1e9999i", "(0 + 1e+9999i)", "(0 + 0x.f8d4a9da224650a8cb2959e10d985ad92adbd44c62917e608b1f24c0e1b76b6f61edffeb15c135a4b601637315f7662f325f82325422b244286a07663c9415d2p+33216i)"},
}

func TestString(t *testing.T) {
	for _, test := range stringTests {
		x := val(test.input)
		if got := x.String(); got != test.short {
			t.Errorf("%s: got %q; want %q as short string", test.input, got, test.short)
		}
		if got := x.ExactString(); got != test.exact {
			t.Errorf("%s: got %q; want %q as exact string", test.input, got, test.exact)
		}
	}
}

// ----------------------------------------------------------------------------
// Support functions

func val(lit string) Value {
	if len(lit) == 0 {
		return MakeUnknown()
	}

	switch lit {
	case "?":
		return MakeUnknown()
	case "true":
		return MakeBool(true)
	case "false":
		return MakeBool(false)
	}

	if i := strings.IndexByte(lit, '/'); i >= 0 {
		// assume fraction
		a := MakeFromLiteral(lit[:i], token.INT, 0)
		b := MakeFromLiteral(lit[i+1:], token.INT, 0)
		return BinaryOp(a, token.QUO, b)
	}

	tok := token.INT
	switch first, last := lit[0], lit[len(lit)-1]; {
	case first == '"' || first == '`':
		tok = token.STRING
		lit = strings.Replace(lit, "_", " ", -1)
	case first == '\'':
		tok = token.CHAR
	case last == 'i':
		tok = token.IMAG
	default:
		if !strings.HasPrefix(lit, "0x") && strings.ContainsAny(lit, "./Ee") {
			tok = token.FLOAT
		}
	}

	return MakeFromLiteral(lit, tok, 0)
}

var optab = map[string]token.Token{
	"!": token.NOT,

	"+": token.ADD,
	"-": token.SUB,
	"*": token.MUL,
	"/": token.QUO,
	"%": token.REM,

	"<<": token.SHL,
	">>": token.SHR,

	"&":  token.AND,
	"|":  token.OR,
	"^":  token.XOR,
	"&^": token.AND_NOT,

	"==": token.EQL,
	"!=": token.NEQ,
	"<":  token.LSS,
	"<=": token.LEQ,
	">":  token.GTR,
	">=": token.GEQ,
}

func panicHandler(v *Value) {
	switch p := recover().(type) {
	case nil:
		// nothing to do
	case string:
		*v = MakeString(p)
	case error:
		*v = MakeString(p.Error())
	default:
		panic(p)
	}
}

func doOp(x Value, op token.Token, y Value) (z Value) {
	defer panicHandler(&z)

	if x == nil {
		return UnaryOp(op, y, 0)
	}

	switch op {
	case token.EQL, token.NEQ, token.LSS, token.LEQ, token.GTR, token.GEQ:
		return MakeBool(Compare(x, op, y))
	case token.SHL, token.SHR:
		s, _ := Int64Val(y)
		return Shift(x, op, uint(s))
	default:
		return BinaryOp(x, op, y)
	}
}

// ----------------------------------------------------------------------------
// Other tests

var fracTests = []string{
	"0",
	"1",
	"-1",
	"1.2",
	"-0.991",
	"2.718281828",
	"3.14159265358979323e-10",
	"1e100",
	"1e1000",
}

func TestFractions(t *testing.T) {
	for _, test := range fracTests {
		x := val(test)
		// We don't check the actual numerator and denominator because they
		// are unlikely to be 100% correct due to floatVal rounding errors.
		// Instead, we compute the fraction again and compare the rounded
		// result.
		q := BinaryOp(Num(x), token.QUO, Denom(x))
		got := q.String()
		want := x.String()
		if got != want {
			t.Errorf("%s: got quotient %s, want %s", x, got, want)
		}
	}
}

var bytesTests = []string{
	"0",
	"1",
	"123456789",
	"123456789012345678901234567890123456789012345678901234567890",
}

func TestBytes(t *testing.T) {
	for _, test := range bytesTests {
		x := val(test)
		bytes := Bytes(x)

		// special case 0
		if Sign(x) == 0 && len(bytes) != 0 {
			t.Errorf("%s: got %v; want empty byte slice", test, bytes)
		}

		if n := len(bytes); n > 0 && bytes[n-1] == 0 {
			t.Errorf("%s: got %v; want no leading 0 byte", test, bytes)
		}

		if got := MakeFromBytes(bytes); !eql(got, x) {
			t.Errorf("%s: got %s; want %s (bytes = %v)", test, got, x, bytes)
		}
	}
}

func TestUnknown(t *testing.T) {
	u := MakeUnknown()
	var values = []Value{
		u,
		MakeBool(false), // token.ADD ok below, operation is never considered
		MakeString(""),
		MakeInt64(1),
		MakeFromLiteral("-1234567890123456789012345678901234567890", token.INT, 0),
		MakeFloat64(1.2),
		MakeImag(MakeFloat64(1.2)),
	}
	for _, val := range values {
		x, y := val, u
		for i := range [2]int{} {
			if i == 1 {
				x, y = y, x
			}
			if got := BinaryOp(x, token.ADD, y); got.Kind() != Unknown {
				t.Errorf("%s + %s: got %s; want %s", x, y, got, u)
			}
			if got := Compare(x, token.EQL, y); got {
				t.Errorf("%s == %s: got true; want false", x, y)
			}
		}
	}
}

func BenchmarkStringAdd(b *testing.B) {
	for size := 1; size <= 65536; size *= 4 {
		b.Run(fmt.Sprint(size), func(b *testing.B) {
			b.ReportAllocs()
			n := int64(0)
			for i := 0; i < b.N; i++ {
				x := MakeString(strings.Repeat("x", 100))
				y := x
				for j := 0; j < size-1; j++ {
					y = BinaryOp(y, token.ADD, x)
				}
				n += int64(len(StringVal(y)))
			}
			if n != int64(b.N)*int64(size)*100 {
				b.Fatalf("bad string %d != %d", n, int64(b.N)*int64(size)*100)
			}
		})
	}
}
