// Copyright IBM Corp. 2020, 2026
// SPDX-License-Identifier: BUSL-1.1

package ldap

import (
	"context"
	"crypto/ed25519"
	"crypto/rand"
	"crypto/x509"
	"fmt"
	"testing"

	"github.com/hashicorp/boundary/internal/auth/ldap/store"
	"github.com/hashicorp/boundary/internal/db"
	"github.com/hashicorp/boundary/internal/errors"
	"github.com/hashicorp/boundary/internal/kms"
	wrapping "github.com/hashicorp/go-kms-wrapping/v2"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"google.golang.org/protobuf/proto"
)

func TestNewClientCertificate(t *testing.T) {
	t.Parallel()
	testCtx := context.Background()
	_, testCertEncoded := TestGenerateCA(t, "localhost")
	_, testPrivKey, err := ed25519.GenerateKey(rand.Reader)
	require.NoError(t, err)
	derPrivKey, err := x509.MarshalPKCS8PrivateKey(testPrivKey)
	require.NoError(t, err)

	tests := []struct {
		name            string
		ctx             context.Context
		authMethodId    string
		certKey         []byte
		cert            string
		want            *ClientCertificate
		wantErr         bool
		wantErrCode     errors.Code
		wantErrContains string
	}{
		{
			name:         "valid",
			ctx:          testCtx,
			authMethodId: "test-id",
			cert:         testCertEncoded,
			certKey:      derPrivKey,
			want: &ClientCertificate{
				ClientCertificate: &store.ClientCertificate{
					LdapMethodId:   "test-id",
					Certificate:    []byte(testCertEncoded),
					CertificateKey: derPrivKey,
				},
			},
		},
		{
			name:            "missing-auth-method-id",
			ctx:             testCtx,
			cert:            testCertEncoded,
			certKey:         derPrivKey,
			wantErr:         true,
			wantErrCode:     errors.InvalidParameter,
			wantErrContains: "missing auth method id",
		},
		{
			name:            "missing-cert",
			ctx:             testCtx,
			authMethodId:    "test-id",
			certKey:         derPrivKey,
			wantErr:         true,
			wantErrCode:     errors.InvalidParameter,
			wantErrContains: "missing certificate",
		},
		{
			name:            "missing-key",
			ctx:             testCtx,
			authMethodId:    "test-id",
			cert:            testCertEncoded,
			wantErr:         true,
			wantErrCode:     errors.InvalidParameter,
			wantErrContains: "missing key",
		},
		{
			name:            "invalid-key",
			ctx:             testCtx,
			authMethodId:    "test-id",
			cert:            testCertEncoded,
			certKey:         []byte("invalid-key"),
			wantErr:         true,
			wantErrCode:     errors.InvalidParameter,
			wantErrContains: "failed to parse key in PKCS #8, ASN.1 DER form",
		},
		{
			name:            "invalid-block",
			ctx:             testCtx,
			authMethodId:    "test-id",
			cert:            TestInvalidPem,
			certKey:         derPrivKey,
			wantErr:         true,
			wantErrCode:     errors.InvalidParameter,
			wantErrContains: "failed to parse certificate: invalid block",
		},
		{
			name:            "invalid-pem",
			ctx:             testCtx,
			authMethodId:    "test-id",
			cert:            "not-encoded",
			certKey:         derPrivKey,
			wantErr:         true,
			wantErrCode:     errors.InvalidParameter,
			wantErrContains: "failed to parse certificate: invalid PEM encoding",
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			assert, require := assert.New(t), require.New(t)
			got, err := NewClientCertificate(tc.ctx, tc.authMethodId, tc.certKey, tc.cert)
			if tc.wantErr {
				require.Error(err)
				assert.Nil(got)
				if tc.wantErrCode != errors.Unknown {
					assert.True(errors.Match(errors.T(tc.wantErrCode), err))
				}
				if tc.wantErrContains != "" {
					assert.Contains(err.Error(), tc.wantErrContains)
				}
				return
			}
			require.NoError(err)
			assert.Equal(tc.want, got)
		})
	}
}

