package nosql

import (
	"context"
	"encoding/json"
	"fmt"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/pkg/errors"

	"github.com/smallstep/assert"
	"github.com/smallstep/certificates/acme"
	certdb "github.com/smallstep/certificates/db"
	"github.com/smallstep/nosql"
	nosqldb "github.com/smallstep/nosql/database"
)

func TestDB_getDBExternalAccountKey(t *testing.T) {
	keyID := "keyID"
	provID := "provID"
	type test struct {
		db      nosql.DB
		err     error
		acmeErr *acme.Error
		dbeak   *dbExternalAccountKey
	}
	var tests = map[string]func(t *testing.T) test{
		"ok": func(t *testing.T) test {
			now := clock.Now()
			dbeak := &dbExternalAccountKey{
				ID:            keyID,
				ProvisionerID: provID,
				Reference:     "ref",
				AccountID:     "",
				HmacKey:       []byte{1, 3, 3, 7},
				CreatedAt:     now,
			}
			b, err := json.Marshal(dbeak)
			assert.FatalError(t, err)
			return test{
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, externalAccountKeyTable)
						assert.Equals(t, string(key), keyID)
						return b, nil
					},
				},
				err:   nil,
				dbeak: dbeak,
			}
		},
		"fail/not-found": func(t *testing.T) test {
			return test{
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, externalAccountKeyTable)
						assert.Equals(t, string(key), keyID)
						return nil, nosqldb.ErrNotFound
					},
				},
				err: acme.ErrNotFound,
			}
		},
		"fail/db.Get-error": func(t *testing.T) test {
			return test{
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, externalAccountKeyTable)
						assert.Equals(t, string(key), keyID)
						return nil, errors.New("force")
					},
				},
				err: errors.New("error loading external account key keyID: force"),
			}
		},
		"fail/unmarshal-error": func(t *testing.T) test {
			return test{
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, externalAccountKeyTable)
						assert.Equals(t, string(key), keyID)

						return []byte("foo"), nil
					},
				},
				err: errors.New("error unmarshaling external account key keyID into dbExternalAccountKey"),
			}
		},
	}
	for name, run := range tests {
		tc := run(t)
		t.Run(name, func(t *testing.T) {
			d := DB{db: tc.db}
			if dbeak, err := d.getDBExternalAccountKey(context.Background(), keyID); err != nil {
				switch k := err.(type) {
				case *acme.Error:
					if assert.NotNil(t, tc.acmeErr) {
						assert.Equals(t, k.Type, tc.acmeErr.Type)
						assert.Equals(t, k.Detail, tc.acmeErr.Detail)
						assert.Equals(t, k.Status, tc.acmeErr.Status)
						assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
						assert.Equals(t, k.Detail, tc.acmeErr.Detail)
					}
				default:
					if assert.NotNil(t, tc.err) {
						assert.HasPrefix(t, err.Error(), tc.err.Error())
					}
				}
			} else if assert.Nil(t, tc.err) {
				assert.Equals(t, dbeak.ID, tc.dbeak.ID)
				assert.Equals(t, dbeak.HmacKey, tc.dbeak.HmacKey)
				assert.Equals(t, dbeak.ProvisionerID, tc.dbeak.ProvisionerID)
				assert.Equals(t, dbeak.Reference, tc.dbeak.Reference)
				assert.Equals(t, dbeak.CreatedAt, tc.dbeak.CreatedAt)
				assert.Equals(t, dbeak.AccountID, tc.dbeak.AccountID)
				assert.Equals(t, dbeak.BoundAt, tc.dbeak.BoundAt)
			}
		})
	}
}

func TestDB_GetExternalAccountKey(t *testing.T) {
	keyID := "keyID"
	provID := "provID"
	type test struct {
		db      nosql.DB
		err     error
		acmeErr *acme.Error
		eak     *acme.ExternalAccountKey
	}
	var tests = map[string]func(t *testing.T) test{
		"ok": func(t *testing.T) test {
			now := clock.Now()
			dbeak := &dbExternalAccountKey{
				ID:            keyID,
				ProvisionerID: provID,
				Reference:     "ref",
				AccountID:     "",
				HmacKey:       []byte{1, 3, 3, 7},
				CreatedAt:     now,
			}
			b, err := json.Marshal(dbeak)
			assert.FatalError(t, err)
			return test{
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, externalAccountKeyTable)
						assert.Equals(t, string(key), keyID)
						return b, nil
					},
				},
				eak: &acme.ExternalAccountKey{
					ID:            keyID,
					ProvisionerID: provID,
					Reference:     "ref",
					AccountID:     "",
					HmacKey:       []byte{1, 3, 3, 7},
					CreatedAt:     now,
				},
			}
		},
		"fail/db.Get-error": func(t *testing.T) test {
			return test{
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, externalAccountKeyTable)
						assert.Equals(t, string(key), keyID)

						return nil, errors.New("force")
					},
				},
				err: errors.New("error loading external account key keyID: force"),
			}
		},
		"fail/non-matching-provisioner": func(t *testing.T) test {
			now := clock.Now()
			dbeak := &dbExternalAccountKey{
				ID:            keyID,
				ProvisionerID: "aDifferentProvID",
				Reference:     "ref",
				AccountID:     "",
				HmacKey:       []byte{1, 3, 3, 7},
				CreatedAt:     now,
			}
			b, err := json.Marshal(dbeak)
			assert.FatalError(t, err)
			return test{
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, externalAccountKeyTable)
						assert.Equals(t, string(key), keyID)
						return b, nil
					},
				},
				eak: &acme.ExternalAccountKey{
					ID:            keyID,
					ProvisionerID: provID,
					Reference:     "ref",
					AccountID:     "",
					HmacKey:       []byte{1, 3, 3, 7},
					CreatedAt:     now,
				},
				acmeErr: acme.NewError(acme.ErrorUnauthorizedType, "provisioner does not match provisioner for which the EAB key was created"),
			}
		},
	}
	for name, run := range tests {
		tc := run(t)
		t.Run(name, func(t *testing.T) {
			d := DB{db: tc.db}
			if eak, err := d.GetExternalAccountKey(context.Background(), provID, keyID); err != nil {
				switch k := err.(type) {
				case *acme.Error:
					if assert.NotNil(t, tc.acmeErr) {
						assert.Equals(t, k.Type, tc.acmeErr.Type)
						assert.Equals(t, k.Detail, tc.acmeErr.Detail)
						assert.Equals(t, k.Status, tc.acmeErr.Status)
						assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
						assert.Equals(t, k.Detail, tc.acmeErr.Detail)
					}
				default:
					if assert.NotNil(t, tc.err) {
						assert.HasPrefix(t, err.Error(), tc.err.Error())
					}
				}
			} else if assert.Nil(t, tc.err) {
				assert.Equals(t, eak.ID, tc.eak.ID)
				assert.Equals(t, eak.HmacKey, tc.eak.HmacKey)
				assert.Equals(t, eak.ProvisionerID, tc.eak.ProvisionerID)
				assert.Equals(t, eak.Reference, tc.eak.Reference)
				assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt)
				assert.Equals(t, eak.AccountID, tc.eak.AccountID)
				assert.Equals(t, eak.BoundAt, tc.eak.BoundAt)
			}
		})
	}
}

