package nonce

import (
	"context"
	"errors"
	"fmt"
	"testing"

	corepb "github.com/letsencrypt/boulder/core/proto"
	"github.com/letsencrypt/boulder/metrics"
	noncepb "github.com/letsencrypt/boulder/nonce/proto"
	"github.com/letsencrypt/boulder/test"
	"google.golang.org/grpc"
)

func TestValidNonce(t *testing.T) {
	ns, err := NewNonceService(metrics.NoopRegisterer, 0, "")
	test.AssertNotError(t, err, "Could not create nonce service")
	n, err := ns.Nonce()
	test.AssertNotError(t, err, "Could not create nonce")
	test.Assert(t, ns.Valid(n), fmt.Sprintf("Did not recognize fresh nonce %s", n))
}

func TestAlreadyUsed(t *testing.T) {
	ns, err := NewNonceService(metrics.NoopRegisterer, 0, "")
	test.AssertNotError(t, err, "Could not create nonce service")
	n, err := ns.Nonce()
	test.AssertNotError(t, err, "Could not create nonce")
	test.Assert(t, ns.Valid(n), "Did not recognize fresh nonce")
	test.Assert(t, !ns.Valid(n), "Recognized the same nonce twice")
}

func TestRejectMalformed(t *testing.T) {
	ns, err := NewNonceService(metrics.NoopRegisterer, 0, "")
	test.AssertNotError(t, err, "Could not create nonce service")
	n, err := ns.Nonce()
	test.AssertNotError(t, err, "Could not create nonce")
	test.Assert(t, !ns.Valid("asdf"+n), "Accepted an invalid nonce")
}

func TestRejectShort(t *testing.T) {
	ns, err := NewNonceService(metrics.NoopRegisterer, 0, "")
	test.AssertNotError(t, err, "Could not create nonce service")
	test.Assert(t, !ns.Valid("aGkK"), "Accepted an invalid nonce")
}

func TestRejectUnknown(t *testing.T) {
	ns1, err := NewNonceService(metrics.NoopRegisterer, 0, "")
	test.AssertNotError(t, err, "Could not create nonce service")
	ns2, err := NewNonceService(metrics.NoopRegisterer, 0, "")
	test.AssertNotError(t, err, "Could not create nonce service")

	n, err := ns1.Nonce()
	test.AssertNotError(t, err, "Could not create nonce")
	test.Assert(t, !ns2.Valid(n), "Accepted a foreign nonce")
}

func TestRejectTooLate(t *testing.T) {
	ns, err := NewNonceService(metrics.NoopRegisterer, 0, "")
	test.AssertNotError(t, err, "Could not create nonce service")

	ns.latest = 2
	n, err := ns.Nonce()
	test.AssertNotError(t, err, "Could not create nonce")
	ns.latest = 1
	test.Assert(t, !ns.Valid(n), "Accepted a nonce with a too-high counter")
}

func TestRejectTooEarly(t *testing.T) {
	ns, err := NewNonceService(metrics.NoopRegisterer, 0, "")
	test.AssertNotError(t, err, "Could not create nonce service")

	n0, err := ns.Nonce()
	test.AssertNotError(t, err, "Could not create nonce")

	for i := 0; i < ns.maxUsed; i++ {
		n, err := ns.Nonce()
		test.AssertNotError(t, err, "Could not create nonce")
		if !ns.Valid(n) {
			t.Errorf("generated invalid nonce")
		}
	}

	n1, err := ns.Nonce()
	test.AssertNotError(t, err, "Could not create nonce")
	n2, err := ns.Nonce()
	test.AssertNotError(t, err, "Could not create nonce")
	n3, err := ns.Nonce()
	test.AssertNotError(t, err, "Could not create nonce")

	test.Assert(t, ns.Valid(n3), "Rejected a valid nonce")
	test.Assert(t, ns.Valid(n2), "Rejected a valid nonce")
	test.Assert(t, ns.Valid(n1), "Rejected a valid nonce")
	test.Assert(t, !ns.Valid(n0), "Accepted a nonce that we should have forgotten")
}

func BenchmarkNonces(b *testing.B) {
	ns, err := NewNonceService(metrics.NoopRegisterer, 0, "")
	if err != nil {
		b.Fatal("creating nonce service", err)
	}

	for i := 0; i < ns.maxUsed; i++ {
		n, err := ns.Nonce()
		if err != nil {
			b.Fatal("noncing", err)
		}
		if !ns.Valid(n) {
			b.Fatal("generated invalid nonce")
		}
	}

	b.ResetTimer()
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			n, err := ns.Nonce()
			if err != nil {
				b.Fatal("noncing", err)
			}
			if !ns.Valid(n) {
				b.Fatal("generated invalid nonce")
			}
		}
	})
}

