package csidh

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

	"golang.org/x/sys/cpu"
)

func resetCPUFeatures() {
	hasBMI2 = cpu.X86.HasBMI2
	hasADXandBMI2 = cpu.X86.HasBMI2 && cpu.X86.HasADX
}

func testFp512Mul3Nominal(t *testing.T) {
	var multiplier64 uint64
	var mod big.Int

	// modulus: 2^512
	mod.SetUint64(1).Lsh(&mod, 512)

	for i := 0; i < numIter; i++ {
		multiplier64 = rand.Uint64()

		fV := randomFp()
		exp, _ := new(big.Int).SetString(fp2S(fV), 16)
		exp.Mul(exp, new(big.Int).SetUint64(multiplier64))
		// Truncate to 512 bits
		exp.Mod(exp, &mod)

		mul512(&fV, &fV, multiplier64)
		res, _ := new(big.Int).SetString(fp2S(fV), 16)

		if exp.Cmp(res) != 0 {
			t.Errorf("%X != %X", exp, res)
		}
	}
}

// Check if mul512 produces result
// z = x*y mod 2^512
func TestFp512Mul3_Nominal(t *testing.T) {
	hasBMI2 = false
	testFp512Mul3Nominal(t)

	resetCPUFeatures()
	testFp512Mul3Nominal(t)

}

func TestAddRdcRandom(t *testing.T) {
	for i := 0; i < numIter; i++ {
		a := randomFp()
		bigA, _ := new(big.Int).SetString(fp2S(a), 16)
		bigA.Mod(bigA, modulus)
		copy(a[:], intGetU64(bigA))

		b := randomFp()
		bigB, _ := new(big.Int).SetString(fp2S(b), 16)
		bigB.Mod(bigB, modulus)
		copy(b[:], intGetU64(bigB))

		addRdc(&a, &a, &b)
		bigRet, _ := new(big.Int).SetString(fp2S(a), 16)

		bigA.Add(bigA, bigB)
		bigA.Mod(bigA, modulus)

		if bigRet.Cmp(bigA) != 0 {
			t.Errorf("%X != %X", bigRet, bigA)
		}
	}
}

func TestAddRdcNominal(t *testing.T) {
	var res fp

	tmp := oneFp512
	addRdc(&res, &tmp, &p)
	if !ceq512(&res, &tmp) {
		t.Errorf("Wrong value\n%X", res)
	}

	tmp = zeroFp512
	addRdc(&res, &p, &p)
	if !ceq512(&res, &p) {
		t.Errorf("Wrong value\n%X", res)
	}

	tmp = fp{1, 1, 1, 1, 1, 1, 1, 1}
	addRdc(&res, &p, &tmp)
	if !ceq512(&res, &tmp) {
		t.Errorf("Wrong value\n%X", res)
	}

	tmp = fp{1, 1, 1, 1, 1, 1, 1, 1}
	exp := fp{2, 2, 2, 2, 2, 2, 2, 2}
	addRdc(&res, &tmp, &tmp)
	if !ceq512(&res, &exp) {
		t.Errorf("Wrong value\n%X", res)
	}
}

func TestFp512Sub3_Nominal(t *testing.T) {
	var ret fp
	var mod big.Int
	// modulus: 2^512
	mod.SetUint64(1).Lsh(&mod, 512)

	for i := 0; i < numIter; i++ {
		a := randomFp()
		bigA, _ := new(big.Int).SetString(fp2S(a), 16)
		b := randomFp()
		bigB, _ := new(big.Int).SetString(fp2S(b), 16)

		sub512(&ret, &a, &b)
		bigRet, _ := new(big.Int).SetString(fp2S(ret), 16)
		bigA.Sub(bigA, bigB)
		// Truncate to 512 bits
		bigA.Mod(bigA, &mod)

		if bigRet.Cmp(bigA) != 0 {
			t.Errorf("%X != %X", bigRet, bigA)
		}
	}
}

func TestFp512Sub3_DoesntReturnCarry(t *testing.T) {
	a := fp{}
	b := fp{
		0xFFFFFFFFFFFFFFFF, 1,
		0, 0,
		0, 0,
		0, 0}
	c := fp{
		0xFFFFFFFFFFFFFFFF, 2,
		0, 0,
		0, 0,
		0, 0}

	if sub512(&a, &b, &c) != 1 {
		t.Error("Carry not returned")
	}
}