func TestDB_GetExternalAccountKeyByReference(t *testing.T) {
	keyID := "keyID"
	provID := "provID"
	ref := "ref"
	type test struct {
		db      nosql.DB
		err     error
		ref     string
		acmeErr *acme.Error
		eak     *acme.ExternalAccountKey
	}
	var tests = map[string]func(t *testing.T) test{
		"ok": func(t *testing.T) test {
			now := clock.Now()
			dbeak := &dbExternalAccountKey{
				ID:            keyID,
				ProvisionerID: provID,
				Reference:     ref,
				AccountID:     "",
				HmacKey:       []byte{1, 3, 3, 7},
				CreatedAt:     now,
			}
			dbref := &dbExternalAccountKeyReference{
				Reference:            ref,
				ExternalAccountKeyID: keyID,
			}
			b, err := json.Marshal(dbeak)
			assert.FatalError(t, err)
			dbrefBytes, err := json.Marshal(dbref)
			assert.FatalError(t, err)
			return test{
				ref: ref,
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						switch string(bucket) {
						case string(externalAccountKeyIDsByReferenceTable):
							assert.Equals(t, string(key), provID+"."+ref)
							return dbrefBytes, nil
						case string(externalAccountKeyTable):
							assert.Equals(t, string(key), keyID)
							return b, nil
						default:
							assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
							return nil, errors.New("force")
						}
					},
				},
				eak: &acme.ExternalAccountKey{
					ID:            keyID,
					ProvisionerID: provID,
					Reference:     ref,
					AccountID:     "",
					HmacKey:       []byte{1, 3, 3, 7},
					CreatedAt:     now,
				},
				err: nil,
			}
		},
		"ok/no-reference": func(t *testing.T) test {
			return test{
				ref: "",
				eak: nil,
				err: nil,
			}
		},
		"fail/reference-not-found": func(t *testing.T) test {
			return test{
				ref: ref,
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, string(bucket), string(externalAccountKeyIDsByReferenceTable))
						assert.Equals(t, string(key), provID+"."+ref)
						return nil, nosqldb.ErrNotFound
					},
				},
				err: errors.New("not found"),
			}
		},
		"fail/reference-load-error": func(t *testing.T) test {
			return test{
				ref: ref,
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, string(bucket), string(externalAccountKeyIDsByReferenceTable))
						assert.Equals(t, string(key), provID+"."+ref)
						return nil, errors.New("force")
					},
				},
				err: errors.New("error loading ACME EAB key for reference ref: force"),
			}
		},
		"fail/reference-unmarshal-error": func(t *testing.T) test {
			return test{
				ref: ref,
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, string(bucket), string(externalAccountKeyIDsByReferenceTable))
						assert.Equals(t, string(key), provID+"."+ref)
						return []byte{0}, nil
					},
				},
				err: errors.New("error unmarshaling ACME EAB key for reference ref"),
			}
		},
		"fail/db.GetExternalAccountKey-error": func(t *testing.T) test {
			dbref := &dbExternalAccountKeyReference{
				Reference:            ref,
				ExternalAccountKeyID: keyID,
			}
			dbrefBytes, err := json.Marshal(dbref)
			assert.FatalError(t, err)
			return test{
				ref: ref,
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						switch string(bucket) {
						case string(externalAccountKeyIDsByReferenceTable):
							assert.Equals(t, string(key), provID+"."+ref)
							return dbrefBytes, nil
						case string(externalAccountKeyTable):
							assert.Equals(t, string(key), keyID)
							return nil, errors.New("force")
						default:
							assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
							return nil, errors.New("force")
						}
					},
				},
				err: errors.New("error loading external account key keyID: force"),
			}
		},
	}
	for name, run := range tests {
		tc := run(t)
		t.Run(name, func(t *testing.T) {
			d := DB{db: tc.db}
			if eak, err := d.GetExternalAccountKeyByReference(context.Background(), provID, tc.ref); err != nil {
				switch k := err.(type) {
				case *acme.Error:
					if assert.NotNil(t, tc.acmeErr) {
						assert.Equals(t, k.Type, tc.acmeErr.Type)
						assert.Equals(t, k.Detail, tc.acmeErr.Detail)
						assert.Equals(t, k.Status, tc.acmeErr.Status)
						assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
						assert.Equals(t, k.Detail, tc.acmeErr.Detail)
					}
				default:
					if assert.NotNil(t, tc.err) {
						assert.HasPrefix(t, err.Error(), tc.err.Error())
					}
				}
			} else if assert.Nil(t, tc.err) && tc.eak != nil {
				assert.Equals(t, eak.ID, tc.eak.ID)
				assert.Equals(t, eak.AccountID, tc.eak.AccountID)
				assert.Equals(t, eak.BoundAt, tc.eak.BoundAt)
				assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt)
				assert.Equals(t, eak.HmacKey, tc.eak.HmacKey)
				assert.Equals(t, eak.ProvisionerID, tc.eak.ProvisionerID)
				assert.Equals(t, eak.Reference, tc.eak.Reference)
			}
		})
	}
}

