package blockmode_test

import (
	"bytes"
	"encoding/hex"
	"fmt"
	"testing"
	"time"

	"xdx.jelly/xgcl/grand"
	"xdx.jelly/xgcl/sm/sm4"
	"xdx.jelly/xgcl/utils/blockmode"
)

// sm4_GCM模式
var sm4GCMTests = []struct {
	key, nonce, plaintext, ad, result string
}{
	{
		"11754cd72aec309bf52f7687212e8957",
		"3c819d9a9bed087615030b65", // nonce should be 12 bytes.
		"plaintext",
		"additional message not need encrypt, empty is ok",
		"6111f78f2f82b913c20e333160bfec034c3720ac133a6203b1",
	},
}

func TestSM4Speed(t *testing.T) {
	key, err := hex.DecodeString("12345678123456781234567812345678")
	if err != nil {
		t.Fatal(err)
	}
	block, err := sm4.NewCipher(key)
	if err != nil {
		t.Fatal(err)
	}
	gcm, err := blockmode.NewGCM(blockmode.Wrap(block))
	if err != nil {
		t.Fatal(err)
	}
	nonce, err := hex.DecodeString("123456781234567812345678")
	if err != nil {
		t.Fatal(err)
	}

	plaintext := grand.GetRandom(1024 * 1024)
	ad := []byte("additional message not need encrypt, empty is ok")
	ct2 := make([]byte, 0, 1024*1024+gcm.Overhead())
	cnt := 100
	start := time.Now()
	for i := 0; i < cnt; i++ {
		err = gcm.EncryptInit(nonce)
		if err != nil {
			t.Fatal(err)
		}
		gcm.SpecifyADD(ad)
		ct2, err := gcm.EncryptUpdate(ct2, plaintext)
		if err != nil {
			t.Fatal(err)
		}

		ct2, err = gcm.EncryptFinal(ct2)
		if err != nil {
			t.Fatal(err)
		}
	}
	end := time.Now()
	elapsed := end.Sub(start)
	fmt.Printf("%f Bps\n", float64(len(plaintext)*cnt)/1024/1024*1000/float64(elapsed.Milliseconds()))

}

func TestSM4GCM(t *testing.T) {
	for i, test := range sm4GCMTests {
		key, _ := hex.DecodeString(test.key)

		block, _ := sm4.NewCipher(key)
		gcm, err := blockmode.NewGCM(blockmode.Wrap(block))
		if err != nil {
			t.Fatal(err)
		}

		nonce, _ := hex.DecodeString(test.nonce)
		plaintext := []byte(test.plaintext)

		ad := []byte(test.ad)

		// 提前分配好空间
		for i := 0; i < gcm.Overhead(); i++ {
			plaintext = append(plaintext, 0)
		}
		plaintext = plaintext[:len(plaintext)-gcm.Overhead()]

		ct := gcm.Seal(plaintext[:0], nonce, plaintext, ad)

		if ctHex := hex.EncodeToString(ct); ctHex != test.result {
			t.Errorf("#%d: got %s, want %s", i, ctHex, test.result)
			continue
		}

		plaintext, err = gcm.Open(ct[:0], nonce, ct, ad)
		if err != nil {
			t.Errorf("#%d: Open failed", i)
			continue
		}

		if !bytes.Equal(plaintext, []byte(test.plaintext)) {
			t.Errorf("#%d: plaintext's don't match: got %x vs %x", i, test.plaintext, plaintext)
			continue
		}

		// if ad, nonce, ct was changed, return err
		if len(ad) > 0 {
			ad[0] ^= 0x80
			if _, err := gcm.Open(nil, nonce, ct, ad); err == nil {
				t.Errorf("#%d: Open was successful after altering additional data", i)
			}
			ad[0] ^= 0x80
		}

		nonce[0] ^= 0x80
		if _, err := gcm.Open(nil, nonce, ct, ad); err == nil {
			t.Errorf("#%d: Open was successful after altering nonce", i)
		}
		nonce[0] ^= 0x80

		ct[0] ^= 0x80
		if _, err := gcm.Open(nil, nonce, ct, ad); err == nil {
			t.Errorf("#%d: Open was successful after altering ciphertext", i)
		}
		ct[0] ^= 0x80
	}
}