func TestNoncePrefixing(t *testing.T) {
	ns, err := NewNonceService(metrics.NoopRegisterer, 0, "zinc")
	test.AssertNotError(t, err, "Could not create nonce service")

	n, err := ns.Nonce()
	test.AssertNotError(t, err, "Could not create nonce")
	test.Assert(t, ns.Valid(n), "Valid nonce rejected")

	n, err = ns.Nonce()
	test.AssertNotError(t, err, "Could not create nonce")
	n = n[1:]
	test.Assert(t, !ns.Valid(n), "Valid nonce with incorrect prefix accepted")

	n, err = ns.Nonce()
	test.AssertNotError(t, err, "Could not create nonce")
	test.Assert(t, !ns.Valid(n[6:]), "Valid nonce without prefix accepted")
}

type malleableNonceClient struct {
	redeem func(ctx context.Context, in *noncepb.NonceMessage, opts ...grpc.CallOption) (*noncepb.ValidMessage, error)
}

func (mnc *malleableNonceClient) Redeem(ctx context.Context, in *noncepb.NonceMessage, opts ...grpc.CallOption) (*noncepb.ValidMessage, error) {
	return mnc.redeem(ctx, in, opts...)
}

func (mnc *malleableNonceClient) Nonce(ctx context.Context, in *corepb.Empty, opts ...grpc.CallOption) (*noncepb.NonceMessage, error) {
	return nil, errors.New("unimplemented")
}

func TestRemoteRedeem(t *testing.T) {
	valid, err := RemoteRedeem(context.Background(), nil, "q")
	test.AssertNotError(t, err, "RemoteRedeem failed")
	test.Assert(t, !valid, "RemoteRedeem accepted an invalid nonce")
	valid, err = RemoteRedeem(context.Background(), nil, "")
	test.AssertNotError(t, err, "RemoteRedeem failed")
	test.Assert(t, !valid, "RemoteRedeem accepted an empty nonce")

	prefixMap := map[string]noncepb.NonceServiceClient{
		"abcd": &malleableNonceClient{
			redeem: func(ctx context.Context, in *noncepb.NonceMessage, opts ...grpc.CallOption) (*noncepb.ValidMessage, error) {
				return nil, errors.New("wrong one!")
			},
		},
		"wxyz": &malleableNonceClient{
			redeem: func(ctx context.Context, in *noncepb.NonceMessage, opts ...grpc.CallOption) (*noncepb.ValidMessage, error) {
				return &noncepb.ValidMessage{Valid: false}, nil
			},
		},
	}
	// Attempt to redeem a nonce with a prefix not in the prefix map, expect return false, nil
	valid, err = RemoteRedeem(context.Background(), prefixMap, "asddCQEC")
	test.AssertNotError(t, err, "RemoteRedeem failed")
	test.Assert(t, !valid, "RemoteRedeem accepted nonce not in prefix map")

	// Attempt to redeem a nonce with a prefix in the prefix map, remote returns error
	// expect false, err
	_, err = RemoteRedeem(context.Background(), prefixMap, "abcdbeef")
	test.AssertError(t, err, "RemoteRedeem didn't return error when remote did")

	// Attempt to redeem a nonce with a prefix in the prefix map, remote returns valid
	// expect true, nil
	valid, err = RemoteRedeem(context.Background(), prefixMap, "wxyzdead")
	test.AssertNotError(t, err, "RemoteRedeem failed")
	test.Assert(t, !valid, "RemoteRedeem didn't honor remote result")

	// Attempt to redeem a nonce with a prefix in the prefix map, remote returns invalid
	// expect false, nil
	prefixMap["wxyz"] = &malleableNonceClient{
		redeem: func(ctx context.Context, in *noncepb.NonceMessage, opts ...grpc.CallOption) (*noncepb.ValidMessage, error) {
			return &noncepb.ValidMessage{Valid: true}, nil
		},
	}
	valid, err = RemoteRedeem(context.Background(), prefixMap, "wxyzdead")
	test.AssertNotError(t, err, "RemoteRedeem failed")
	test.Assert(t, valid, "RemoteRedeem didn't honor remote result")
}

func TestNoncePrefixValidation(t *testing.T) {
	_, err := NewNonceService(metrics.NoopRegisterer, 0, "hey")
	test.AssertError(t, err, "NewNonceService didn't fail with short prefix")
	_, err = NewNonceService(metrics.NoopRegisterer, 0, "hey!")
	test.AssertError(t, err, "NewNonceService didn't fail with invalid base64")
	_, err = NewNonceService(metrics.NoopRegisterer, 0, "heyy")
	test.AssertNotError(t, err, "NewNonceService failed with valid nonce prefix")
}