func TestDB_GetExternalAccountKeys(t *testing.T) {
	keyID1 := "keyID1"
	keyID2 := "keyID2"
	keyID3 := "keyID3"
	provID := "provID"
	ref := "ref"
	type test struct {
		db      nosql.DB
		err     error
		acmeErr *acme.Error
		eaks    []*acme.ExternalAccountKey
	}
	var tests = map[string]func(t *testing.T) test{
		"ok": func(t *testing.T) test {
			now := clock.Now()
			dbeak1 := &dbExternalAccountKey{
				ID:            keyID1,
				ProvisionerID: provID,
				Reference:     ref,
				AccountID:     "",
				HmacKey:       []byte{1, 3, 3, 7},
				CreatedAt:     now,
			}
			b1, err := json.Marshal(dbeak1)
			assert.FatalError(t, err)
			dbeak2 := &dbExternalAccountKey{
				ID:            keyID2,
				ProvisionerID: provID,
				Reference:     ref,
				AccountID:     "",
				HmacKey:       []byte{1, 3, 3, 7},
				CreatedAt:     now,
			}
			b2, err := json.Marshal(dbeak2)
			assert.FatalError(t, err)
			dbeak3 := &dbExternalAccountKey{
				ID:            keyID3,
				ProvisionerID: "aDifferentProvID",
				Reference:     ref,
				AccountID:     "",
				HmacKey:       []byte{1, 3, 3, 7},
				CreatedAt:     now,
			}
			b3, err := json.Marshal(dbeak3)
			assert.FatalError(t, err)
			return test{
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						switch string(bucket) {
						case string(externalAccountKeyIDsByProvisionerIDTable):
							keys := []string{"", keyID1, keyID2} // includes an empty keyID
							b, err := json.Marshal(keys)
							assert.FatalError(t, err)
							return b, nil
						case string(externalAccountKeyTable):
							switch string(key) {
							case keyID1:
								return b1, nil
							case keyID2:
								return b2, nil
							default:
								assert.FatalError(t, errors.Errorf("unexpected key %s", string(key)))
								return nil, errors.New("force default")
							}
						default:
							assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
							return nil, errors.New("force default")
						}
					},
					// TODO: remove the MList
					MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
						switch string(bucket) {
						case string(externalAccountKeyTable):
							return []*nosqldb.Entry{
								{
									Bucket: bucket,
									Key:    []byte(keyID1),
									Value:  b1,
								},
								{
									Bucket: bucket,
									Key:    []byte(keyID2),
									Value:  b2,
								},
								{
									Bucket: bucket,
									Key:    []byte(keyID3),
									Value:  b3,
								},
							}, nil
						case string(externalAccountKeyIDsByProvisionerIDTable):
							keys := []string{keyID1, keyID2}
							b, err := json.Marshal(keys)
							assert.FatalError(t, err)
							return []*nosqldb.Entry{
								{
									Bucket: bucket,
									Key:    []byte(provID),
									Value:  b,
								},
							}, nil
						default:
							assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
							return nil, errors.New("force default")
						}
					},
				},
				eaks: []*acme.ExternalAccountKey{
					{
						ID:            keyID1,
						ProvisionerID: provID,
						Reference:     ref,
						AccountID:     "",
						HmacKey:       []byte{1, 3, 3, 7},
						CreatedAt:     now,
					},
					{
						ID:            keyID2,
						ProvisionerID: provID,
						Reference:     ref,
						AccountID:     "",
						HmacKey:       []byte{1, 3, 3, 7},
						CreatedAt:     now,
					},
				},
			}
		},
		"fail/db.Get-externalAccountKeysByProvisionerIDTable": func(t *testing.T) test {
			return test{
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, string(bucket), string(externalAccountKeyIDsByProvisionerIDTable))
						return nil, errors.New("force")
					},
				},
				err: errors.New("error loading ACME EAB Key IDs for provisioner provID: force"),
			}
		},
		"fail/db.Get-externalAccountKeysByProvisionerIDTable-unmarshal": func(t *testing.T) test {
			return test{
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, string(bucket), string(externalAccountKeyIDsByProvisionerIDTable))
						b, _ := json.Marshal(1)
						return b, nil
					},
				},
				err: errors.New("error unmarshaling ACME EAB Key IDs for provisioner provID: json: cannot unmarshal number into Go value of type []string"),
			}
		},
		"fail/db.getDBExternalAccountKey": func(t *testing.T) test {
			return test{
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						switch string(bucket) {
						case string(externalAccountKeyIDsByProvisionerIDTable):
							keys := []string{keyID1, keyID2}
							b, err := json.Marshal(keys)
							assert.FatalError(t, err)
							return b, nil
						case string(externalAccountKeyTable):
							return nil, errors.New("force")
						default:
							assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
							return nil, errors.New("force bucket")
						}
					},
				},
				err: errors.New("error retrieving ACME EAB Key for provisioner provID and keyID keyID1: error loading external account key keyID1: force"),
			}
		},
	}
	for name, run := range tests {
		tc := run(t)
		t.Run(name, func(t *testing.T) {
			d := DB{db: tc.db}
			cursor, limit := "", 0
			if eaks, nextCursor, err := d.GetExternalAccountKeys(context.Background(), provID, cursor, limit); err != nil {
				assert.Equals(t, "", nextCursor)
				switch k := err.(type) {
				case *acme.Error:
					if assert.NotNil(t, tc.acmeErr) {
						assert.Equals(t, k.Type, tc.acmeErr.Type)
						assert.Equals(t, k.Detail, tc.acmeErr.Detail)
						assert.Equals(t, k.Status, tc.acmeErr.Status)
						assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
						assert.Equals(t, k.Detail, tc.acmeErr.Detail)
					}
				default:
					if assert.NotNil(t, tc.err) {
						assert.Equals(t, tc.err.Error(), err.Error())
					}
				}
			} else if assert.Nil(t, tc.err) {
				assert.Equals(t, len(eaks), len(tc.eaks))
				assert.Equals(t, "", nextCursor)
				for i, eak := range eaks {
					assert.Equals(t, eak.ID, tc.eaks[i].ID)
					assert.Equals(t, eak.HmacKey, tc.eaks[i].HmacKey)
					assert.Equals(t, eak.ProvisionerID, tc.eaks[i].ProvisionerID)
					assert.Equals(t, eak.Reference, tc.eaks[i].Reference)
					assert.Equals(t, eak.CreatedAt, tc.eaks[i].CreatedAt)
					assert.Equals(t, eak.AccountID, tc.eaks[i].AccountID)
					assert.Equals(t, eak.BoundAt, tc.eaks[i].BoundAt)
				}
			}
		})
	}
}

