/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package crypto

import (
	"bytes"
	"encoding/pem"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/hyperledger/fabric-protos-go-apiv2/msp"
	"github.com/hyperledger/fabric/common/crypto/tlsgen"
	"github.com/hyperledger/fabric/protoutil"
	"github.com/stretchr/testify/require"
	"google.golang.org/protobuf/proto"
)

func TestX509CertExpiresAt(t *testing.T) {
	certBytes, err := os.ReadFile(filepath.Join("testdata", "cert.pem"))
	require.NoError(t, err)
	sId := &msp.SerializedIdentity{
		IdBytes: certBytes,
	}
	serializedIdentity, err := proto.Marshal(sId)
	require.NoError(t, err)
	expirationTime := ExpiresAt(serializedIdentity)
	require.Equal(t, time.Date(2027, 8, 17, 12, 19, 48, 0, time.UTC), expirationTime)
}

func TestX509InvalidCertExpiresAt(t *testing.T) {
	certBytes, err := os.ReadFile(filepath.Join("testdata", "badCert.pem"))
	require.NoError(t, err)
	sId := &msp.SerializedIdentity{
		IdBytes: certBytes,
	}
	serializedIdentity, err := proto.Marshal(sId)
	require.NoError(t, err)
	expirationTime := ExpiresAt(serializedIdentity)
	require.True(t, expirationTime.IsZero())
}

func TestIdemixIdentityExpiresAt(t *testing.T) {
	idemixId := &msp.SerializedIdemixIdentity{
		NymX: []byte{1, 2, 3},
		NymY: []byte{1, 2, 3},
		Ou:   []byte("OU1"),
	}
	idemixBytes, err := proto.Marshal(idemixId)
	require.NoError(t, err)
	sId := &msp.SerializedIdentity{
		IdBytes: idemixBytes,
	}
	serializedIdentity, err := proto.Marshal(sId)
	require.NoError(t, err)
	expirationTime := ExpiresAt(serializedIdentity)
	require.True(t, expirationTime.IsZero())
}

func TestInvalidIdentityExpiresAt(t *testing.T) {
	expirationTime := ExpiresAt([]byte{1, 2, 3})
	require.True(t, expirationTime.IsZero())
}

func TestTrackExpiration(t *testing.T) {
	ca, err := tlsgen.NewCA()
	require.NoError(t, err)

	now := time.Now()
	expirationTime := certExpirationTime(ca.CertBytes())

	timeUntilExpiration := expirationTime.Sub(now)
	timeUntilOneMonthBeforeExpiration := timeUntilExpiration - 28*24*time.Hour
	timeUntil2DaysBeforeExpiration := timeUntilExpiration - 2*24*time.Hour - time.Hour*12

	monthBeforeExpiration := now.Add(timeUntilOneMonthBeforeExpiration)
	twoDaysBeforeExpiration := now.Add(timeUntil2DaysBeforeExpiration)

	tlsCert, err := ca.NewServerCertKeyPair("127.0.0.1")
	require.NoError(t, err)

	signingIdentity := protoutil.MarshalOrPanic(&msp.SerializedIdentity{
		IdBytes: tlsCert.Cert,
	})

	warnShouldNotBeInvoked := func(format string, args ...interface{}) {
		t.Fatalf(format, args...)
	}

	var formattedWarning string
	warnShouldBeInvoked := func(format string, args ...interface{}) {
		formattedWarning = fmt.Sprintf(format, args...)
	}

	var formattedInfo string
	infoShouldBeInvoked := func(format string, args ...interface{}) {
		formattedInfo = fmt.Sprintf(format, args...)
	}

	for _, testCase := range []struct {
		description        string
		tls                bool
		serverCert         []byte
		clientCertChain    [][]byte
		sIDBytes           []byte
		info               MessageFunc
		warn               MessageFunc
		now                time.Time
		expectedInfoPrefix string
		expectedWarn       string
	}{
		{
			description: "No TLS, enrollment cert isn't valid logs a warning",
			warn:        warnShouldNotBeInvoked,
			sIDBytes:    []byte{1, 2, 3},
		},
		{
			description:        "No TLS, enrollment cert expires soon",
			sIDBytes:           signingIdentity,
			info:               infoShouldBeInvoked,
			warn:               warnShouldBeInvoked,
			now:                monthBeforeExpiration,
			expectedInfoPrefix: "The enrollment certificate will expire on",
			expectedWarn:       "The enrollment certificate will expire within one week",
		},
		{
			description:        "TLS, server cert expires soon",
			info:               infoShouldBeInvoked,
			warn:               warnShouldBeInvoked,
			now:                monthBeforeExpiration,
			tls:                true,
			serverCert:         tlsCert.Cert,
			expectedInfoPrefix: "The server TLS certificate will expire on",
			expectedWarn:       "The server TLS certificate will expire within one week",
		},
		{
			description:        "TLS, server cert expires really soon",
			info:               infoShouldBeInvoked,
			warn:               warnShouldBeInvoked,
			now:                twoDaysBeforeExpiration,
			tls:                true,
			serverCert:         tlsCert.Cert,
			expectedInfoPrefix: "The server TLS certificate will expire on",
			expectedWarn:       "The server TLS certificate expires within 2 days and 12 hours",
		},
		{
			description:  "TLS, server cert has expired",
			info:         infoShouldBeInvoked,
			warn:         warnShouldBeInvoked,
			now:          expirationTime.Add(time.Hour),
			tls:          true,
			serverCert:   tlsCert.Cert,
			expectedWarn: "The server TLS certificate has expired",
		},
		{
			description:        "TLS, client cert expires soon",
			info:               infoShouldBeInvoked,
			warn:               warnShouldBeInvoked,
			now:                monthBeforeExpiration,
			tls:                true,
			clientCertChain:    [][]byte{tlsCert.Cert},
			expectedInfoPrefix: "The client TLS certificate will expire on",
			expectedWarn:       "The client TLS certificate will expire within one week",
		},
	} {
		t.Run(testCase.description, func(t *testing.T) {
			defer func() {
				formattedWarning = ""
				formattedInfo = ""
			}()

			fakeTimeAfter := func(duration time.Duration, f func()) *time.Timer {
				require.NotEmpty(t, testCase.expectedWarn)
				threeWeeks := 3 * 7 * 24 * time.Hour
				require.Equal(t, threeWeeks, duration)
				f()
				return nil
			}

			TrackExpiration(testCase.tls,
				testCase.serverCert,
				testCase.clientCertChain,
				testCase.sIDBytes,
				testCase.info,
				testCase.warn,
				testCase.now,
				fakeTimeAfter)

			if testCase.expectedInfoPrefix != "" {
				require.True(t, strings.HasPrefix(formattedInfo, testCase.expectedInfoPrefix))
			} else {
				require.Empty(t, formattedInfo)
			}

			if testCase.expectedWarn != "" {
				require.Equal(t, testCase.expectedWarn, formattedWarning)
			} else {
				require.Empty(t, formattedWarning)
			}
		})
	}
}

