// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package keying

import (
	"crypto/cipher"
	"crypto/hkdf"
	"encoding/base64"
	"math"
	"sync"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/crypto/chacha20poly1305"
)

func TestKeying(t *testing.T) {
	t.Run("Initialization", func(t *testing.T) {
		Init([]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07})
	})
	t.Run("Double initialization", func(t *testing.T) {
		t.Run("Same key allowed", func(t *testing.T) {
			Init([]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07})
		})
		t.Run("Different key panics", func(t *testing.T) {
			assert.Panics(t, func() {
				Init([]byte{0x02, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07})
			})
		})
	})

	t.Run("Context separation", func(t *testing.T) {
		key1 := deriveKey("TESTING")
		key2 := deriveKey("TESTING2")

		ciphertext := key1.Encrypt([]byte("This is for context TESTING"), nil)

		plaintext, err := key2.Decrypt(ciphertext, nil)
		require.Error(t, err)
		assert.Empty(t, plaintext)

		plaintext, err = key1.Decrypt(ciphertext, nil)
		require.NoError(t, err)
		assert.EqualValues(t, "This is for context TESTING", plaintext)
	})

	key := deriveKey("TESTING PURPOSES")
	plainText := []byte("Forgejo is run by [Redacted]")
	var cipherText []byte
	t.Run("Encrypt", func(t *testing.T) {
		cipherText = key.Encrypt(plainText, []byte{0x05, 0x06})
		cipherText2 := key.Encrypt(plainText, []byte{0x05, 0x06})

		// Ensure ciphertexts don't have an deterministic output.
		assert.NotEqual(t, cipherText, cipherText2)
	})

	t.Run("Decrypt", func(t *testing.T) {
		t.Run("Successful", func(t *testing.T) {
			convertedPlainText, err := key.Decrypt(cipherText, []byte{0x05, 0x06})
			require.NoError(t, err)
			assert.Equal(t, plainText, convertedPlainText)
		})

		t.Run("Old secret", func(t *testing.T) {
			// ensure that new code can still decode old secrets
			known, err := base64.RawStdEncoding.DecodeString("LABcdFTke+FAESOAUkaQvdFO/tLFdugvXHqUYQaESy9eCedUsorjpe1N350NN+AU7gv6xyK3DHuugD+wjnVcNvt+9hA")
			require.NoError(t, err)
			convertedPlainText, err := key.Decrypt(known, []byte{0x05, 0x06})
			require.NoError(t, err)
			assert.Equal(t, plainText, convertedPlainText)
		})

		t.Run("Not enough additional data", func(t *testing.T) {
			plainText, err := key.Decrypt(cipherText, []byte{0x05})
			require.Error(t, err)
			assert.Empty(t, plainText)
		})

		t.Run("Too much additional data", func(t *testing.T) {
			plainText, err := key.Decrypt(cipherText, []byte{0x05, 0x06, 0x07})
			require.Error(t, err)
			assert.Empty(t, plainText)
		})

		t.Run("Incorrect nonce", func(t *testing.T) {
			// Flip the first byte of the nonce.
			cipherText[0] = ^cipherText[0]

			plainText, err := key.Decrypt(cipherText, []byte{0x05, 0x06})
			require.Error(t, err)
			assert.Empty(t, plainText)
		})

		t.Run("Incorrect ciphertext", func(t *testing.T) {
			_, err := key.Decrypt(nil, nil)
			require.Error(t, err)

			cipherText := make([]byte, chacha20poly1305.NonceSizeX)
			_, err = key.Decrypt(cipherText, nil)
			require.Error(t, err)
		})
	})
}

func TestKeyingColumnAndID(t *testing.T) {
	assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ColumnAndID("table", math.MinInt64))
	assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, ColumnAndID("table", -1))
	assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ColumnAndID("table", 0))
	assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, ColumnAndID("table", 1))
	assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, ColumnAndID("table", math.MaxInt64))

	assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ColumnAndID("table2", math.MinInt64))
	assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, ColumnAndID("table2", -1))
	assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, ColumnAndID("table2", 0))
	assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, ColumnAndID("table2", 1))
	assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, ColumnAndID("table2", math.MaxInt64))
}

func TestColumnAndJSONSelectorAndID(t *testing.T) {
	assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, ColumnAndJSONSelectorAndID("table", "field1", math.MinInt64))
	assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, ColumnAndJSONSelectorAndID("table", "field1", -1))
	assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, ColumnAndJSONSelectorAndID("table", "field1", 0))
	assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}, ColumnAndJSONSelectorAndID("table", "field1", 1))
	assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, ColumnAndJSONSelectorAndID("table", "field1", math.MaxInt64))

	assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x3a, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, ColumnAndJSONSelectorAndID("table", "field2", math.MinInt64))
	assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x3a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, ColumnAndJSONSelectorAndID("table", "field2", -1))
	assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x3a, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, ColumnAndJSONSelectorAndID("table", "field2", 0))
	assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x3a, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}, ColumnAndJSONSelectorAndID("table", "field2", 1))
	assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x32, 0x3a, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, ColumnAndJSONSelectorAndID("table", "field2", math.MaxInt64))

	assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, ColumnAndJSONSelectorAndID("table2", "field1", math.MinInt64))
	assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, ColumnAndJSONSelectorAndID("table2", "field1", -1))
	assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, ColumnAndJSONSelectorAndID("table2", "field1", 0))
	assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}, ColumnAndJSONSelectorAndID("table2", "field1", 1))
	assert.Equal(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x31, 0x3a, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, ColumnAndJSONSelectorAndID("table2", "field1", math.MaxInt64))
}

// 500 ns/op
func BenchmarkExpandPRK(b *testing.B) {
	prk, err := hkdf.Extract(hash, []byte("secret"), nil)
	require.NoError(b, err)
	for b.Loop() {
		expandPRK(prk, "testing")
	}
}

// 2 ns/op
func BenchmarkExpandPRKOnce(b *testing.B) {
	prk, err := hkdf.Extract(hash, []byte("secret"), nil)
	require.NoError(b, err)
	once := sync.OnceValue(func() cipher.AEAD {
		return expandPRK(prk, "testing")
	})
	for b.Loop() {
		once()
	}
}