func TestDB_DeleteExternalAccountKey(t *testing.T) {
	keyID := "keyID"
	provID := "provID"
	ref := "ref"
	type test struct {
		db      nosql.DB
		err     error
		acmeErr *acme.Error
	}
	var tests = map[string]func(t *testing.T) test{
		"ok": func(t *testing.T) test {
			now := clock.Now()
			dbeak := &dbExternalAccountKey{
				ID:            keyID,
				ProvisionerID: provID,
				Reference:     ref,
				AccountID:     "",
				HmacKey:       []byte{1, 3, 3, 7},
				CreatedAt:     now,
			}
			dbref := &dbExternalAccountKeyReference{
				Reference:            ref,
				ExternalAccountKeyID: keyID,
			}
			b, err := json.Marshal(dbeak)
			assert.FatalError(t, err)
			dbrefBytes, err := json.Marshal(dbref)
			assert.FatalError(t, err)
			return test{
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						switch string(bucket) {
						case string(externalAccountKeyIDsByReferenceTable):
							assert.Equals(t, string(key), provID+"."+ref)
							return dbrefBytes, nil
						case string(externalAccountKeyTable):
							assert.Equals(t, string(key), keyID)
							return b, nil
						case string(externalAccountKeyIDsByProvisionerIDTable):
							assert.Equals(t, provID, string(key))
							b, err := json.Marshal([]string{keyID})
							assert.FatalError(t, err)
							return b, nil
						default:
							assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
							return nil, errors.New("force default")
						}
					},
					MDel: func(bucket, key []byte) error {
						switch string(bucket) {
						case string(externalAccountKeyIDsByReferenceTable):
							assert.Equals(t, string(key), provID+"."+ref)
							return nil
						case string(externalAccountKeyTable):
							assert.Equals(t, string(key), keyID)
							return nil
						default:
							assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
							return errors.New("force default")
						}
					},
					MCmpAndSwap: func(bucket, key, old, new []byte) ([]byte, bool, error) {
						fmt.Println(string(bucket))
						switch string(bucket) {
						case string(externalAccountKeyIDsByReferenceTable):
							assert.Equals(t, provID+"."+ref, string(key))
							return nil, true, nil
						case string(externalAccountKeyIDsByProvisionerIDTable):
							assert.Equals(t, provID, string(key))
							return nil, true, nil
						default:
							assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
							return nil, false, errors.New("force default")
						}
					},
				},
			}
		},
		"fail/not-found": func(t *testing.T) test {
			return test{
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, string(bucket), string(externalAccountKeyTable))
						assert.Equals(t, string(key), keyID)
						return nil, nosqldb.ErrNotFound
					},
				},
				err: errors.New("error loading ACME EAB Key with Key ID keyID: not found"),
			}
		},
		"fail/non-matching-provisioner": func(t *testing.T) test {
			now := clock.Now()
			dbeak := &dbExternalAccountKey{
				ID:            keyID,
				ProvisionerID: "aDifferentProvID",
				Reference:     ref,
				AccountID:     "",
				HmacKey:       []byte{1, 3, 3, 7},
				CreatedAt:     now,
			}
			b, err := json.Marshal(dbeak)
			assert.FatalError(t, err)
			return test{
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, string(bucket), string(externalAccountKeyTable))
						assert.Equals(t, string(key), keyID)
						return b, nil
					},
				},
				err: errors.New("provisioner does not match provisioner for which the EAB key was created"),
			}
		},
		"fail/delete-reference": func(t *testing.T) test {
			now := clock.Now()
			dbeak := &dbExternalAccountKey{
				ID:            keyID,
				ProvisionerID: provID,
				Reference:     ref,
				AccountID:     "",
				HmacKey:       []byte{1, 3, 3, 7},
				CreatedAt:     now,
			}
			dbref := &dbExternalAccountKeyReference{
				Reference:            ref,
				ExternalAccountKeyID: keyID,
			}
			b, err := json.Marshal(dbeak)
			assert.FatalError(t, err)
			dbrefBytes, err := json.Marshal(dbref)
			assert.FatalError(t, err)
			return test{
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						switch string(bucket) {
						case string(externalAccountKeyIDsByReferenceTable):
							assert.Equals(t, string(key), ref)
							return dbrefBytes, nil
						case string(externalAccountKeyTable):
							assert.Equals(t, string(key), keyID)
							return b, nil
						default:
							assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
							return nil, errors.New("force default")
						}
					},
					MDel: func(bucket, key []byte) error {
						switch string(bucket) {
						case string(externalAccountKeyIDsByReferenceTable):
							assert.Equals(t, string(key), provID+"."+ref)
							return errors.New("force")
						case string(externalAccountKeyTable):
							assert.Equals(t, string(key), keyID)
							return nil
						default:
							assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
							return errors.New("force default")
						}
					},
				},
				err: errors.New("error deleting ACME EAB Key reference with Key ID keyID and reference ref: force"),
			}
		},
		"fail/delete-eak": func(t *testing.T) test {
			now := clock.Now()
			dbeak := &dbExternalAccountKey{
				ID:            keyID,
				ProvisionerID: provID,
				Reference:     ref,
				AccountID:     "",
				HmacKey:       []byte{1, 3, 3, 7},
				CreatedAt:     now,
			}
			dbref := &dbExternalAccountKeyReference{
				Reference:            ref,
				ExternalAccountKeyID: keyID,
			}
			b, err := json.Marshal(dbeak)
			assert.FatalError(t, err)
			dbrefBytes, err := json.Marshal(dbref)
			assert.FatalError(t, err)
			return test{
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						switch string(bucket) {
						case string(externalAccountKeyIDsByReferenceTable):
							assert.Equals(t, string(key), ref)
							return dbrefBytes, nil
						case string(externalAccountKeyTable):
							assert.Equals(t, string(key), keyID)
							return b, nil
						default:
							assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
							return nil, errors.New("force default")
						}
					},
					MDel: func(bucket, key []byte) error {
						switch string(bucket) {
						case string(externalAccountKeyIDsByReferenceTable):
							assert.Equals(t, string(key), provID+"."+ref)
							return nil
						case string(externalAccountKeyTable):
							assert.Equals(t, string(key), keyID)
							return errors.New("force")
						default:
							assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
							return errors.New("force default")
						}
					},
				},
				err: errors.New("error deleting ACME EAB Key with Key ID keyID: force"),
			}
		},
		"fail/delete-eakID": func(t *testing.T) test {
			now := clock.Now()
			dbeak := &dbExternalAccountKey{
				ID:            keyID,
				ProvisionerID: provID,
				Reference:     ref,
				AccountID:     "",
				HmacKey:       []byte{1, 3, 3, 7},
				CreatedAt:     now,
			}
			dbref := &dbExternalAccountKeyReference{
				Reference:            ref,
				ExternalAccountKeyID: keyID,
			}
			b, err := json.Marshal(dbeak)
			assert.FatalError(t, err)
			dbrefBytes, err := json.Marshal(dbref)
			assert.FatalError(t, err)
			return test{
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						switch string(bucket) {
						case string(externalAccountKeyIDsByReferenceTable):
							assert.Equals(t, string(key), ref)
							return dbrefBytes, nil
						case string(externalAccountKeyTable):
							assert.Equals(t, string(key), keyID)
							return b, nil
						case string(externalAccountKeyIDsByProvisionerIDTable):
							return b, errors.New("force")
						default:
							assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
							return nil, errors.New("force default")
						}
					},
					MDel: func(bucket, key []byte) error {
						switch string(bucket) {
						case string(externalAccountKeyIDsByReferenceTable):
							assert.Equals(t, string(key), provID+"."+ref)
							return nil
						case string(externalAccountKeyTable):
							assert.Equals(t, string(key), keyID)
							return nil
						default:
							assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
							return errors.New("force default")
						}
					},
				},
				err: errors.New("error removing ACME EAB Key ID keyID: error loading eakIDs for provisioner provID: force"),
			}
		},
	}
	for name, run := range tests {
		tc := run(t)
		t.Run(name, func(t *testing.T) {
			d := DB{db: tc.db}
			if err := d.DeleteExternalAccountKey(context.Background(), provID, keyID); err != nil {
				switch k := err.(type) {
				case *acme.Error:
					if assert.NotNil(t, tc.acmeErr) {
						assert.Equals(t, k.Type, tc.acmeErr.Type)
						assert.Equals(t, k.Detail, tc.acmeErr.Detail)
						assert.Equals(t, k.Status, tc.acmeErr.Status)
						assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
						assert.Equals(t, k.Detail, tc.acmeErr.Detail)
					}
				default:
					if assert.NotNil(t, tc.err) {
						assert.Equals(t, err.Error(), tc.err.Error())
					}
				}
			} else {
				assert.Nil(t, tc.err)
			}
		})
	}
}

