//go:build !noasm && !appengine && gc
// +build !noasm,!appengine,gc

/*
 * Minio Cloud Storage, (C) 2017 Minio, Inc.
 *
 * 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 sha256

import (
	"bytes"
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"hash"
	"reflect"
	"sync"
	"testing"
)

func TestGoldenAVX512(t *testing.T) {

	if !hasAvx512 {
		// t.SkipNow()
		return
	}

	server := NewAvx512Server()
	h512 := NewAvx512(server)

	for _, g := range golden {
		h512.Reset()
		h512.Write([]byte(g.in))
		digest := h512.Sum([]byte{})
		s := fmt.Sprintf("%x", digest)
		if !reflect.DeepEqual(digest, g.out[:]) {
			t.Fatalf(
				"Sum256 function: sha256(%s) = %s want %s", g.in, s,
				hex.EncodeToString(g.out[:]),
			)
		}
	}
}

func createInputs(size int) [16][]byte {
	input := [16][]byte{}
	for i := 0; i < 16; i++ {
		input[i] = make([]byte, size)
	}
	return input
}

func initDigests() *[512]byte {
	digests := [512]byte{}
	for i := 0; i < 16; i++ {
		binary.LittleEndian.PutUint32(digests[(i+0*16)*4:], init0)
		binary.LittleEndian.PutUint32(digests[(i+1*16)*4:], init1)
		binary.LittleEndian.PutUint32(digests[(i+2*16)*4:], init2)
		binary.LittleEndian.PutUint32(digests[(i+3*16)*4:], init3)
		binary.LittleEndian.PutUint32(digests[(i+4*16)*4:], init4)
		binary.LittleEndian.PutUint32(digests[(i+5*16)*4:], init5)
		binary.LittleEndian.PutUint32(digests[(i+6*16)*4:], init6)
		binary.LittleEndian.PutUint32(digests[(i+7*16)*4:], init7)
	}
	return &digests
}

func testSha256Avx512(t *testing.T, offset, padding int) [16][]byte {

	if !hasAvx512 {
		// t.SkipNow()
		return [16][]byte{}
	}

	l := uint(len(golden[offset].in))
	extraBlock := uint(0)
	if padding == 0 {
		extraBlock += 9
	} else {
		extraBlock += 64
	}
	input := createInputs(int(l + extraBlock))
	for i := 0; i < 16; i++ {
		copy(input[i], golden[offset+i].in)
		input[i][l] = 0x80
		copy(input[i][l+1:], bytes.Repeat([]byte{0}, padding))

		// Length in bits.
		len := uint64(l)
		len <<= 3
		for ii := uint(0); ii < 8; ii++ {
			input[i][l+1+uint(padding)+ii] = byte(len >> (56 - 8*ii))
		}
	}
	mask := make([]uint64, len(input[0])>>6)
	for m := range mask {
		mask[m] = 0xffff
	}
	output := blockAvx512(initDigests(), input, mask)
	for i := 0; i < 16; i++ {
		if bytes.Compare(output[i][:], golden[offset+i].out[:]) != 0 {
			t.Fatalf(
				"Sum256 function: sha256(%s) = %s want %s", golden[offset+i].in,
				hex.EncodeToString(output[i][:]),
				hex.EncodeToString(golden[offset+i].out[:]),
			)
		}
	}
	return input
}

func TestAvx512_1Block(t *testing.T)  { testSha256Avx512(t, 31, 0) }
func TestAvx512_3Blocks(t *testing.T) { testSha256Avx512(t, 47, 55) }

func TestAvx512_MixedBlocks(t *testing.T) {

	if !hasAvx512 {
		// t.SkipNow()
		return
	}

	inputSingleBlock := testSha256Avx512(t, 31, 0)
	inputMultiBlock := testSha256Avx512(t, 47, 55)

	input := [16][]byte{}

	for i := range input {
		if i%2 == 0 {
			input[i] = inputMultiBlock[i]
		} else {
			input[i] = inputSingleBlock[i]
		}
	}

	mask := [3]uint64{0xffff, 0x5555, 0x5555}
	output := blockAvx512(initDigests(), input, mask[:])
	var offset int
	for i := 0; i < len(output); i++ {
		if i%2 == 0 {
			offset = 47
		} else {
			offset = 31
		}
		if bytes.Compare(output[i][:], golden[offset+i].out[:]) != 0 {
			t.Fatalf(
				"Sum256 function: sha256(%s) = %s want %s", golden[offset+i].in,
				hex.EncodeToString(output[i][:]),
				hex.EncodeToString(golden[offset+i].out[:]),
			)
		}
	}
}

func TestAvx512_MixedWithNilBlocks(t *testing.T) {

	if !hasAvx512 {
		// t.SkipNow()
		return
	}

	inputSingleBlock := testSha256Avx512(t, 31, 0)
	inputMultiBlock := testSha256Avx512(t, 47, 55)

	input := [16][]byte{}

	for i := range input {
		if i%3 == 0 {
			input[i] = inputMultiBlock[i]
		} else if i%3 == 1 {
			input[i] = inputSingleBlock[i]
		} else {
			input[i] = nil
		}
	}

	mask := [3]uint64{0xb6db, 0x9249, 0x9249}
	output := blockAvx512(initDigests(), input, mask[:])
	var offset int
	for i := 0; i < len(output); i++ {
		if i%3 == 2 { // for nil inputs
			initvec := [32]byte{
				0x6a, 0x09, 0xe6, 0x67, 0xbb, 0x67, 0xae, 0x85,
				0x3c, 0x6e, 0xf3, 0x72, 0xa5, 0x4f, 0xf5, 0x3a,
				0x51, 0x0e, 0x52, 0x7f, 0x9b, 0x05, 0x68, 0x8c,
				0x1f, 0x83, 0xd9, 0xab, 0x5b, 0xe0, 0xcd, 0x19,
			}
			if bytes.Compare(output[i][:], initvec[:]) != 0 {
				t.Fatalf(
					"Sum256 function: sha256 for nil vector = %s want %s",
					hex.EncodeToString(output[i][:]),
					hex.EncodeToString(initvec[:]),
				)
			}
			continue
		}
		if i%3 == 0 {
			offset = 47
		} else {
			offset = 31
		}
		if bytes.Compare(output[i][:], golden[offset+i].out[:]) != 0 {
			t.Fatalf(
				"Sum256 function: sha256(%s) = %s want %s", golden[offset+i].in,
				hex.EncodeToString(output[i][:]),
				hex.EncodeToString(golden[offset+i].out[:]),
			)
		}
	}
}

func TestAvx512Server(t *testing.T) {

	if !hasAvx512 {
		// t.SkipNow()
		return
	}

	const offset = 31 + 16
	server := NewAvx512Server()

	// First block of 64 bytes
	for i := 0; i < 16; i++ {
		input := make([]byte, 64)
		copy(input, golden[offset+i].in)
		server.Write(uint64(Avx512ServerUID+i), input)
	}

	// Second block of 64 bytes
	for i := 0; i < 16; i++ {
		input := make([]byte, 64)
		copy(input, golden[offset+i].in[64:])
		server.Write(uint64(Avx512ServerUID+i), input)
	}

	wg := sync.WaitGroup{}
	wg.Add(16)

	// Third and final block
	for i := 0; i < 16; i++ {
		input := make([]byte, 64)
		input[0] = 0x80
		copy(input[1:], bytes.Repeat([]byte{0}, 63-8))

		// Length in bits.
		len := uint64(128)
		len <<= 3
		for ii := uint(0); ii < 8; ii++ {
			input[63-8+1+ii] = byte(len >> (56 - 8*ii))
		}
		go func(i int, uid uint64, input []byte) {
			output := server.Sum(uid, input)
			if bytes.Compare(output[:], golden[offset+i].out[:]) != 0 {
				t.Fatalf(
					"Sum256 function: sha256(%s) = %s want %s",
					golden[offset+i].in,
					hex.EncodeToString(output[:]),
					hex.EncodeToString(golden[offset+i].out[:]),
				)
			}
			wg.Done()
		}(i, uint64(Avx512ServerUID+i), input)
	}

	wg.Wait()
}

func TestAvx512Digest(t *testing.T) {

	if !hasAvx512 {
		// t.SkipNow()
		return
	}

	server := NewAvx512Server()

	const tests = 16
	h512 := [16]hash.Hash{}
	for i := 0; i < tests; i++ {
		h512[i] = NewAvx512(server)
	}

	const offset = 31 + 16
	for i := 0; i < tests; i++ {
		input := make([]byte, 64)
		copy(input, golden[offset+i].in)
		h512[i].Write(input)
	}
	for i := 0; i < tests; i++ {
		input := make([]byte, 64)
		copy(input, golden[offset+i].in[64:])
		h512[i].Write(input)
	}
	for i := 0; i < tests; i++ {
		output := h512[i].Sum([]byte{})
		if bytes.Compare(output[:], golden[offset+i].out[:]) != 0 {
			t.Fatalf(
				"Sum256 function: sha256(%s) = %s want %s", golden[offset+i].in,
				hex.EncodeToString(output[:]),
				hex.EncodeToString(golden[offset+i].out[:]),
			)
		}
	}
}

func benchmarkAvx512SingleCore(h512 []hash.Hash, body []byte) {

	for i := 0; i < len(h512); i++ {
		h512[i].Write(body)
	}
	for i := 0; i < len(h512); i++ {
		_ = h512[i].Sum([]byte{})
	}
}

func benchmarkAvx512(b *testing.B, size int) {

	if !hasAvx512 {
		b.SkipNow()
		return
	}

	server := NewAvx512Server()

	const tests = 16
	body := make([]byte, size)

	b.SetBytes(int64(len(body) * tests))
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		h512 := make([]hash.Hash, tests)
		for i := 0; i < tests; i++ {
			h512[i] = NewAvx512(server)
		}

		benchmarkAvx512SingleCore(h512, body)
	}
}

func BenchmarkAvx512_05M(b *testing.B) { benchmarkAvx512(b, 512*1024) }
func BenchmarkAvx512_1M(b *testing.B)  { benchmarkAvx512(b, 1*1024*1024) }
func BenchmarkAvx512_5M(b *testing.B)  { benchmarkAvx512(b, 5*1024*1024) }
func BenchmarkAvx512_10M(b *testing.B) { benchmarkAvx512(b, 10*1024*1024) }

func benchmarkAvx512MultiCore(b *testing.B, size, cores int) {

	if !hasAvx512 {
		b.SkipNow()
		return
	}

	servers := make([]*Avx512Server, cores)
	for c := 0; c < cores; c++ {
		servers[c] = NewAvx512Server()
	}

	const tests = 16

	body := make([]byte, size)

	h512 := make([]hash.Hash, tests*cores)
	for i := 0; i < tests*cores; i++ {
		h512[i] = NewAvx512(servers[i>>4])
	}

	b.SetBytes(int64(size * 16 * cores))
	b.ResetTimer()

	var wg sync.WaitGroup

	for i := 0; i < b.N; i++ {
		wg.Add(cores)
		for c := 0; c < cores; c++ {
			go func(c int) {
				benchmarkAvx512SingleCore(
					h512[c*tests:(c+1)*tests],
					body,
				)
				wg.Done()
			}(c)
		}
		wg.Wait()
	}
}

func BenchmarkAvx512_5M_2Cores(b *testing.B) {
	benchmarkAvx512MultiCore(
		b, 5*1024*1024, 2,
	)
}
func BenchmarkAvx512_5M_4Cores(b *testing.B) {
	benchmarkAvx512MultiCore(
		b, 5*1024*1024, 4,
	)
}
func BenchmarkAvx512_5M_6Cores(b *testing.B) {
	benchmarkAvx512MultiCore(
		b, 5*1024*1024, 6,
	)
}

type maskTest struct {
	in  [16]int
	out [16]maskRounds
}

var goldenMask = []maskTest{
	{[16]int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, [16]maskRounds{}},
	{
		[16]int{64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0},
		[16]maskRounds{{0x5555, 1}},
	},
	{
		[16]int{0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64},
		[16]maskRounds{{0xaaaa, 1}},
	},
	{
		[16]int{64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64},
		[16]maskRounds{{0xffff, 1}},
	},
	{
		[16]int{
			128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128,
			128, 128, 128,
		},
		[16]maskRounds{{0xffff, 2}},
	},
	{
		[16]int{
			64, 128, 64, 128, 64, 128, 64, 128, 64, 128, 64, 128, 64, 128, 64,
			128,
		},
		[16]maskRounds{{0xffff, 1}, {0xaaaa, 1}},
	},
	{
		[16]int{
			128, 64, 128, 64, 128, 64, 128, 64, 128, 64, 128, 64, 128, 64, 128,
			64,
		},
		[16]maskRounds{{0xffff, 1}, {0x5555, 1}},
	},
	{
		[16]int{
			64, 192, 64, 192, 64, 192, 64, 192, 64, 192, 64, 192, 64, 192, 64,
			192,
		},
		[16]maskRounds{{0xffff, 1}, {0xaaaa, 2}},
	},
	//
	//  >= 64   0110=6          1011=b          1101=d           0110=6
	//  >=128   0100=4          0010=2          1001=9           0100=4
	{
		[16]int{0, 64, 128, 0, 64, 128, 0, 64, 128, 0, 64, 128, 0, 64, 128, 0},
		[16]maskRounds{{0x6db6, 1}, {0x4924, 1}},
	},
	{
		[16]int{
			1 * 64, 2 * 64, 3 * 64, 4 * 64, 5 * 64, 6 * 64, 7 * 64, 8 * 64,
			9 * 64, 10 * 64,
			11 * 64, 12 * 64, 13 * 64, 14 * 64, 15 * 64, 16 * 64,
		},
		[16]maskRounds{
			{0xffff, 1}, {0xfffe, 1}, {0xfffc, 1}, {0xfff8, 1}, {0xfff0, 1},
			{0xffe0, 1}, {0xffc0, 1}, {0xff80, 1},
			{0xff00, 1}, {0xfe00, 1}, {0xfc00, 1}, {0xf800, 1}, {0xf000, 1},
			{0xe000, 1},
			{0xc000, 1}, {0x8000, 1},
		},
	},
	{
		[16]int{
			2 * 64, 1 * 64, 3 * 64, 4 * 64, 5 * 64, 6 * 64, 7 * 64, 8 * 64,
			9 * 64, 10 * 64,
			11 * 64, 12 * 64, 13 * 64, 14 * 64, 15 * 64, 16 * 64,
		},
		[16]maskRounds{
			{0xffff, 1}, {0xfffd, 1}, {0xfffc, 1}, {0xfff8, 1}, {0xfff0, 1},
			{0xffe0, 1}, {0xffc0, 1}, {0xff80, 1},
			{0xff00, 1}, {0xfe00, 1}, {0xfc00, 1}, {0xf800, 1}, {0xf000, 1},
			{0xe000, 1},
			{0xc000, 1}, {0x8000, 1},
		},
	},
	{
		[16]int{
			10 * 64, 20 * 64, 30 * 64, 40 * 64, 50 * 64, 60 * 64, 70 * 64,
			80 * 64, 90 * 64,
			100 * 64, 110 * 64, 120 * 64, 130 * 64, 140 * 64, 150 * 64,
			160 * 64,
		},
		[16]maskRounds{
			{0xffff, 10}, {0xfffe, 10}, {0xfffc, 10}, {0xfff8, 10},
			{0xfff0, 10},
			{0xffe0, 10}, {0xffc0, 10}, {0xff80, 10},
			{0xff00, 10}, {0xfe00, 10}, {0xfc00, 10}, {0xf800, 10},
			{0xf000, 10}, {0xe000, 10},
			{0xc000, 10}, {0x8000, 10},
		},
	},
	{
		[16]int{
			10 * 64, 19 * 64, 27 * 64, 34 * 64, 40 * 64, 45 * 64, 49 * 64,
			52 * 64, 54 * 64,
			55 * 64, 57 * 64, 60 * 64, 64 * 64, 69 * 64, 75 * 64, 82 * 64,
		},
		[16]maskRounds{
			{0xffff, 10}, {0xfffe, 9}, {0xfffc, 8}, {0xfff8, 7}, {0xfff0, 6},
			{0xffe0, 5}, {0xffc0, 4}, {0xff80, 3},
			{0xff00, 2}, {0xfe00, 1}, {0xfc00, 2}, {0xf800, 3}, {0xf000, 4},
			{0xe000, 5},
			{0xc000, 6}, {0x8000, 7},
		},
	},
}

func TestMaskGen(t *testing.T) {
	input := [16][]byte{}
	for gcase, g := range goldenMask {
		for i, l := range g.in {
			buf := make([]byte, l)
			input[i] = buf[:]
		}

		mr := genMask(input)

		if !reflect.DeepEqual(mr, g.out) {
			t.Fatalf(
				"case %d: got %04x\n                    want %04x", gcase, mr,
				g.out,
			)
		}
	}
}