func TestClientCertificate_SetTableName(t *testing.T) {
	t.Parallel()
	defaultTableName := clientCertificateTableName
	tests := []struct {
		name      string
		setNameTo string
		want      string
	}{
		{
			name:      "new-name",
			setNameTo: "new-name",
			want:      "new-name",
		},
		{
			name:      "reset to default",
			setNameTo: "",
			want:      defaultTableName,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert, require := assert.New(t), require.New(t)
			def := allocClientCertificate()
			require.Equal(defaultTableName, def.TableName())
			m := allocClientCertificate()
			m.SetTableName(tt.setNameTo)
			assert.Equal(tt.want, m.TableName())
		})
	}
}

func TestClientCertificate_clone(t *testing.T) {
	t.Parallel()
	testCtx := context.Background()
	t.Run("valid", func(t *testing.T) {
		assert, require := assert.New(t), require.New(t)
		_, pem := TestGenerateCA(t, "localhost")
		_, privKey, err := ed25519.GenerateKey(rand.Reader)
		require.NoError(err)
		derEncodedKey, err := x509.MarshalPKCS8PrivateKey(privKey)
		require.NoError(err)

		cc, err := NewClientCertificate(testCtx, "test-id", derEncodedKey, pem)
		require.NoError(err)
		cp := cc.clone()
		assert.True(proto.Equal(cp.ClientCertificate, cc.ClientCertificate))
	})
	t.Run("not-equal", func(t *testing.T) {
		assert, require := assert.New(t), require.New(t)
		_, pem := TestGenerateCA(t, "localhost")
		_, privKey, err := ed25519.GenerateKey(rand.Reader)
		require.NoError(err)
		derEncodedKey, err := x509.MarshalPKCS8PrivateKey(privKey)
		require.NoError(err)
		cc, err := NewClientCertificate(testCtx, "test-id", derEncodedKey, pem)
		require.NoError(err)

		_, pem2 := TestGenerateCA(t, "localhost")
		_, privKey2, err := ed25519.GenerateKey(rand.Reader)
		require.NoError(err)
		derEncodedKey2, err := x509.MarshalPKCS8PrivateKey(privKey2)
		require.NoError(err)
		cc2, err := NewClientCertificate(testCtx, "test-id", derEncodedKey2, pem2)
		require.NoError(err)

		cp := cc.clone()
		assert.True(!proto.Equal(cp.ClientCertificate, cc2.ClientCertificate))
	})
}