func TestDB_CreateExternalAccountKey(t *testing.T) {
	keyID := "keyID"
	provID := "provID"
	ref := "ref"
	type test struct {
		db  nosql.DB
		err error
		_id *string
		eak *acme.ExternalAccountKey
	}
	var tests = map[string]func(t *testing.T) test{
		"ok": func(t *testing.T) test {
			var (
				id    string
				idPtr = &id
			)
			now := clock.Now()
			eak := &acme.ExternalAccountKey{
				ID:            keyID,
				ProvisionerID: provID,
				Reference:     "ref",
				AccountID:     "",
				CreatedAt:     now,
			}
			return test{
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, string(bucket), string(externalAccountKeyIDsByProvisionerIDTable))
						assert.Equals(t, provID, string(key))
						b, _ := json.Marshal([]string{})
						return b, nil
					},
					MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
						switch string(bucket) {
						case string(externalAccountKeyIDsByProvisionerIDTable):
							assert.Equals(t, provID, string(key))
							return nu, true, nil
						case string(externalAccountKeyIDsByReferenceTable):
							assert.Equals(t, provID+"."+ref, string(key))
							assert.Equals(t, nil, old)
							return nu, true, nil
						case string(externalAccountKeyTable):
							assert.Equals(t, nil, old)

							id = string(key)

							dbeak := new(dbExternalAccountKey)
							assert.FatalError(t, json.Unmarshal(nu, dbeak))
							assert.Equals(t, string(key), dbeak.ID)
							assert.Equals(t, eak.ProvisionerID, dbeak.ProvisionerID)
							assert.Equals(t, eak.Reference, dbeak.Reference)
							assert.Equals(t, 32, len(dbeak.HmacKey))
							assert.False(t, dbeak.CreatedAt.IsZero())
							assert.Equals(t, dbeak.AccountID, eak.AccountID)
							assert.True(t, dbeak.BoundAt.IsZero())
							return nu, true, nil
						default:
							assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
							return nil, false, errors.New("force default")
						}
					},
				},
				eak: eak,
				_id: idPtr,
			}
		},
		"fail/externalAccountKeyID-cmpAndSwap-error": func(t *testing.T) test {
			return test{
				db: &certdb.MockNoSQLDB{
					MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
						switch string(bucket) {
						case string(externalAccountKeyIDsByReferenceTable):
							assert.Equals(t, string(key), ref)
							assert.Equals(t, old, nil)
							return nu, true, nil
						case string(externalAccountKeyTable):
							assert.Equals(t, old, nil)
							return nu, true, errors.New("force")
						default:
							assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
							return nil, false, errors.New("force default")
						}
					},
				},
				err: errors.New("error saving acme external_account_key: force"),
			}
		},
		"fail/addEAKID-error": func(t *testing.T) test {
			return test{
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, string(bucket), string(externalAccountKeyIDsByProvisionerIDTable))
						assert.Equals(t, provID, string(key))
						return nil, errors.New("force")
					},
					MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
						switch string(bucket) {
						case string(externalAccountKeyIDsByReferenceTable):
							assert.Equals(t, string(key), ref)
							assert.Equals(t, old, nil)
							return nu, true, nil
						case string(externalAccountKeyTable):
							assert.Equals(t, old, nil)
							return nu, true, nil
						default:
							assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
							return nil, false, errors.New("force default")
						}
					},
				},
				err: errors.New("error loading eakIDs for provisioner provID: force"),
			}
		},
		"fail/externalAccountKeyReference-cmpAndSwap-error": func(t *testing.T) test {
			return test{
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, string(bucket), string(externalAccountKeyIDsByProvisionerIDTable))
						assert.Equals(t, provID, string(key))
						b, _ := json.Marshal([]string{})
						return b, nil
					},
					MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
						switch string(bucket) {
						case string(externalAccountKeyIDsByProvisionerIDTable):
							assert.Equals(t, provID, string(key))
							return nu, true, nil
						case string(externalAccountKeyIDsByReferenceTable):
							assert.Equals(t, provID+"."+ref, string(key))
							assert.Equals(t, old, nil)
							return nu, true, errors.New("force")
						case string(externalAccountKeyTable):
							assert.Equals(t, old, nil)
							return nu, true, nil
						default:
							assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
							return nil, false, errors.New("force default")
						}
					},
				},
				err: errors.New("error saving acme external_account_key_reference: force"),
			}
		},
	}
	for name, run := range tests {
		tc := run(t)
		t.Run(name, func(t *testing.T) {
			d := DB{db: tc.db}
			eak, err := d.CreateExternalAccountKey(context.Background(), provID, ref)
			if err != nil {
				if assert.NotNil(t, tc.err) {
					assert.Equals(t, err.Error(), tc.err.Error())
				}
			} else if assert.Nil(t, tc.err) {
				assert.Equals(t, *tc._id, eak.ID)
				assert.Equals(t, provID, eak.ProvisionerID)
				assert.Equals(t, ref, eak.Reference)
				assert.Equals(t, "", eak.AccountID)
				assert.False(t, eak.CreatedAt.IsZero())
				assert.False(t, eak.AlreadyBound())
				assert.True(t, eak.BoundAt.IsZero())
			}
		})
	}
}

