package provider

import (
	"bytes"
	"encoding/hex"
	"io"
	"testing"

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

func TestGcmEncryption(t *testing.T) {
	t.Parallel()

	t.Run("encrypts correctly", func(t *testing.T) {
		t.Parallel()

		// The expected values are generated by test_fixtures/aesgcm_encrypt_correct_output.rb

		salt := []byte("abcdefgh")
		nonce := []byte("123456789012")

		cipher := NewAesGcmCipher()
		cipher.randReader = bytes.NewReader(append(salt, nonce...))

		payload := []byte("grafana unit test")
		secret := "secret here"

		encrypted, err := cipher.Encrypt(t.Context(), payload, secret)
		require.NoError(t, err, "failed to encrypt with GCM")
		require.NotEmpty(t, encrypted, "encrypted payload should not be empty")
		require.Equal(t, "61626364656667683132333435363738393031328123655291d1f5eebe34c54ba55900f68a2700818a8fda9e2921190b67271d97ce",
			hex.EncodeToString(encrypted), "encrypted payload should match expected value")

		// Sanity check that all our pre-provided random data is used.
		_, err = cipher.randReader.Read([]byte{0})
		require.ErrorIs(t, err, io.EOF, "expected us to have read the entire random source")
	})

	t.Run("fails if random source is empty", func(t *testing.T) {
		t.Parallel()

		cipher := NewAesGcmCipher()
		cipher.randReader = bytes.NewReader([]byte{})

		payload := []byte("grafana unit test")
		secret := "secret here"

		_, err := cipher.Encrypt(t.Context(), payload, secret)
		require.Error(t, err, "expected error when random source is empty")
	})

	t.Run("fails if random source does not provide nonce", func(t *testing.T) {
		t.Parallel()

		// Scenario: the random source has enough entropy for the salt, but not for the nonce.
		// In this case, we should fail with an error.

		cipher := NewAesGcmCipher()
		cipher.randReader = bytes.NewReader([]byte("abcdefgh")) // 8 bytes for salt, but not enough for nonce

		payload := []byte("grafana unit test")
		secret := "secret here"

		_, err := cipher.Encrypt(t.Context(), payload, secret)
		require.Error(t, err, "expected error when random source does not provide nonce")
	})
}

func TestGcmDecryption(t *testing.T) {
	t.Parallel()

	t.Run("decrypts correctly", func(t *testing.T) {
		t.Parallel()

		// The expected values are generated by test_fixtures/aesgcm_encrypt_correct_output.rb

		cipher := NewAesGcmCipher()
		cipher.randReader = bytes.NewReader([]byte{}) // should not be used

		payload, err := hex.DecodeString("61626364656667683132333435363738393031328123655291d1f5eebe34c54ba55900f68a2700818a8fda9e2921190b67271d97ce")
		require.NoError(t, err, "failed to decode pre-computed encrypted payload")
		secret := "secret here"

		decrypted, err := cipher.Decrypt(t.Context(), payload, secret)
		require.NoError(t, err, "failed to decrypt with GCM")
		require.Equal(t, "grafana unit test", string(decrypted), "decrypted payload should match expected value")
	})

	t.Run("fails if payload is shorter than salt", func(t *testing.T) {
		t.Parallel()

		cipher := NewAesGcmCipher()
		cipher.randReader = bytes.NewReader([]byte{}) // should not be used

		payload := []byte{1, 2, 3, 4}
		secret := "secret here"

		_, err := cipher.Decrypt(t.Context(), payload, secret)
		require.Error(t, err, "expected error when payload is shorter than salt")
	})

	t.Run("fails if payload has length of salt but no nonce", func(t *testing.T) {
		t.Parallel()

		cipher := NewAesGcmCipher()
		cipher.randReader = bytes.NewReader([]byte{}) // should not be used

		payload := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} // salt and a little more
		secret := "secret here"

		_, err := cipher.Decrypt(t.Context(), payload, secret)
		require.Error(t, err, "expected error when payload has length of salt but no nonce")
	})

	t.Run("fails when authentication tag is wrong", func(t *testing.T) {
		t.Parallel()

		cipher := NewAesGcmCipher()
		cipher.randReader = bytes.NewReader([]byte{}) // should not be used

		// Removed 2 bytes from the end of the payload to simulate a wrong authentication tag.
		payload, err := hex.DecodeString("61626364656667683132333435363738393031328123655291d1f5eebe34c54ba55900f68a2700818a8fda9e2921190b67271d")
		require.NoError(t, err, "failed to decode pre-computed encrypted payload")
		secret := "secret here"

		_, err = cipher.Decrypt(t.Context(), payload, secret)
		require.Error(t, err, "expected to fail validation")
	})

	t.Run("fails if secret does not match", func(t *testing.T) {
		t.Parallel()

		cipher := NewAesGcmCipher()
		cipher.randReader = bytes.NewReader([]byte{}) // should not be used

		payload, err := hex.DecodeString("61626364656667683132333435363738393031328123655291d1f5eebe34c54ba55900f68a2700818a8fda9e2921190b67271d97ce")
		require.NoError(t, err, "failed to decode pre-computed encrypted payload")
		secret := "should have been 'secret here'"

		_, err = cipher.Decrypt(t.Context(), payload, secret)
		require.Error(t, err, "expected to fail decryption")
	})
}