func TestFp512Sub3_ReturnsCarry(t *testing.T) {
	a := fp{}
	b := fp{
		0xFFFFFFFFFFFFFFFF, 2,
		0, 0,
		0, 0,
		0, 0}
	c := fp{
		0xFFFFFFFFFFFFFFFF, 1,
		0, 0,
		0, 0,
		0, 0}

	if sub512(&a, &b, &c) != 0 {
		t.Error("Carry not returned")
	}
}

func TestCswap(t *testing.T) {
	arg1 := randomFp()
	arg2 := randomFp()

	arg1cpy := arg1
	cswap512(&arg1, &arg2, 0)
	if !ceq512(&arg1, &arg1cpy) {
		t.Error("cswap swapped")
	}

	arg1cpy = arg1
	cswap512(&arg1, &arg2, 1)
	if ceq512(&arg1, &arg1cpy) {
		t.Error("cswap didn't swapped")
	}

	arg1cpy = arg1
	cswap512(&arg1, &arg2, 0xF2)
	if ceq512(&arg1, &arg1cpy) {
		t.Error("cswap didn't swapped")
	}
}

func TestSubRdc(t *testing.T) {
	var res fp

	// 1 - 1 mod P
	tmp := oneFp512
	subRdc(&res, &tmp, &tmp)
	if !ceq512(&res, &zeroFp512) {
		t.Errorf("Wrong value\n%X", res)
	}
	zero(&res)

	// 0 - 1 mod P
	exp := p
	exp[0]--

	subRdc(&res, &zeroFp512, &oneFp512)
	if !ceq512(&res, &exp) {
		t.Errorf("Wrong value\n%X\n%X", res, exp)
	}
	zero(&res)

	// P - (P-1)
	pMinusOne := p
	pMinusOne[0]--
	subRdc(&res, &p, &pMinusOne)
	if !ceq512(&res, &oneFp512) {
		t.Errorf("Wrong value\n[%X != %X]", res, oneFp512)
	}
	zero(&res)

	subRdc(&res, &p, &oneFp512)
	if !ceq512(&res, &pMinusOne) {
		t.Errorf("Wrong value\n[%X != %X]", res, pMinusOne)
	}
}

func testMulRdc(t *testing.T) {
	var res fp
	var m1 = fp{
		0x85E2579C786882D0, 0x4E3433657E18DA95,
		0x850AE5507965A0B3, 0xA15BC4E676475964}
	var m2 = fp{
		0x85E2579C786882CF, 0x4E3433657E18DA95,
		0x850AE5507965A0B3, 0xA15BC4E676475964}

	// Expected
	var m1m1 = fp{
		0xAEBF46E92C88A4B4, 0xCFE857977B946347,
		0xD3B264FF08493901, 0x6EEB3D23746B6C7C,
		0xC0CA874A349D64B4, 0x7AD4A38B406F8504,
		0x38B6B6CEB82472FB, 0x1587015FD7DDFC7D}
	var m1m2 = fp{
		0x51534771258C4624, 0x2BFEDE86504E2160,
		0xE8127D5E9329670B, 0x0C84DBD584491D75,
		0x656C73C68B16E38C, 0x01C0DA470B30B8DE,
		0x2532E3903EAA950B, 0x3F2C28EA97FE6FEC}

	// 0*0
	tmp := zeroFp512
	mulRdc(&res, &tmp, &tmp)
	if !ceq512(&res, &tmp) {
		t.Errorf("Wrong value\n%X", res)
	}

	// 1*m1 == m1
	zero(&res)
	mulRdc(&res, &m1, &one)
	if !ceq512(&res, &m1) {
		t.Errorf("Wrong value\n%X", res)
	}

	// m1*m2 < p
	zero(&res)
	mulRdc(&res, &m1, &m2)
	if !ceq512(&res, &m1m2) {
		t.Errorf("Wrong value\n%X", res)
	}

	// m1*m1 > p
	zero(&res)
	mulRdc(&res, &m1, &m1)
	if !ceq512(&res, &m1m1) {
		t.Errorf("Wrong value\n%X", res)
	}
}

func TestMulRdc(t *testing.T) {
	hasADXandBMI2 = false
	testMulRdc(t)

	resetCPUFeatures()
	testMulRdc(t)
}

func TestModExp(t *testing.T) {
	var resExp, base, exp big.Int
	var baseFp, expFp, resFp, resFpExp fp

	for i := 0; i < numIter; i++ {
		// Perform modexp with reference implementation
		// in Montgomery domain
		base.SetString(fp2S(randomFp()), 16)
		exp.SetString(fp2S(randomFp()), 16)
		resExp.Exp(&base, &exp, modulus)
		toMont(&base, true)
		toMont(&resExp, true)

		// Convert to fp
		copy(baseFp[:], intGetU64(&base))
		copy(expFp[:], intGetU64(&exp))
		copy(resFpExp[:], intGetU64(&resExp))

		// Perform modexp with our implementation
		modExpRdc512(&resFp, &baseFp, &expFp)

		if !ceq512(&resFp, &resFpExp) {
			t.Errorf("Wrong value\n%X!=%X", resFp, intGetU64(&resExp))
		}
	}
}