func TestDB_UpdateExternalAccountKey(t *testing.T) {
	keyID := "keyID"
	provID := "provID"
	ref := "ref"
	now := clock.Now()
	dbeak := &dbExternalAccountKey{
		ID:            keyID,
		ProvisionerID: provID,
		Reference:     ref,
		AccountID:     "",
		HmacKey:       []byte{1, 3, 3, 7},
		CreatedAt:     now,
	}
	b, err := json.Marshal(dbeak)
	assert.FatalError(t, err)
	type test struct {
		db  nosql.DB
		eak *acme.ExternalAccountKey
		err error
	}
	var tests = map[string]func(t *testing.T) test{

		"ok": func(t *testing.T) test {
			eak := &acme.ExternalAccountKey{
				ID:            keyID,
				ProvisionerID: provID,
				Reference:     ref,
				AccountID:     "",
				HmacKey:       []byte{1, 3, 3, 7},
				CreatedAt:     now,
			}
			return test{
				eak: eak,
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, externalAccountKeyTable)
						assert.Equals(t, string(key), keyID)

						return b, nil
					},
					MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
						assert.Equals(t, bucket, externalAccountKeyTable)
						assert.Equals(t, old, b)

						dbNew := new(dbExternalAccountKey)
						assert.FatalError(t, json.Unmarshal(nu, dbNew))
						assert.Equals(t, dbNew.ID, dbeak.ID)
						assert.Equals(t, dbNew.ProvisionerID, dbeak.ProvisionerID)
						assert.Equals(t, dbNew.Reference, dbeak.Reference)
						assert.Equals(t, dbNew.AccountID, dbeak.AccountID)
						assert.Equals(t, dbNew.CreatedAt, dbeak.CreatedAt)
						assert.Equals(t, dbNew.BoundAt, dbeak.BoundAt)
						assert.Equals(t, dbNew.HmacKey, dbeak.HmacKey)
						return nu, true, nil
					},
				},
			}
		},
		"fail/db.Get-error": func(t *testing.T) test {
			return test{
				eak: &acme.ExternalAccountKey{
					ID: keyID,
				},
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, externalAccountKeyTable)
						assert.Equals(t, string(key), keyID)

						return nil, errors.New("force")
					},
				},
				err: errors.New("error loading external account key keyID: force"),
			}
		},
		"fail/provisioner-mismatch": func(t *testing.T) test {
			newDBEAK := &dbExternalAccountKey{
				ID:            keyID,
				ProvisionerID: "aDifferentProvID",
				Reference:     ref,
				AccountID:     "",
				HmacKey:       []byte{1, 3, 3, 7},
				CreatedAt:     now,
			}
			b, err := json.Marshal(newDBEAK)
			assert.FatalError(t, err)
			return test{
				eak: &acme.ExternalAccountKey{
					ID: keyID,
				},
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, externalAccountKeyTable)
						assert.Equals(t, string(key), keyID)

						return b, nil
					},
				},
				err: errors.New("provisioner does not match provisioner for which the EAB key was created"),
			}
		},
		"fail/provisioner-change": func(t *testing.T) test {
			newDBEAK := &dbExternalAccountKey{
				ID:            keyID,
				ProvisionerID: provID,
				Reference:     ref,
				AccountID:     "",
				HmacKey:       []byte{1, 3, 3, 7},
				CreatedAt:     now,
			}
			b, err := json.Marshal(newDBEAK)
			assert.FatalError(t, err)
			return test{
				eak: &acme.ExternalAccountKey{
					ID:            keyID,
					ProvisionerID: "aDifferentProvisionerID",
				},
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, externalAccountKeyTable)
						assert.Equals(t, string(key), keyID)
						return b, nil
					},
				},
				err: errors.New("cannot change provisioner for an existing ACME EAB Key"),
			}
		},
		"fail/reference-change": func(t *testing.T) test {
			newDBEAK := &dbExternalAccountKey{
				ID:            keyID,
				ProvisionerID: provID,
				Reference:     ref,
				AccountID:     "",
				HmacKey:       []byte{1, 3, 3, 7},
				CreatedAt:     now,
			}
			b, err := json.Marshal(newDBEAK)
			assert.FatalError(t, err)
			return test{
				eak: &acme.ExternalAccountKey{
					ID:            keyID,
					ProvisionerID: provID,
					Reference:     "aDifferentReference",
				},
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, externalAccountKeyTable)
						assert.Equals(t, string(key), keyID)
						return b, nil
					},
				},
				err: errors.New("cannot change reference for an existing ACME EAB Key"),
			}
		},
	}
	for name, run := range tests {
		tc := run(t)
		t.Run(name, func(t *testing.T) {
			d := DB{db: tc.db}
			if err := d.UpdateExternalAccountKey(context.Background(), provID, tc.eak); err != nil {
				if assert.NotNil(t, tc.err) {
					assert.HasPrefix(t, err.Error(), tc.err.Error())
				}
			} else if assert.Nil(t, tc.err) {
				assert.Equals(t, dbeak.ID, tc.eak.ID)
				assert.Equals(t, dbeak.ProvisionerID, tc.eak.ProvisionerID)
				assert.Equals(t, dbeak.Reference, tc.eak.Reference)
				assert.Equals(t, dbeak.AccountID, tc.eak.AccountID)
				assert.Equals(t, dbeak.CreatedAt, tc.eak.CreatedAt)
				assert.Equals(t, dbeak.BoundAt, tc.eak.BoundAt)
				assert.Equals(t, dbeak.HmacKey, tc.eak.HmacKey)
			}
		})
	}
}