func TestLogNonPubKeyMismatchErr(t *testing.T) {
	ca, err := tlsgen.NewCA()
	require.NoError(t, err)

	aliceKeyPair, err := ca.NewClientCertKeyPair()
	require.NoError(t, err)

	bobKeyPair, err := ca.NewClientCertKeyPair()
	require.NoError(t, err)

	expected := &bytes.Buffer{}
	expected.WriteString(fmt.Sprintf("Failed determining if public key of %s matches public key of %s: foo",
		string(aliceKeyPair.Cert),
		string(bobKeyPair.Cert)))

	b := &bytes.Buffer{}
	f := func(template string, args ...interface{}) {
		fmt.Fprintf(b, template, args...)
	}

	LogNonPubKeyMismatchErr(f, errors.New("foo"), aliceKeyPair.TLSCert.Raw, bobKeyPair.TLSCert.Raw)

	require.Equal(t, expected.String(), b.String())
}

func TestCertificatesWithSamePublicKey(t *testing.T) {
	ca, err := tlsgen.NewCA()
	require.NoError(t, err)

	bobKeyPair, err := ca.NewClientCertKeyPair()
	require.NoError(t, err)

	bobCert := bobKeyPair.Cert
	bob := pem2der(bobCert)

	aliceCert := `-----BEGIN CERTIFICATE-----
MIIBNjCB3KADAgECAgELMAoGCCqGSM49BAMCMBAxDjAMBgNVBAUTBUFsaWNlMB4X
DTIwMDgxODIxMzU1NFoXDTIwMDgyMDIxMzU1NFowEDEOMAwGA1UEBRMFQWxpY2Uw
WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQjZP5VD/RaczoPFbA4gkt1qb54R6SP
J/V5oxkhDboG9xWi0wpyghaMGwwxC7Q9wegEnyOVp9nXoLrQ8LUJ5BfZoycwJTAO
BgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwCgYIKoZIzj0EAwID
SQAwRgIhAK4le5XgH5edyhaQ9Sz7sFz3Zc4bbhPAzt9zQUYnoqK+AiEA5zcyLB/4
Oqe93lroE6GF9W7UoCZFzD7lXsWku/dgFOU=
-----END CERTIFICATE-----`

	reIssuedAliceCert := `-----BEGIN CERTIFICATE-----
MIIBNDCB3KADAgECAgELMAoGCCqGSM49BAMCMBAxDjAMBgNVBAUTBUFsaWNlMB4X
DTIwMDgxODIxMzY1NFoXDTIwMDgyMDIxMzY1NFowEDEOMAwGA1UEBRMFQWxpY2Uw
WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQjZP5VD/RaczoPFbA4gkt1qb54R6SP
J/V5oxkhDboG9xWi0wpyghaMGwwxC7Q9wegEnyOVp9nXoLrQ8LUJ5BfZoycwJTAO
BgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwCgYIKoZIzj0EAwID
RwAwRAIgDc8WyXFvsxCk97KS7D/LdYJxMpDKdHNFqpzJT9LddlsCIEr8KcMd/t5p
cRv6rqxvy5M+t0DhRtiwCen70YCUsksb
-----END CERTIFICATE-----`

	alice := pem2der([]byte(aliceCert))
	aliceMakesComeback := pem2der([]byte(reIssuedAliceCert))

	for _, test := range []struct {
		description string
		errContains string
		first       []byte
		second      []byte
	}{
		{
			description: "Bad first certificate",
			errContains: "malformed certificate",
			first:       []byte{1, 2, 3},
			second:      bob,
		},

		{
			description: "Bad second certificate",
			errContains: "malformed certificate",
			first:       alice,
			second:      []byte{1, 2, 3},
		},

		{
			description: "Different certificate",
			errContains: ErrPubKeyMismatch.Error(),
			first:       alice,
			second:      bob,
		},

		{
			description: "Same certificate",
			first:       alice,
			second:      alice,
		},

		{
			description: "Same certificate but different validity period",
			first:       alice,
			second:      aliceMakesComeback,
		},
	} {
		t.Run(test.description, func(t *testing.T) {
			err := CertificatesWithSamePublicKey(test.first, test.second)
			if test.errContains != "" {
				require.Error(t, err)
				require.Contains(t, err.Error(), test.errContains)
				return
			}

			require.NoError(t, err)
		})
	}
}

func pem2der(p []byte) []byte {
	b, _ := pem.Decode(p)
	return b.Bytes
}