func TestClientCertificate_encrypt_decrypt(t *testing.T) {
	t.Parallel()
	testCtx := context.Background()
	testWrapper := db.TestWrapper(t)
	_, testCertEncoded := TestGenerateCA(t, "localhost")
	_, testPrivKey, err := ed25519.GenerateKey(rand.Reader)
	require.NoError(t, err)
	derPrivKey, err := x509.MarshalPKCS8PrivateKey(testPrivKey)
	require.NoError(t, err)

	tests := []struct {
		name            string
		ctx             context.Context
		cipher          wrapping.Wrapper
		cc              *ClientCertificate
		wantErr         bool
		wantErrCode     errors.Code
		wantErrContains string
	}{
		{
			name:   "valid",
			ctx:    testCtx,
			cipher: testWrapper,
			cc: func() *ClientCertificate {
				testClientCert, err := NewClientCertificate(testCtx, "test-auth-method-id", derPrivKey, testCertEncoded)
				require.NoError(t, err)
				return testClientCert
			}(),
		},
		{
			name: "missing-cipher",
			ctx:  testCtx,
			cc: func() *ClientCertificate {
				testClientCert, err := NewClientCertificate(testCtx, "test-auth-method-id", derPrivKey, testCertEncoded)
				require.NoError(t, err)
				return testClientCert
			}(),
			wantErr:         true,
			wantErrCode:     errors.InvalidParameter,
			wantErrContains: "missing cipher",
		},
		{
			name: "encrypt-err",
			ctx:  testCtx,
			cipher: &kms.MockWrapper{
				Wrapper:    testWrapper,
				EncryptErr: fmt.Errorf("test encrypt error"),
			},
			cc: func() *ClientCertificate {
				testClientCert, err := NewClientCertificate(testCtx, "test-auth-method-id", derPrivKey, testCertEncoded)
				require.NoError(t, err)
				return testClientCert
			}(),
			wantErr:         true,
			wantErrCode:     errors.Encrypt,
			wantErrContains: "test encrypt error",
		},
		{
			name: "keyId-err",
			ctx:  testCtx,
			cipher: &kms.MockWrapper{
				Wrapper:  testWrapper,
				KeyIdErr: fmt.Errorf("test key id error"),
			},
			cc: func() *ClientCertificate {
				testClientCert, err := NewClientCertificate(testCtx, "test-auth-method-id", derPrivKey, testCertEncoded)
				require.NoError(t, err)
				return testClientCert
			}(),
			wantErr:         true,
			wantErrCode:     errors.Encrypt,
			wantErrContains: "test key id error",
		},
		{
			name: "keyBytes-err",
			ctx:  testCtx,
			cipher: &kms.MockWrapper{
				Wrapper: testWrapper,
				// KeyBytesErr: fmt.Errorf("test key bytes error"),
			},
			cc: func() *ClientCertificate {
				testClientCert, err := NewClientCertificate(testCtx, "test-auth-method-id", derPrivKey, testCertEncoded)
				require.NoError(t, err)
				return testClientCert
			}(),
			wantErr:         true,
			wantErrCode:     errors.Encrypt,
			wantErrContains: "failed to hmac client certificate",
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			assert, require := assert.New(t), require.New(t)
			require.Empty(tc.cc.CtCertificateKey)
			require.Empty(tc.cc.CertificateKeyHmac)

			err := tc.cc.encrypt(tc.ctx, tc.cipher)
			if tc.wantErr {
				require.Error(err)
				if tc.wantErrCode != errors.Unknown {
					assert.True(errors.Match(errors.T(tc.wantErrCode), err))
				}
				if tc.wantErrContains != "" {
					assert.Contains(err.Error(), tc.wantErrContains)
				}
				return
			}
			require.NoError(err)
			assert.NotEmpty(tc.cc.GetCertificateKeyHmac())
			assert.NotEmpty(tc.cc.GetCtCertificateKey())

			origKey := make([]byte, len(tc.cc.CertificateKey))
			copy(origKey, tc.cc.CertificateKey)
			tc.cc.CertificateKey = nil
			require.NoError(tc.cc.decrypt(tc.ctx, tc.cipher))
			assert.NotEmpty(tc.cc.CertificateKey)
			assert.Equal(origKey, tc.cc.CertificateKey)
		})
	}
	t.Run("decrypt-missing-cipher", func(t *testing.T) {
		testBindCred, err := NewClientCertificate(testCtx, "test-id", derPrivKey, testCertEncoded)
		require.NoError(t, err)
		err = testBindCred.decrypt(testCtx, nil)
		require.Error(t, err)
		assert.Contains(t, err.Error(), "missing cipher")
	})
	t.Run("decrypt-err", func(t *testing.T) {
		w := &kms.MockWrapper{
			Wrapper:    testWrapper,
			DecryptErr: fmt.Errorf("test decrypt error"),
		}
		testBindCred, err := NewClientCertificate(testCtx, "test-id", derPrivKey, testCertEncoded)
		require.NoError(t, err)
		require.NoError(t, testBindCred.encrypt(testCtx, testWrapper))
		err = testBindCred.decrypt(testCtx, w)
		require.Error(t, err)
		assert.Contains(t, err.Error(), "test decrypt error")
	})
}
