package types

import (
	"math/big"
	"math/rand"
	"testing"

	"github.com/stretchr/testify/suite"
)

type internalIntTestSuite struct {
	suite.Suite
}

func TestInternalIntTestSuite(t *testing.T) {
	suite.Run(t, new(internalIntTestSuite))
}

func (s *internalIntTestSuite) TestEncodingRandom() {
	for i := 0; i < 1000; i++ {
		n := rand.Int63()
		ni := NewInt(n)
		var ri Int

		str, err := ni.Marshal()
		s.Require().Nil(err)
		err = (&ri).Unmarshal(str)
		s.Require().Nil(err)

		s.Require().Equal(ni, ri, "binary mismatch; tc #%d, expected %s, actual %s", i, ni.String(), ri.String())
		s.Require().True(ni.i != ri.i, "pointer addresses are equal; tc #%d", i)

		bz, err := ni.MarshalJSON()
		s.Require().Nil(err)
		err = (&ri).UnmarshalJSON(bz)
		s.Require().Nil(err)

		s.Require().Equal(ni, ri, "json mismatch; tc #%d, expected %s, actual %s", i, ni.String(), ri.String())
		s.Require().True(ni.i != ri.i, "pointer addresses are equal; tc #%d", i)
	}

	for i := 0; i < 1000; i++ {
		n := rand.Uint64()
		ni := NewUint(n)
		var ri Uint

		str, err := ni.Marshal()
		s.Require().Nil(err)
		err = (&ri).Unmarshal(str)
		s.Require().Nil(err)

		s.Require().Equal(ni, ri, "binary mismatch; tc #%d, expected %s, actual %s", i, ni.String(), ri.String())
		s.Require().True(ni.i != ri.i, "pointer addresses are equal; tc #%d", i)

		bz, err := ni.MarshalJSON()
		s.Require().Nil(err)
		err = (&ri).UnmarshalJSON(bz)
		s.Require().Nil(err)

		s.Require().Equal(ni, ri, "json mismatch; tc #%d, expected %s, actual %s", i, ni.String(), ri.String())
		s.Require().True(ni.i != ri.i, "pointer addresses are equal; tc #%d", i)
	}
}

func (s *internalIntTestSuite) TestSerializationOverflow() {
	bx, _ := new(big.Int).SetString("91888242871839275229946405745257275988696311157297823662689937894645226298583", 10)
	x := Int{bx}
	y := new(Int)

	bz, err := x.Marshal()
	s.Require().NoError(err)

	// require deserialization to fail due to overflow
	s.Require().Error(y.Unmarshal(bz))

	// require JSON deserialization to fail due to overflow
	bz, err = x.MarshalJSON()
	s.Require().NoError(err)

	s.Require().Error(y.UnmarshalJSON(bz))
}

func (s *internalIntTestSuite) TestImmutabilityArithInt() {
	size := 500

	ops := []intOp{
		applyWithRand(Int.Add, (*big.Int).Add),
		applyWithRand(Int.Sub, (*big.Int).Sub),
		applyWithRand(Int.Mul, (*big.Int).Mul),
		applyWithRand(Int.Quo, (*big.Int).Quo),
		applyRawWithRand(Int.AddRaw, (*big.Int).Add),
		applyRawWithRand(Int.SubRaw, (*big.Int).Sub),
		applyRawWithRand(Int.MulRaw, (*big.Int).Mul),
		applyRawWithRand(Int.QuoRaw, (*big.Int).Quo),
	}

	for i := 0; i < 100; i++ {
		uis := make([]Int, size)
		bis := make([]*big.Int, size)

		n := rand.Int63()
		ui := NewInt(n)
		bi := new(big.Int).SetInt64(n)

		for j := 0; j < size; j++ {
			op := ops[rand.Intn(len(ops))]
			uis[j], bis[j] = op(ui, bi)
		}

		for j := 0; j < size; j++ {
			s.Require().Equal(0, bis[j].Cmp(uis[j].BigInt()), "Int is different from *big.Int. tc #%d, Int %s, *big.Int %s", j, uis[j].String(), bis[j].String())
			s.Require().Equal(NewIntFromBigInt(bis[j]), uis[j], "Int is different from *big.Int. tc #%d, Int %s, *big.Int %s", j, uis[j].String(), bis[j].String())
			s.Require().True(uis[j].i != bis[j], "Pointer addresses are equal. tc #%d, Int %s, *big.Int %s", j, uis[j].String(), bis[j].String())
		}
	}
}

type (
	intOp      func(Int, *big.Int) (Int, *big.Int)
	bigIntFunc func(*big.Int, *big.Int, *big.Int) *big.Int
)

func applyWithRand(intFn func(Int, Int) Int, bigIntFn bigIntFunc) intOp {
	return func(integer Int, bigInteger *big.Int) (Int, *big.Int) {
		r := rand.Int63()
		br := new(big.Int).SetInt64(r)
		return intFn(integer, NewInt(r)), bigIntFn(new(big.Int), bigInteger, br)
	}
}

func applyRawWithRand(intFn func(Int, int64) Int, bigIntFn bigIntFunc) intOp {
	return func(integer Int, bigInteger *big.Int) (Int, *big.Int) {
		r := rand.Int63()
		br := new(big.Int).SetInt64(r)
		return intFn(integer, r), bigIntFn(new(big.Int), bigInteger, br)
	}
}
