package wfe2

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/jmhodges/clock"
	"google.golang.org/grpc"
	"google.golang.org/protobuf/types/known/timestamppb"

	corepb "github.com/letsencrypt/boulder/core/proto"
	"github.com/letsencrypt/boulder/metrics"
	sapb "github.com/letsencrypt/boulder/sa/proto"
	"github.com/letsencrypt/boulder/test"
)

type recordingBackend struct {
	requests []int64
}

func (rb *recordingBackend) GetRegistration(
	ctx context.Context,
	regID *sapb.RegistrationID,
	opts ...grpc.CallOption,
) (*corepb.Registration, error) {
	rb.requests = append(rb.requests, regID.Id)
	return &corepb.Registration{
		Id: regID.Id,
	}, nil
}

func TestCacheAddRetrieve(t *testing.T) {
	ctx := context.Background()
	backend := &recordingBackend{}

	cache := NewAccountCache(backend, 10, time.Second, clock.NewFake(), metrics.NoopRegisterer)

	result, err := cache.GetRegistration(ctx, &sapb.RegistrationID{Id: 1234})
	test.AssertNotError(t, err, "getting registration")
	test.AssertEquals(t, result.Id, int64(1234))
	test.AssertEquals(t, len(backend.requests), 1)

	// Request it again. This should hit the cache so our backend should not see additional requests.
	result, err = cache.GetRegistration(ctx, &sapb.RegistrationID{Id: 1234})
	test.AssertNotError(t, err, "getting registration")
	test.AssertEquals(t, result.Id, int64(1234))
	test.AssertEquals(t, len(backend.requests), 1)
}

// Test that the cache copies values before giving them out, so code that receives a cached
// value can't modify the cache's contents.
func TestCacheCopy(t *testing.T) {
	ctx := context.Background()
	backend := &recordingBackend{}

	cache := NewAccountCache(backend, 10, time.Second, clock.NewFake(), metrics.NoopRegisterer)

	acct, err := cache.GetRegistration(ctx, &sapb.RegistrationID{Id: 1234})
	test.AssertNotError(t, err, "getting registration")
	test.AssertEquals(t, len(backend.requests), 1)
	origTimestamp := acct.CreatedAt

	test.AssertEquals(t, cache.cache.Len(), 1)

	// Request it again. This should hit the cache.
	result, err := cache.GetRegistration(ctx, &sapb.RegistrationID{Id: 1234})
	test.AssertNotError(t, err, "getting registration")
	test.AssertEquals(t, len(backend.requests), 1)

	// Modify a pointer value inside the result
	result.CreatedAt = timestamppb.New(time.Now().Add(24 * time.Hour))

	result, err = cache.GetRegistration(ctx, &sapb.RegistrationID{Id: 1234})
	test.AssertNotError(t, err, "getting registration")
	test.AssertEquals(t, len(backend.requests), 1)

	test.AssertDeepEquals(t, result.CreatedAt, origTimestamp)
}

// Test that the cache expires values.
func TestCacheExpires(t *testing.T) {
	ctx := context.Background()
	backend := &recordingBackend{}

	clk := clock.NewFake()
	cache := NewAccountCache(backend, 10, time.Second, clk, metrics.NoopRegisterer)

	_, err := cache.GetRegistration(ctx, &sapb.RegistrationID{Id: 1234})
	test.AssertNotError(t, err, "getting registration")
	test.AssertEquals(t, len(backend.requests), 1)

	// Request it again. This should hit the cache.
	_, err = cache.GetRegistration(ctx, &sapb.RegistrationID{Id: 1234})
	test.AssertNotError(t, err, "getting registration")
	test.AssertEquals(t, len(backend.requests), 1)

	test.AssertEquals(t, cache.cache.Len(), 1)

	// "Sleep" 10 seconds to expire the entry
	clk.Sleep(10 * time.Second)

	// This should not hit the cache
	_, err = cache.GetRegistration(ctx, &sapb.RegistrationID{Id: 1234})
	test.AssertNotError(t, err, "getting registration")
	test.AssertEquals(t, len(backend.requests), 2)
}

type wrongIDBackend struct{}

func (wib wrongIDBackend) GetRegistration(
	ctx context.Context,
	regID *sapb.RegistrationID,
	opts ...grpc.CallOption,
) (*corepb.Registration, error) {
	return &corepb.Registration{
		Id: regID.Id + 1,
	}, nil
}

func TestWrongId(t *testing.T) {
	ctx := context.Background()
	cache := NewAccountCache(wrongIDBackend{}, 10, time.Second, clock.NewFake(), metrics.NoopRegisterer)

	_, err := cache.GetRegistration(ctx, &sapb.RegistrationID{Id: 1234})
	test.AssertError(t, err, "expected error when backend returns wrong ID")
}

type errorBackend struct{}

func (eb errorBackend) GetRegistration(ctx context.Context,
	regID *sapb.RegistrationID,
	opts ...grpc.CallOption,
) (*corepb.Registration, error) {
	return nil, errors.New("some error")
}

func TestErrorPassthrough(t *testing.T) {
	ctx := context.Background()
	cache := NewAccountCache(errorBackend{}, 10, time.Second, clock.NewFake(), metrics.NoopRegisterer)

	_, err := cache.GetRegistration(ctx, &sapb.RegistrationID{Id: 1234})
	test.AssertError(t, err, "expected error when backend errors")
	test.AssertEquals(t, err.Error(), "some error")
}
