package state_test

import (
	"encoding/base64"
	"strings"
	"testing"

	"github.com/jkroepke/openvpn-auth-oauth2/internal/state"
	"github.com/jkroepke/openvpn-auth-oauth2/internal/test/testsuite"
	"github.com/stretchr/testify/require"
)

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

	for _, tc := range []struct {
		name         string
		commonName   string
		ipAddr       string
		ipPort       string
		sessionState string
	}{
		{name: "empty session state", commonName: "foobar", ipAddr: "127.0.0.1", ipPort: "12345", sessionState: ""},
		{name: "non-empty session state", commonName: "", ipAddr: "127.0.0.1", ipPort: "12345", sessionState: "Authenticated"},
		{name: "empty ip address and port", commonName: "foobar", ipAddr: "", ipPort: "", sessionState: "Authenticated"},
		{name: "ipv6 address", commonName: "foobar", ipAddr: "2001:db8::1", ipPort: "12345", sessionState: "Authenticated"},
		{name: "with special characters", commonName: "foo bar/baz@qux", ipAddr: "127.0.0.1", ipPort: "12345", sessionState: "AuthenticatedEmptyUser"},
		{name: "with unicode characters", commonName: "foo bar/baz@qux 你好", ipAddr: "127.0.0.1", ipPort: "12345", sessionState: "ExpiredEmptyUser"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			sessionState := state.State{
				Client: state.ClientIdentifier{
					CID:        9223372036854775807,
					KID:        2,
					CommonName: tc.commonName,
				},
				IPAddr:       tc.ipAddr,
				IPPort:       tc.ipPort,
				SessionState: tc.sessionState,
			}

			encryptedToken, err := state.Encrypt(testsuite.Cipher, sessionState)
			require.NoError(t, err)

			token, err := state.Decrypt(testsuite.Cipher, encryptedToken)
			require.NoError(t, err)

			require.Equal(t, token, sessionState)
		})
	}
}

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

	for _, tc := range []struct {
		name         string
		encodedToken func() (state.EncryptedState, error)
		expectedErr  string
	}{
		{
			name: "invalid base64",
			encodedToken: func() (state.EncryptedState, error) {
				return "invalid*", nil
			},
			expectedErr: "illegal base64 data at input",
		},
		{
			name: "invalid ciphertext",
			encodedToken: func() (state.EncryptedState, error) {
				return base64.RawURLEncoding.EncodeToString([]byte("a")), nil
			},
			expectedErr: "ciphertext block size is too short",
		},
		{
			name: "invalid state too long",
			encodedToken: func() (state.EncryptedState, error) {
				return strings.Repeat("a", 10000), nil
			},
			expectedErr: "invalid data: token too large",
		},
		{
			name: "state too short",
			encodedToken: func() (state.EncryptedState, error) {
				encrypted, err := testsuite.Cipher.EncryptBytesWithTime([]byte{2, 0})
				if err != nil {
					return "", err
				}

				return string(encrypted), nil
			},
			expectedErr: "state is too short",
		},
		{
			name: "unsupported state version",
			encodedToken: func() (state.EncryptedState, error) {
				encrypted, err := testsuite.Cipher.EncryptBytesWithTime([]byte{1, 0, '0', 1, 1})
				if err != nil {
					return "", err
				}

				return string(encrypted), nil
			},
			expectedErr: "unsupported state version",
		},
		{
			name: "invalid CID",
			encodedToken: func() (state.EncryptedState, error) {
				encrypted, err := testsuite.Cipher.EncryptBytesWithTime([]byte{2, 0, '0'})
				if err != nil {
					return "", err
				}

				return string(encrypted), nil
			},
			expectedErr: "read CID: invalid uvarint",
		},
		{
			name: "invalid KID",
			encodedToken: func() (state.EncryptedState, error) {
				encrypted, err := testsuite.Cipher.EncryptBytesWithTime([]byte{2, 0, '0', 1})
				if err != nil {
					return "", err
				}

				return string(encrypted), nil
			},
			expectedErr: "read KID: invalid uvarint",
		},
		{
			name: "truncated common name",
			encodedToken: func() (state.EncryptedState, error) {
				encrypted, err := testsuite.Cipher.EncryptBytesWithTime([]byte{2, 2, '0', 1, 1, 5, 'a'})
				if err != nil {
					return "", err
				}

				return string(encrypted), nil
			},
			expectedErr: "read CommonName: string length 5 exceeds remaining data 1",
		},
		{
			name: "trailing data",
			encodedToken: func() (state.EncryptedState, error) {
				encrypted, err := testsuite.Cipher.EncryptBytesWithTime([]byte{2, 0, '0', 1, 1, 'x'})
				if err != nil {
					return "", err
				}

				return string(encrypted), nil
			},
			expectedErr: "unexpected trailing state data",
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			encodedTokenString, err := tc.encodedToken()
			require.NoError(t, err)

			_, err = state.Decrypt(testsuite.Cipher, encodedTokenString)

			require.ErrorContains(t, err, tc.expectedErr)
		})
	}
}