func TestDB_addEAKID(t *testing.T) {
	provID := "provID"
	eakID := "eakID"
	type test struct {
		ctx           context.Context
		provisionerID string
		eakID         string
		db            nosql.DB
		err           error
	}
	var tests = map[string]func(t *testing.T) test{
		"fail/empty-eakID": func(t *testing.T) test {
			return test{
				ctx:           context.Background(),
				provisionerID: provID,
				eakID:         "",
				err:           errors.New("can't add empty eakID for provisioner provID"),
			}
		},
		"fail/db.Get": func(t *testing.T) test {
			return test{
				ctx:           context.Background(),
				provisionerID: provID,
				eakID:         eakID,
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)
						assert.Equals(t, string(key), provID)
						return nil, errors.New("force")
					},
				},
				err: errors.New("error loading eakIDs for provisioner provID: force"),
			}
		},
		"fail/unmarshal": func(t *testing.T) test {
			return test{
				ctx:           context.Background(),
				provisionerID: provID,
				eakID:         eakID,
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)
						assert.Equals(t, string(key), provID)
						b, _ := json.Marshal(1)
						return b, nil
					},
				},
				err: errors.New("error unmarshaling eakIDs for provisioner provID: json: cannot unmarshal number into Go value of type []string"),
			}
		},
		"fail/eakID-already-exists": func(t *testing.T) test {
			return test{
				ctx:           context.Background(),
				provisionerID: provID,
				eakID:         eakID,
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)
						assert.Equals(t, string(key), provID)
						b, _ := json.Marshal([]string{eakID})
						return b, nil
					},
				},
				err: errors.New("eakID eakID already exists for provisioner provID"),
			}
		},
		"fail/db.save": func(t *testing.T) test {
			return test{
				ctx:           context.Background(),
				provisionerID: provID,
				eakID:         eakID,
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)
						assert.Equals(t, string(key), provID)
						b, _ := json.Marshal([]string{"id1"})
						return b, nil
					},
					MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
						assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)
						assert.Equals(t, string(key), provID)
						oldB, _ := json.Marshal([]string{"id1"})
						assert.Equals(t, old, oldB)
						newB, _ := json.Marshal([]string{"id1", eakID})
						assert.Equals(t, nu, newB)
						return newB, true, errors.New("force")
					},
				},
				err: errors.New("error saving eakIDs index for provisioner provID: error saving acme externalAccountKeyIDsByProvisionerID: force"),
			}
		},
		"ok/db.Get-not-found": func(t *testing.T) test {
			return test{
				ctx:           context.Background(),
				provisionerID: provID,
				eakID:         eakID,
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)
						assert.Equals(t, string(key), provID)
						return nil, nosqldb.ErrNotFound
					},
					MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
						assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)
						assert.Equals(t, string(key), provID)
						assert.Equals(t, old, nil)
						b, _ := json.Marshal([]string{eakID})
						assert.Equals(t, nu, b)
						return b, true, nil
					},
				},
				err: nil,
			}
		},
		"ok": func(t *testing.T) test {
			return test{
				ctx:           context.Background(),
				provisionerID: provID,
				eakID:         eakID,
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)
						assert.Equals(t, string(key), provID)
						b, _ := json.Marshal([]string{"id1", "id2"})
						return b, nil
					},
					MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
						assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)
						assert.Equals(t, string(key), provID)
						oldB, _ := json.Marshal([]string{"id1", "id2"})
						assert.Equals(t, old, oldB)
						newB, _ := json.Marshal([]string{"id1", "id2", eakID})
						assert.Equals(t, nu, newB)
						return newB, true, nil
					},
				},
				err: nil,
			}
		},
	}
	for name, run := range tests {
		tc := run(t)
		t.Run(name, func(t *testing.T) {
			db := &DB{
				db: tc.db,
			}
			wantErr := tc.err != nil
			err := db.addEAKID(tc.ctx, tc.provisionerID, tc.eakID)
			if (err != nil) != wantErr {
				t.Errorf("DB.addEAKID() error = %v, wantErr %v", err, wantErr)
			}
			if err != nil {
				assert.Equals(t, tc.err.Error(), err.Error())
			}
		})
	}
}

func TestDB_deleteEAKID(t *testing.T) {
	provID := "provID"
	eakID := "eakID"
	type test struct {
		ctx           context.Context
		provisionerID string
		eakID         string
		db            nosql.DB
		err           error
	}
	var tests = map[string]func(t *testing.T) test{
		"fail/db.Get": func(t *testing.T) test {
			return test{
				ctx:           context.Background(),
				provisionerID: provID,
				eakID:         eakID,
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)
						assert.Equals(t, string(key), provID)
						return nil, errors.New("force")
					},
				},
				err: errors.New("error loading eakIDs for provisioner provID: force"),
			}
		},
		"fail/unmarshal": func(t *testing.T) test {
			return test{
				ctx:           context.Background(),
				provisionerID: provID,
				eakID:         eakID,
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)
						assert.Equals(t, string(key), provID)
						b, _ := json.Marshal(1)
						return b, nil
					},
				},
				err: errors.New("error unmarshaling eakIDs for provisioner provID: json: cannot unmarshal number into Go value of type []string"),
			}
		},
		"fail/db.save": func(t *testing.T) test {
			return test{
				ctx:           context.Background(),
				provisionerID: provID,
				eakID:         eakID,
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)
						assert.Equals(t, string(key), provID)
						b, _ := json.Marshal([]string{"id1", eakID})
						return b, nil
					},
					MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
						assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)
						assert.Equals(t, string(key), provID)
						oldB, _ := json.Marshal([]string{"id1", eakID})
						assert.Equals(t, old, oldB)
						newB, _ := json.Marshal([]string{"id1"})
						assert.Equals(t, nu, newB)
						return newB, true, errors.New("force")
					},
				},
				err: errors.New("error saving eakIDs index for provisioner provID: error saving acme externalAccountKeyIDsByProvisionerID: force"),
			}
		},
		"ok/db.Get-not-found": func(t *testing.T) test {
			return test{
				ctx:           context.Background(),
				provisionerID: provID,
				eakID:         eakID,
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)
						assert.Equals(t, string(key), provID)
						return nil, nosqldb.ErrNotFound
					},
					MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
						assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)
						assert.Equals(t, string(key), provID)
						assert.Equals(t, old, nil)
						b, _ := json.Marshal([]string{})
						assert.Equals(t, nu, b)
						return b, true, nil
					},
				},
				err: nil,
			}
		},
		"ok": func(t *testing.T) test {
			return test{
				ctx:           context.Background(),
				provisionerID: provID,
				eakID:         eakID,
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)
						assert.Equals(t, string(key), provID)
						b, _ := json.Marshal([]string{"id1", eakID, "id2"})
						return b, nil
					},
					MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
						assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)
						assert.Equals(t, string(key), provID)
						oldB, _ := json.Marshal([]string{"id1", eakID, "id2"})
						assert.Equals(t, old, oldB)
						newB, _ := json.Marshal([]string{"id1", "id2"})
						assert.Equals(t, nu, newB)
						return newB, true, nil
					},
				},
				err: nil,
			}
		},
	}
	for name, run := range tests {
		tc := run(t)
		t.Run(name, func(t *testing.T) {
			db := &DB{
				db: tc.db,
			}
			wantErr := tc.err != nil
			err := db.deleteEAKID(tc.ctx, tc.provisionerID, tc.eakID)
			if (err != nil) != wantErr {
				t.Errorf("DB.deleteEAKID() error = %v, wantErr %v", err, wantErr)
			}
			if err != nil {
				assert.Equals(t, tc.err.Error(), err.Error())
			}
		})
	}
}