func TestGcmUpdate(t *testing.T) {
	test := sm4GCMTests[0]
	key, _ := hex.DecodeString(test.key)

	block, _ := sm4.NewCipher(key)
	gcm, err := blockmode.NewGCM(blockmode.Wrap(block))
	if err != nil {
		t.Fatal(err)
	}

	nonce, _ := hex.DecodeString(test.nonce)
	plaintext := grand.GetRandom(0)
	ad := []byte(test.ad)
	ct1 := gcm.Seal(nil, nonce, plaintext, ad)

	ct2 := make([]byte, 0, 116)
	err = gcm.EncryptInit(nonce)
	if err != nil {
		t.Fatal(err)
	}
	gcm.SpecifyADD(ad)
	for i := range plaintext {
		ct2, err = gcm.EncryptUpdate(ct2, []byte{plaintext[i]})
		if err != nil {
			t.Fatal(err)
		}
	}
	ct2, err = gcm.EncryptFinal(ct2)
	if err != nil {
		t.Fatal(err)
	}
	if !bytes.Equal(ct1, ct2) {
		t.Errorf("Seal failed")
	}

	if err = gcm.DecryptInit(nonce); err != nil {
		t.Fatal(err)
	}
	gcm.SpecifyADD(ad)

	decrypted := make([]byte, 0, len(plaintext))
	for i := range ct1 {
		decrypted, err = gcm.DecryptUpdate(decrypted, []byte{ct1[i]})
		if err != nil {
			t.Fatal(err)
		}
	}
	decrypted, err = gcm.DecryptFinal(decrypted)
	if !bytes.Equal(decrypted, plaintext) {
		t.Fatal("plaintext unequal decrypted plaintext")
	}
	if err != nil {
		t.Fatal("auth failed")
	}
}

func FuzzSm4Gcm(f *testing.F) {
	nonce := grand.GetRandom(12)
	key := grand.GetRandom(16)

	block, _ := sm4.NewCipher(key)
	gcm, err := blockmode.NewGCM(blockmode.Wrap(block))
	if err != nil {
		f.Fatal(err)
	}
	stdgcm, _ := sm4.NewGCM(key)

	f.Add([]byte{}, []byte{})
	f.Fuzz(func(t *testing.T, plaintext, ad []byte) {
		stdct := stdgcm.Seal(nil, nonce, plaintext, ad)
		ct1 := gcm.Seal(nil, nonce, plaintext, ad)
		if !bytes.Equal(ct1, stdct) {
			t.Errorf("Seal failed")
		}

		if err := gcm.EncryptInit(nonce); err != nil {
			t.Fatal(err)
		}
		// if additional data is empty, then call
		// gcm.SpecifyADD(nil)
		gcm.SpecifyADD(ad)

		ct2, err := gcm.EncryptUpdate(nil, plaintext[:len(plaintext)/2])
		if err != nil {
			t.Fatal(err)
		}
		for _, p := range plaintext[len(plaintext)/2:] {
			if ct2, err = gcm.EncryptUpdate(ct2, []byte{p}); err != nil {
				t.Fatal(err)
			}
		}
		ct2, err = gcm.EncryptFinal(ct2)
		if err != nil {
			t.Fatal(err)
		}

		if !bytes.Equal(ct2, stdct) {
			t.Errorf("Encrypt failed")
		}

		pt1, err1 := gcm.Open(nil, nonce, ct1, ad)
		if err1 != nil {
			t.Errorf("Open faile: %v\n", err1)
		}
		if err := gcm.DecryptInit(nonce); err != nil {
			t.Fatal(err)
		}
		gcm.SpecifyADD(ad)

		pt2, err := gcm.DecryptUpdate(nil, ct2[:len(ct2)/2])
		if err != nil {
			t.Fatal(err)
		}
		for _, p := range ct2[len(ct2)/2:] {
			if pt2, err = gcm.DecryptUpdate(pt2, []byte{p}); err != nil {
				t.Fatal(err)
			}
		}
		pt2, err = gcm.DecryptFinal(pt2)
		if err != nil {
			t.Fatal(err)
		}
		if !bytes.Equal(pt2, plaintext) {
			t.Errorf("Decrypt failed")
		}

		if !bytes.Equal(pt1, plaintext) {
			t.Errorf("Open failed")
		}
	})
}