// Test uses Euler's Criterion
func TestIsNonQuadRes(t *testing.T) {
	var n, nMont big.Int
	var pm1o2, rawP big.Int
	var nMontFp fp

	// (p-1)/2
	pm1o2.SetString("0x32da4747ba07c4dffe455868af1f26255a16841d76e446212d7dfe63499164e6d3d56362b3f9aa83a8b398660f85a792e1390dfa2bd6541a8dc0dc8299e3643d", 0)
	// modulus value (not in montgomery)
	rawP.SetString("0x65b48e8f740f89bffc8ab0d15e3e4c4ab42d083aedc88c425afbfcc69322c9cda7aac6c567f35507516730cc1f0b4f25c2721bf457aca8351b81b90533c6c87b", 0)

	// There is 641 quadratic residues in this range
	for i := uint64(1); i < 1000; i++ {
		n.SetUint64(i)
		n.Exp(&n, &pm1o2, &rawP)
		// exp == 1 iff n is quadratic non-residue
		exp := n.Cmp(big.NewInt(1))
		if exp < 0 {
			panic("Should never happen")
		}

		nMont.SetUint64(i)
		toMont(&nMont, true)
		copy(nMontFp[:], intGetU64(&nMont))
		ret := nMontFp.isNonQuadRes()

		if ret != exp {
			toMont(&nMont, false)
			t.Errorf("Test failed for value %s", nMont.Text(10))
		}
	}
}

func TestCheckSmaller(t *testing.T) {
	// p-1
	var pMin1 = p
	pMin1[0]--

	// p-1 < p => 1
	if !isLess(&pMin1, &p) {
		t.Error("pMin1>p")
	}

	// p < p-1 => 0
	if isLess(&p, &pMin1) {
		t.Error("p>pMin1")
	}

	// p == p => 0
	if isLess(&p, &p) {
		t.Error("p==p")
	}
}

func BenchmarkFp512Sub(b *testing.B) {
	var arg1 fp
	arg2, arg3 := randomFp(), randomFp()
	for n := 0; n < b.N; n++ {
		sub512(&arg1, &arg2, &arg3)
	}
}

func BenchmarkFp512Mul(b *testing.B) {
	var arg1 = rand.Uint64()
	arg2, arg3 := randomFp(), randomFp()
	for n := 0; n < b.N; n++ {
		mul512(&arg2, &arg3, arg1)
	}
}

func BenchmarkCSwap(b *testing.B) {
	arg1 := randomFp()
	arg2 := randomFp()
	for n := 0; n < b.N; n++ {
		cswap512(&arg1, &arg2, uint8(n%2))
	}
}

func BenchmarkAddRdc(b *testing.B) {
	var res fp
	arg1 := randomFp()
	arg2 := randomFp()

	for n := 0; n < b.N; n++ {
		addRdc(&res, &arg1, &arg2)
	}
}

func BenchmarkSubRdc(b *testing.B) {
	arg1 := randomFp()
	arg2 := randomFp()
	var res fp
	for n := 0; n < b.N; n++ {
		subRdc(&res, &arg1, &arg2)
	}
}

func BenchmarkModExpRdc(b *testing.B) {
	arg1 := randomFp()
	arg2 := randomFp()
	var res fp
	for n := 0; n < b.N; n++ {
		modExpRdc512(&res, &arg1, &arg2)
	}
}

func BenchmarkMulGeneric(b *testing.B) {
	arg1 := randomFp()
	arg2 := randomFp()
	var res fp
	for n := 0; n < b.N; n++ {
		mulGeneric(&res, &arg1, &arg2)
	}
}

func BenchmarkMulBmiAsm(b *testing.B) {
	arg1 := randomFp()
	arg2 := randomFp()
	var res fp
	for n := 0; n < b.N; n++ {
		mulRdc(&res, &arg1, &arg2)
	}
}

func BenchmarkMulGenAsm(b *testing.B) {
	arg1 := randomFp()
	arg2 := randomFp()
	var res fp
	hasADXandBMI2 = false
	for n := 0; n < b.N; n++ {
		mulRdc(&res, &arg1, &arg2)
	}

	resetCPUFeatures()
}