func TestDB_addAndDeleteEAKID(t *testing.T) {
	provID := "provID"
	callCounter := 0
	type test struct {
		ctx context.Context
		db  nosql.DB
		err error
	}
	var tests = map[string]func(t *testing.T) test{
		"ok/multi": func(t *testing.T) test {
			return test{
				ctx: context.Background(),
				db: &certdb.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)
						assert.Equals(t, string(key), provID)
						switch callCounter {
						case 0:
							return nil, nosqldb.ErrNotFound
						case 1:
							b, _ := json.Marshal([]string{"eakID"})
							return b, nil
						case 2:
							b, _ := json.Marshal([]string{})
							return b, nil
						case 3:
							b, _ := json.Marshal([]string{"eakID1"})
							return b, nil
						case 4:
							b, _ := json.Marshal([]string{"eakID1", "eakID2"})
							return b, nil
						case 5:
							b, _ := json.Marshal([]string{"eakID2"})
							return b, nil
						default:
							assert.FatalError(t, errors.New("unexpected get iteration"))
							return nil, errors.New("force get default")
						}
					},
					MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
						assert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)
						assert.Equals(t, string(key), provID)
						switch callCounter {
						case 0:
							assert.Equals(t, old, nil)
							newB, _ := json.Marshal([]string{"eakID"})
							assert.Equals(t, nu, newB)
							return newB, true, nil
						case 1:
							oldB, _ := json.Marshal([]string{"eakID"})
							assert.Equals(t, old, oldB)
							newB, _ := json.Marshal([]string{})
							return newB, true, nil
						case 2:
							assert.Equals(t, old, nil)
							newB, _ := json.Marshal([]string{"eakID1"})
							assert.Equals(t, nu, newB)
							return newB, true, nil
						case 3:
							oldB, _ := json.Marshal([]string{"eakID1"})
							assert.Equals(t, old, oldB)
							newB, _ := json.Marshal([]string{"eakID1", "eakID2"})
							assert.Equals(t, nu, newB)
							return newB, true, nil
						case 4:
							oldB, _ := json.Marshal([]string{"eakID1", "eakID2"})
							assert.Equals(t, old, oldB)
							newB, _ := json.Marshal([]string{"eakID2"})
							assert.Equals(t, nu, newB)
							return newB, true, nil
						case 5:
							oldB, _ := json.Marshal([]string{"eakID2"})
							assert.Equals(t, old, oldB)
							newB, _ := json.Marshal([]string{})
							assert.Equals(t, nu, newB)
							return newB, true, nil
						default:
							assert.FatalError(t, errors.New("unexpected get iteration"))
							return nil, true, errors.New("force save default")
						}
					},
				},
				err: nil,
			}
		},
	}
	for name, run := range tests {
		tc := run(t)
		t.Run(name, func(t *testing.T) {

			// goal of this test is to simulate multiple calls; no errors expected.

			db := &DB{
				db: tc.db,
			}

			err := db.addEAKID(tc.ctx, provID, "eakID")
			if err != nil {
				t.Errorf("DB.addEAKID() error = %v", err)
			}

			callCounter++
			err = db.deleteEAKID(tc.ctx, provID, "eakID")
			if err != nil {
				t.Errorf("DB.deleteEAKID() error = %v", err)
			}

			callCounter++
			err = db.addEAKID(tc.ctx, provID, "eakID1")
			if err != nil {
				t.Errorf("DB.addEAKID() error = %v", err)
			}

			callCounter++
			err = db.addEAKID(tc.ctx, provID, "eakID2")
			if err != nil {
				t.Errorf("DB.addEAKID() error = %v", err)
			}

			callCounter++
			err = db.deleteEAKID(tc.ctx, provID, "eakID1")
			if err != nil {
				t.Errorf("DB.deleteEAKID() error = %v", err)
			}

			callCounter++
			err = db.deleteEAKID(tc.ctx, provID, "eakID2")
			if err != nil {
				t.Errorf("DB.deleteAKID() error = %v", err)
			}
		})
	}
}

func Test_removeElement(t *testing.T) {
	tests := []struct {
		name  string
		slice []string
		item  string
		want  []string
	}{
		{
			name:  "remove-first",
			slice: []string{"id1", "id2", "id3"},
			item:  "id1",
			want:  []string{"id2", "id3"},
		},
		{
			name:  "remove-last",
			slice: []string{"id1", "id2", "id3"},
			item:  "id3",
			want:  []string{"id1", "id2"},
		},
		{
			name:  "remove-middle",
			slice: []string{"id1", "id2", "id3"},
			item:  "id2",
			want:  []string{"id1", "id3"},
		},
		{
			name:  "remove-non-existing",
			slice: []string{"id1", "id2", "id3"},
			item:  "none",
			want:  []string{"id1", "id2", "id3"},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := removeElement(tt.slice, tt.item)
			if !cmp.Equal(tt.want, got) {
				t.Errorf("removeElement() diff =\n %s", cmp.Diff(tt.want, got))
			}
		})
	}
}
