package handshake

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"crypto/tls"
	"fmt"
	"testing"

	"github.com/quic-go/quic-go/internal/protocol"

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

func getSealerAndOpener(t *testing.T, cs *cipherSuite, v protocol.Version) (LongHeaderSealer, LongHeaderOpener) {
	t.Helper()
	key := make([]byte, 16)
	hpKey := make([]byte, 16)
	rand.Read(key)
	rand.Read(hpKey)
	block, err := aes.NewCipher(key)
	require.NoError(t, err)
	aead, err := cipher.NewGCM(block)
	require.NoError(t, err)

	return newLongHeaderSealer(&xorNonceAEAD{aead: aead}, newHeaderProtector(cs, hpKey, true, v)),
		newLongHeaderOpener(&xorNonceAEAD{aead: aead}, newHeaderProtector(cs, hpKey, true, v))
}

func TestEncryptAndDecryptMessage(t *testing.T) {
	for _, v := range []protocol.Version{protocol.Version1, protocol.Version2} {
		for _, cs := range cipherSuites {
			t.Run(fmt.Sprintf("QUIC %s/%s", v, tls.CipherSuiteName(cs.ID)), func(t *testing.T) {
				sealer, opener := getSealerAndOpener(t, cs, v)
				msg := []byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")
				ad := []byte("Donec in velit neque.")

				encrypted := sealer.Seal(nil, msg, 0x1337, ad)

				opened, err := opener.Open(nil, encrypted, 0x1337, ad)
				require.NoError(t, err)
				require.Equal(t, msg, opened)

				// incorrect associated data
				_, err = opener.Open(nil, encrypted, 0x1337, []byte("wrong ad"))
				require.Equal(t, ErrDecryptionFailed, err)

				// incorrect packet number
				_, err = opener.Open(nil, encrypted, 0x42, ad)
				require.Equal(t, ErrDecryptionFailed, err)
			})
		}
	}
}

func TestDecodePacketNumber(t *testing.T) {
	msg := []byte("Lorem ipsum dolor sit amet")
	ad := []byte("Donec in velit neque.")

	sealer, opener := getSealerAndOpener(t, getCipherSuite(tls.TLS_AES_128_GCM_SHA256), protocol.Version1)
	encrypted := sealer.Seal(nil, msg, 0x1337, ad)

	// can't decode the packet number if encryption failed
	_, err := opener.Open(nil, encrypted[:len(encrypted)-1], 0x1337, ad)
	require.Error(t, err)
	require.Equal(t, protocol.PacketNumber(0x38), opener.DecodePacketNumber(0x38, protocol.PacketNumberLen1))

	_, err = opener.Open(nil, encrypted, 0x1337, ad)
	require.NoError(t, err)
	require.Equal(t, protocol.PacketNumber(0x1338), opener.DecodePacketNumber(0x38, protocol.PacketNumberLen1))
}

func TestEncryptAndDecryptHeader(t *testing.T) {
	for _, v := range []protocol.Version{protocol.Version1, protocol.Version2} {
		t.Run("QUIC "+v.String(), func(t *testing.T) {
			for _, cs := range cipherSuites {
				t.Run(tls.CipherSuiteName(cs.ID), func(t *testing.T) {
					testEncryptAndDecryptHeader(t, cs, v)
				})
			}
		})
	}
}

func testEncryptAndDecryptHeader(t *testing.T, cs *cipherSuite, v protocol.Version) {
	sealer, opener := getSealerAndOpener(t, cs, v)
	var lastFourBitsDifferent int

	for i := 0; i < 100; i++ {
		sample := make([]byte, 16)
		rand.Read(sample)
		header := []byte{0xb5, 1, 2, 3, 4, 5, 6, 7, 8, 0xde, 0xad, 0xbe, 0xef}
		sealer.EncryptHeader(sample, &header[0], header[9:13])
		if header[0]&0xf != 0xb5&0xf {
			lastFourBitsDifferent++
		}
		require.Equal(t, byte(0xb5&0xf0), header[0]&0xf0)
		require.Equal(t, []byte{1, 2, 3, 4, 5, 6, 7, 8}, header[1:9])
		require.NotEqual(t, []byte{0xde, 0xad, 0xbe, 0xef}, header[9:13])
		opener.DecryptHeader(sample, &header[0], header[9:13])
		require.Equal(t, []byte{0xb5, 1, 2, 3, 4, 5, 6, 7, 8, 0xde, 0xad, 0xbe, 0xef}, header)
	}
	require.Greater(t, lastFourBitsDifferent, 75)

	// decryption failure with different sample
	header := []byte{0xb5, 1, 2, 3, 4, 5, 6, 7, 8, 0xde, 0xad, 0xbe, 0xef}
	sample := make([]byte, 16)
	rand.Read(sample)
	sealer.EncryptHeader(sample, &header[0], header[9:13])
	rand.Read(sample) // use a different sample
	opener.DecryptHeader(sample, &header[0], header[9:13])
	require.NotEqual(t, []byte{0xb5, 1, 2, 3, 4, 5, 6, 7, 8, 0xde, 0xad, 0xbe, 0xef}, header)
}
