package acme

import (
	"context"
	"encoding/json"
	"fmt"
	"net/url"
	"testing"
	"time"

	"github.com/pkg/errors"
	"github.com/smallstep/assert"
	"github.com/smallstep/certificates/authority/provisioner"
	"github.com/smallstep/certificates/db"
	"github.com/smallstep/cli/jose"
	"github.com/smallstep/nosql"
	"github.com/smallstep/nosql/database"
)

var (
	defaultDisableRenewal   = false
	globalProvisionerClaims = provisioner.Claims{
		MinTLSDur:      &provisioner.Duration{Duration: 5 * time.Minute},
		MaxTLSDur:      &provisioner.Duration{Duration: 24 * time.Hour},
		DefaultTLSDur:  &provisioner.Duration{Duration: 24 * time.Hour},
		DisableRenewal: &defaultDisableRenewal,
	}
)

func newProv() Provisioner {
	// Initialize provisioners
	p := &provisioner.ACME{
		Type: "ACME",
		Name: "test@acme-provisioner.com",
	}
	if err := p.Init(provisioner.Config{Claims: globalProvisionerClaims}); err != nil {
		fmt.Printf("%v", err)
	}
	return p
}

func newAcc() (*account, error) {
	jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
	if err != nil {
		return nil, err
	}
	mockdb := &db.MockNoSQLDB{
		MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
			return nil, true, nil
		},
	}
	return newAccount(mockdb, AccountOptions{
		Key: jwk, Contact: []string{"foo", "bar"},
	})
}

func TestGetAccountByID(t *testing.T) {
	type test struct {
		id  string
		db  nosql.DB
		acc *account
		err *Error
	}
	tests := map[string]func(t *testing.T) test{
		"fail/not-found": func(t *testing.T) test {
			acc, err := newAcc()
			assert.FatalError(t, err)
			return test{
				acc: acc,
				id:  acc.ID,
				db: &db.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						return nil, database.ErrNotFound
					},
				},
				err: MalformedErr(errors.Errorf("account %s not found: not found", acc.ID)),
			}
		},
		"fail/db-error": func(t *testing.T) test {
			acc, err := newAcc()
			assert.FatalError(t, err)
			return test{
				acc: acc,
				id:  acc.ID,
				db: &db.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						return nil, errors.New("force")
					},
				},
				err: ServerInternalErr(errors.Errorf("error loading account %s: force", acc.ID)),
			}
		},
		"fail/unmarshal-error": func(t *testing.T) test {
			acc, err := newAcc()
			assert.FatalError(t, err)
			return test{
				acc: acc,
				id:  acc.ID,
				db: &db.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, accountTable)
						assert.Equals(t, key, []byte(acc.ID))
						return nil, nil
					},
				},
				err: ServerInternalErr(errors.New("error unmarshaling account: unexpected end of JSON input")),
			}
		},
		"ok": func(t *testing.T) test {
			acc, err := newAcc()
			assert.FatalError(t, err)
			b, err := json.Marshal(acc)
			assert.FatalError(t, err)
			return test{
				acc: acc,
				id:  acc.ID,
				db: &db.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, accountTable)
						assert.Equals(t, key, []byte(acc.ID))
						return b, nil
					},
				},
			}
		},
	}
	for name, run := range tests {
		t.Run(name, func(t *testing.T) {
			tc := run(t)
			if acc, err := getAccountByID(tc.db, tc.id); err != nil {
				if assert.NotNil(t, tc.err) {
					ae, ok := err.(*Error)
					assert.True(t, ok)
					assert.HasPrefix(t, ae.Error(), tc.err.Error())
					assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
					assert.Equals(t, ae.Type, tc.err.Type)
				}
			} else {
				if assert.Nil(t, tc.err) {
					assert.Equals(t, tc.acc.ID, acc.ID)
					assert.Equals(t, tc.acc.Status, acc.Status)
					assert.Equals(t, tc.acc.Created, acc.Created)
					assert.Equals(t, tc.acc.Deactivated, acc.Deactivated)
					assert.Equals(t, tc.acc.Contact, acc.Contact)
					assert.Equals(t, tc.acc.Key.KeyID, acc.Key.KeyID)
				}
			}
		})
	}
}

func TestGetAccountByKeyID(t *testing.T) {
	type test struct {
		kid string
		db  nosql.DB
		acc *account
		err *Error
	}
	tests := map[string]func(t *testing.T) test{
		"fail/kid-not-found": func(t *testing.T) test {
			return test{
				kid: "foo",
				db: &db.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						return nil, database.ErrNotFound
					},
				},
				err: MalformedErr(errors.Errorf("account with key id foo not found: not found")),
			}
		},
		"fail/db-error": func(t *testing.T) test {
			return test{
				kid: "foo",
				db: &db.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						return nil, errors.New("force")
					},
				},
				err: ServerInternalErr(errors.New("error loading key-account index: force")),
			}
		},
		"fail/getAccount-error": func(t *testing.T) test {
			count := 0
			return test{
				kid: "foo",
				db: &db.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						if count == 0 {
							assert.Equals(t, bucket, accountByKeyIDTable)
							assert.Equals(t, key, []byte("foo"))
							count++
							return []byte("bar"), nil
						}
						return nil, errors.New("force")
					},
				},
				err: ServerInternalErr(errors.New("error loading account bar: force")),
			}
		},
		"ok": func(t *testing.T) test {
			acc, err := newAcc()
			assert.FatalError(t, err)
			b, err := json.Marshal(acc)
			assert.FatalError(t, err)
			count := 0
			return test{
				kid: acc.Key.KeyID,
				db: &db.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						var ret []byte
						switch count {
						case 0:
							assert.Equals(t, bucket, accountByKeyIDTable)
							assert.Equals(t, key, []byte(acc.Key.KeyID))
							ret = []byte(acc.ID)
						case 1:
							assert.Equals(t, bucket, accountTable)
							assert.Equals(t, key, []byte(acc.ID))
							ret = b
						}
						count++
						return ret, nil
					},
				},
				acc: acc,
			}
		},
	}
	for name, run := range tests {
		t.Run(name, func(t *testing.T) {
			tc := run(t)
			if acc, err := getAccountByKeyID(tc.db, tc.kid); err != nil {
				if assert.NotNil(t, tc.err) {
					ae, ok := err.(*Error)
					assert.True(t, ok)
					assert.HasPrefix(t, ae.Error(), tc.err.Error())
					assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
					assert.Equals(t, ae.Type, tc.err.Type)
				}
			} else {
				if assert.Nil(t, tc.err) {
					assert.Equals(t, tc.acc.ID, acc.ID)
					assert.Equals(t, tc.acc.Status, acc.Status)
					assert.Equals(t, tc.acc.Created, acc.Created)
					assert.Equals(t, tc.acc.Deactivated, acc.Deactivated)
					assert.Equals(t, tc.acc.Contact, acc.Contact)
					assert.Equals(t, tc.acc.Key.KeyID, acc.Key.KeyID)
				}
			}
		})
	}
}

func Test_getOrderIDsByAccount(t *testing.T) {
	type test struct {
		id  string
		db  nosql.DB
		res []string
		err *Error
	}
	tests := map[string]func(t *testing.T) test{
		"ok/not-found": func(t *testing.T) test {
			return test{
				id: "foo",
				db: &db.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						return nil, database.ErrNotFound
					},
				},
				res: []string{},
			}
		},
		"fail/db-error": func(t *testing.T) test {
			return test{
				id: "foo",
				db: &db.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						return nil, errors.New("force")
					},
				},
				err: ServerInternalErr(errors.New("error loading orderIDs for account foo: force")),
			}
		},
		"fail/unmarshal-error": func(t *testing.T) test {
			return test{
				id: "foo",
				db: &db.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						assert.Equals(t, bucket, ordersByAccountIDTable)
						assert.Equals(t, key, []byte("foo"))
						return nil, nil
					},
				},
				err: ServerInternalErr(errors.New("error unmarshaling orderIDs for account foo: unexpected end of JSON input")),
			}
		},
		"fail/error-loading-order-from-order-IDs": func(t *testing.T) test {
			oids := []string{"o1", "o2", "o3"}
			boids, err := json.Marshal(oids)
			assert.FatalError(t, err)
			dbHit := 0
			return test{
				id: "foo",
				db: &db.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						dbHit++
						switch dbHit {
						case 1:
							assert.Equals(t, bucket, ordersByAccountIDTable)
							assert.Equals(t, key, []byte("foo"))
							return boids, nil
						case 2:
							assert.Equals(t, bucket, orderTable)
							assert.Equals(t, key, []byte("o1"))
							return nil, errors.New("force")
						default:
							assert.FatalError(t, errors.New("should not be here"))
							return nil, nil
						}
					},
				},
				err: ServerInternalErr(errors.New("error loading order o1 for account foo: error loading order o1: force")),
			}
		},
		"fail/error-updating-order-from-order-IDs": func(t *testing.T) test {
			oids := []string{"o1", "o2", "o3"}
			boids, err := json.Marshal(oids)
			assert.FatalError(t, err)

			o, err := newO()
			assert.FatalError(t, err)
			bo, err := json.Marshal(o)
			assert.FatalError(t, err)

			dbHit := 0
			return test{
				id: "foo",
				db: &db.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						dbHit++
						switch dbHit {
						case 1:
							assert.Equals(t, bucket, ordersByAccountIDTable)
							assert.Equals(t, key, []byte("foo"))
							return boids, nil
						case 2:
							assert.Equals(t, bucket, orderTable)
							assert.Equals(t, key, []byte("o1"))
							return bo, nil
						case 3:
							assert.Equals(t, bucket, authzTable)
							assert.Equals(t, key, []byte(o.Authorizations[0]))
							return nil, errors.New("force")
						default:
							assert.FatalError(t, errors.New("should not be here"))
							return nil, nil
						}
					},
				},
				err: ServerInternalErr(errors.Errorf("error updating order o1 for account foo: error loading authz %s: force", o.Authorizations[0])),
			}
		},
		"ok/no-change-to-pending-orders": func(t *testing.T) test {
			oids := []string{"o1", "o2", "o3"}
			boids, err := json.Marshal(oids)
			assert.FatalError(t, err)

			o, err := newO()
			assert.FatalError(t, err)
			bo, err := json.Marshal(o)
			assert.FatalError(t, err)

			az, err := newAz()
			assert.FatalError(t, err)
			baz, err := json.Marshal(az)
			assert.FatalError(t, err)

			ch, err := newDNSCh()
			assert.FatalError(t, err)
			bch, err := json.Marshal(ch)
			assert.FatalError(t, err)

			return test{
				id: "foo",
				db: &db.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						switch string(bucket) {
						case string(ordersByAccountIDTable):
							assert.Equals(t, key, []byte("foo"))
							return boids, nil
						case string(orderTable):
							return bo, nil
						case string(authzTable):
							return baz, nil
						case string(challengeTable):
							return bch, nil
						default:
							assert.FatalError(t, errors.Errorf("did not expect query to table %s", bucket))
							return nil, nil
						}
					},
					MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
						return nil, false, errors.New("should not be attempting to store anything")
					},
				},
				res: oids,
			}
		},
		"fail/error-storing-new-oids": func(t *testing.T) test {
			oids := []string{"o1", "o2", "o3"}
			boids, err := json.Marshal(oids)
			assert.FatalError(t, err)

			o, err := newO()
			assert.FatalError(t, err)
			bo, err := json.Marshal(o)
			assert.FatalError(t, err)

			invalidOrder, err := newO()
			assert.FatalError(t, err)
			invalidOrder.Status = StatusInvalid
			binvalidOrder, err := json.Marshal(invalidOrder)
			assert.FatalError(t, err)

			az, err := newAz()
			assert.FatalError(t, err)
			baz, err := json.Marshal(az)
			assert.FatalError(t, err)

			ch, err := newDNSCh()
			assert.FatalError(t, err)
			bch, err := json.Marshal(ch)
			assert.FatalError(t, err)

			dbGetOrder := 0
			return test{
				id: "foo",
				db: &db.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						switch string(bucket) {
						case string(ordersByAccountIDTable):
							assert.Equals(t, key, []byte("foo"))
							return boids, nil
						case string(orderTable):
							dbGetOrder++
							if dbGetOrder == 1 {
								return binvalidOrder, nil
							}
							return bo, nil
						case string(authzTable):
							return baz, nil
						case string(challengeTable):
							return bch, nil
						default:
							assert.FatalError(t, errors.Errorf("did not expect query to table %s", bucket))
							return nil, nil
						}
					},
					MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
						assert.Equals(t, bucket, ordersByAccountIDTable)
						assert.Equals(t, key, []byte("foo"))
						return nil, false, errors.New("force")
					},
				},
				err: ServerInternalErr(errors.New("error storing orderIDs as part of getOrderIDsByAccount logic: len(orderIDs) = 2: error storing order IDs for account foo: force")),
			}
		},
		"ok": func(t *testing.T) test {
			oids := []string{"o1", "o2", "o3", "o4"}
			boids, err := json.Marshal(oids)
			assert.FatalError(t, err)

			o, err := newO()
			assert.FatalError(t, err)
			bo, err := json.Marshal(o)
			assert.FatalError(t, err)

			invalidOrder, err := newO()
			assert.FatalError(t, err)
			invalidOrder.Status = StatusInvalid
			binvalidOrder, err := json.Marshal(invalidOrder)
			assert.FatalError(t, err)

			az, err := newAz()
			assert.FatalError(t, err)
			baz, err := json.Marshal(az)
			assert.FatalError(t, err)

			ch, err := newDNSCh()
			assert.FatalError(t, err)
			bch, err := json.Marshal(ch)
			assert.FatalError(t, err)

			dbGetOrder := 0
			return test{
				id: "foo",
				db: &db.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						switch string(bucket) {
						case string(ordersByAccountIDTable):
							assert.Equals(t, key, []byte("foo"))
							return boids, nil
						case string(orderTable):
							dbGetOrder++
							if dbGetOrder == 1 || dbGetOrder == 3 {
								return binvalidOrder, nil
							}
							return bo, nil
						case string(authzTable):
							return baz, nil
						case string(challengeTable):
							return bch, nil
						default:
							assert.FatalError(t, errors.Errorf("did not expect query to table %s", bucket))
							return nil, nil
						}
					},
					MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
						assert.Equals(t, bucket, ordersByAccountIDTable)
						assert.Equals(t, key, []byte("foo"))
						return nil, true, nil
					},
				},
				res: []string{"o2", "o4"},
			}
		},
		"ok/no-pending-orders": func(t *testing.T) test {
			oids := []string{"o1"}
			boids, err := json.Marshal(oids)
			assert.FatalError(t, err)

			invalidOrder, err := newO()
			assert.FatalError(t, err)
			invalidOrder.Status = StatusInvalid
			binvalidOrder, err := json.Marshal(invalidOrder)
			assert.FatalError(t, err)

			return test{
				id: "foo",
				db: &db.MockNoSQLDB{
					MGet: func(bucket, key []byte) ([]byte, error) {
						switch string(bucket) {
						case string(ordersByAccountIDTable):
							assert.Equals(t, key, []byte("foo"))
							return boids, nil
						case string(orderTable):
							return binvalidOrder, nil
						default:
							assert.FatalError(t, errors.Errorf("did not expect query to table %s", bucket))
							return nil, nil
						}
					},
					MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
						assert.Equals(t, bucket, ordersByAccountIDTable)
						assert.Equals(t, key, []byte("foo"))
						assert.Equals(t, old, boids)
						assert.Nil(t, newval)
						return nil, true, nil
					},
				},
				res: []string{},
			}
		},
	}
	for name, run := range tests {
		t.Run(name, func(t *testing.T) {
			tc := run(t)
			if oids, err := getOrderIDsByAccount(tc.db, tc.id); err != nil {
				if assert.NotNil(t, tc.err) {
					ae, ok := err.(*Error)
					assert.True(t, ok)
					assert.HasPrefix(t, ae.Error(), tc.err.Error())
					assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
					assert.Equals(t, ae.Type, tc.err.Type)
				}
			} else {
				if assert.Nil(t, tc.err) {
					assert.Equals(t, tc.res, oids)
				}
			}
		})
	}
}

func TestAccountToACME(t *testing.T) {
	dir := newDirectory("ca.smallstep.com", "acme")
	prov := newProv()
	provName := url.PathEscape(prov.GetName())
	baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
	ctx := context.WithValue(context.Background(), ProvisionerContextKey, prov)
	ctx = context.WithValue(ctx, BaseURLContextKey, baseURL)

	type test struct {
		acc *account
		err *Error
	}
	tests := map[string]func(t *testing.T) test{
		"ok": func(t *testing.T) test {
			acc, err := newAcc()
			assert.FatalError(t, err)
			return test{acc: acc}
		},
	}
	for name, run := range tests {
		tc := run(t)
		t.Run(name, func(t *testing.T) {
			acmeAccount, err := tc.acc.toACME(ctx, nil, dir)
			if err != nil {
				if assert.NotNil(t, tc.err) {
					ae, ok := err.(*Error)
					assert.True(t, ok)
					assert.HasPrefix(t, ae.Error(), tc.err.Error())
					assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
					assert.Equals(t, ae.Type, tc.err.Type)
				}
			} else {
				if assert.Nil(t, tc.err) {
					assert.Equals(t, acmeAccount.ID, tc.acc.ID)
					assert.Equals(t, acmeAccount.Status, tc.acc.Status)
					assert.Equals(t, acmeAccount.Contact, tc.acc.Contact)
					assert.Equals(t, acmeAccount.Key.KeyID, tc.acc.Key.KeyID)
					assert.Equals(t, acmeAccount.Orders,
						fmt.Sprintf("%s/acme/%s/account/%s/orders", baseURL.String(), provName, tc.acc.ID))
				}
			}
		})
	}
}

func TestAccountSave(t *testing.T) {
	type test struct {
		acc, old *account
		db       nosql.DB
		err      *Error
	}
	tests := map[string]func(t *testing.T) test{
		"fail/old-nil/swap-error": func(t *testing.T) test {
			acc, err := newAcc()
			assert.FatalError(t, err)
			return test{
				acc: acc,
				old: nil,
				db: &db.MockNoSQLDB{
					MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
						return nil, false, errors.New("force")
					},
				},
				err: ServerInternalErr(errors.New("error storing account: force")),
			}
		},
		"fail/old-nil/swap-false": func(t *testing.T) test {
			acc, err := newAcc()
			assert.FatalError(t, err)
			return test{
				acc: acc,
				old: nil,
				db: &db.MockNoSQLDB{
					MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
						return []byte("foo"), false, nil
					},
				},
				err: ServerInternalErr(errors.New("error storing account; value has changed since last read")),
			}
		},
		"ok/old-nil": func(t *testing.T) test {
			acc, err := newAcc()
			assert.FatalError(t, err)
			b, err := json.Marshal(acc)
			assert.FatalError(t, err)
			return test{
				acc: acc,
				old: nil,
				db: &db.MockNoSQLDB{
					MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
						assert.Equals(t, old, nil)
						assert.Equals(t, b, newval)
						assert.Equals(t, bucket, accountTable)
						assert.Equals(t, []byte(acc.ID), key)
						return nil, true, nil
					},
				},
			}
		},
		"ok/old-not-nil": func(t *testing.T) test {
			oldAcc, err := newAcc()
			assert.FatalError(t, err)
			acc, err := newAcc()
			assert.FatalError(t, err)

			oldb, err := json.Marshal(oldAcc)
			assert.FatalError(t, err)
			b, err := json.Marshal(acc)
			assert.FatalError(t, err)
			return test{
				acc: acc,
				old: oldAcc,
				db: &db.MockNoSQLDB{
					MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
						assert.Equals(t, old, oldb)
						assert.Equals(t, newval, b)
						assert.Equals(t, bucket, accountTable)
						assert.Equals(t, []byte(acc.ID), key)
						return []byte("foo"), true, nil
					},
				},
			}
		},
	}
	for name, run := range tests {
		t.Run(name, func(t *testing.T) {
			tc := run(t)
			if err := tc.acc.save(tc.db, tc.old); err != nil {
				if assert.NotNil(t, tc.err) {
					ae, ok := err.(*Error)
					assert.True(t, ok)
					assert.HasPrefix(t, ae.Error(), tc.err.Error())
					assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
					assert.Equals(t, ae.Type, tc.err.Type)
				}
			} else {
				assert.Nil(t, tc.err)
			}
		})
	}
}

func TestAccountSaveNew(t *testing.T) {
	type test struct {
		acc *account
		db  nosql.DB
		err *Error
	}
	tests := map[string]func(t *testing.T) test{
		"fail/keyToID-error": func(t *testing.T) test {
			acc, err := newAcc()
			assert.FatalError(t, err)
			acc.Key.Key = "foo"
			return test{
				acc: acc,
				err: ServerInternalErr(errors.New("error generating jwk thumbprint: square/go-jose: unknown key type 'string'")),
			}
		},
		"fail/swap-error": func(t *testing.T) test {
			acc, err := newAcc()
			assert.FatalError(t, err)
			kid, err := keyToID(acc.Key)
			assert.FatalError(t, err)
			return test{
				acc: acc,
				db: &db.MockNoSQLDB{
					MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
						assert.Equals(t, bucket, accountByKeyIDTable)
						assert.Equals(t, key, []byte(kid))
						assert.Equals(t, old, nil)
						assert.Equals(t, newval, []byte(acc.ID))
						return nil, false, errors.New("force")
					},
				},
				err: ServerInternalErr(errors.New("error setting key-id to account-id index: force")),
			}
		},
		"fail/swap-false": func(t *testing.T) test {
			acc, err := newAcc()
			assert.FatalError(t, err)
			kid, err := keyToID(acc.Key)
			assert.FatalError(t, err)
			return test{
				acc: acc,
				db: &db.MockNoSQLDB{
					MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
						assert.Equals(t, bucket, accountByKeyIDTable)
						assert.Equals(t, key, []byte(kid))
						assert.Equals(t, old, nil)
						assert.Equals(t, newval, []byte(acc.ID))
						return nil, false, nil
					},
				},
				err: ServerInternalErr(errors.New("key-id to account-id index already exists")),
			}
		},
		"fail/save-error": func(t *testing.T) test {
			acc, err := newAcc()
			assert.FatalError(t, err)
			kid, err := keyToID(acc.Key)
			assert.FatalError(t, err)
			b, err := json.Marshal(acc)
			assert.FatalError(t, err)
			count := 0
			return test{
				acc: acc,
				db: &db.MockNoSQLDB{
					MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
						if count == 0 {
							assert.Equals(t, bucket, accountByKeyIDTable)
							assert.Equals(t, key, []byte(kid))
							assert.Equals(t, old, nil)
							assert.Equals(t, newval, []byte(acc.ID))
							count++
							return nil, true, nil
						}
						assert.Equals(t, bucket, accountTable)
						assert.Equals(t, key, []byte(acc.ID))
						assert.Equals(t, old, nil)
						assert.Equals(t, newval, b)
						return nil, false, errors.New("force")
					},
					MDel: func(bucket, key []byte) error {
						assert.Equals(t, bucket, accountByKeyIDTable)
						assert.Equals(t, key, []byte(kid))
						return nil
					},
				},
				err: ServerInternalErr(errors.New("error storing account: force")),
			}
		},
		"ok": func(t *testing.T) test {
			acc, err := newAcc()
			assert.FatalError(t, err)
			kid, err := keyToID(acc.Key)
			assert.FatalError(t, err)
			b, err := json.Marshal(acc)
			assert.FatalError(t, err)
			count := 0
			return test{
				acc: acc,
				db: &db.MockNoSQLDB{
					MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
						if count == 0 {
							assert.Equals(t, bucket, accountByKeyIDTable)
							assert.Equals(t, key, []byte(kid))
							assert.Equals(t, old, nil)
							assert.Equals(t, newval, []byte(acc.ID))
							count++
							return nil, true, nil
						}
						assert.Equals(t, bucket, accountTable)
						assert.Equals(t, key, []byte(acc.ID))
						assert.Equals(t, old, nil)
						assert.Equals(t, newval, b)
						return nil, true, nil
					},
				},
			}
		},
	}
	for name, run := range tests {
		t.Run(name, func(t *testing.T) {
			tc := run(t)
			if err := tc.acc.saveNew(tc.db); err != nil {
				if assert.NotNil(t, tc.err) {
					ae, ok := err.(*Error)
					assert.True(t, ok)
					assert.HasPrefix(t, ae.Error(), tc.err.Error())
					assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
					assert.Equals(t, ae.Type, tc.err.Type)
				}
			} else {
				assert.Nil(t, tc.err)
			}
		})
	}
}

func TestAccountUpdate(t *testing.T) {
	type test struct {
		acc     *account
		contact []string
		db      nosql.DB
		res     []byte
		err     *Error
	}
	contact := []string{"foo", "bar"}
	tests := map[string]func(t *testing.T) test{
		"fail/save-error": func(t *testing.T) test {
			acc, err := newAcc()
			assert.FatalError(t, err)
			oldb, err := json.Marshal(acc)
			assert.FatalError(t, err)

			_acc := *acc
			clone := &_acc
			clone.Contact = contact
			b, err := json.Marshal(clone)
			assert.FatalError(t, err)
			return test{
				acc:     acc,
				contact: contact,
				db: &db.MockNoSQLDB{
					MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
						assert.Equals(t, bucket, accountTable)
						assert.Equals(t, key, []byte(acc.ID))
						assert.Equals(t, old, oldb)
						assert.Equals(t, newval, b)
						return nil, false, errors.New("force")
					},
				},
				err: ServerInternalErr(errors.New("error storing account: force")),
			}
		},
		"ok": func(t *testing.T) test {
			acc, err := newAcc()
			assert.FatalError(t, err)
			oldb, err := json.Marshal(acc)
			assert.FatalError(t, err)

			_acc := *acc
			clone := &_acc
			clone.Contact = contact
			b, err := json.Marshal(clone)
			assert.FatalError(t, err)
			return test{
				acc:     acc,
				contact: contact,
				res:     b,
				db: &db.MockNoSQLDB{
					MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
						assert.Equals(t, bucket, accountTable)
						assert.Equals(t, key, []byte(acc.ID))
						assert.Equals(t, old, oldb)
						assert.Equals(t, newval, b)
						return nil, true, nil
					},
				},
			}
		},
	}
	for name, run := range tests {
		t.Run(name, func(t *testing.T) {
			tc := run(t)
			acc, err := tc.acc.update(tc.db, tc.contact)
			if err != nil {
				if assert.NotNil(t, tc.err) {
					ae, ok := err.(*Error)
					assert.True(t, ok)
					assert.HasPrefix(t, ae.Error(), tc.err.Error())
					assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
					assert.Equals(t, ae.Type, tc.err.Type)
				}
			} else {
				if assert.Nil(t, tc.err) {
					b, err := json.Marshal(acc)
					assert.FatalError(t, err)
					assert.Equals(t, b, tc.res)
				}
			}
		})
	}
}

func TestAccountDeactivate(t *testing.T) {
	type test struct {
		acc *account
		db  nosql.DB
		err *Error
	}
	tests := map[string]func(t *testing.T) test{
		"fail/save-error": func(t *testing.T) test {
			acc, err := newAcc()
			assert.FatalError(t, err)
			oldb, err := json.Marshal(acc)
			assert.FatalError(t, err)

			return test{
				acc: acc,
				db: &db.MockNoSQLDB{
					MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
						assert.Equals(t, bucket, accountTable)
						assert.Equals(t, key, []byte(acc.ID))
						assert.Equals(t, old, oldb)
						return nil, false, errors.New("force")
					},
				},
				err: ServerInternalErr(errors.New("error storing account: force")),
			}
		},
		"ok": func(t *testing.T) test {
			acc, err := newAcc()
			assert.FatalError(t, err)
			oldb, err := json.Marshal(acc)
			assert.FatalError(t, err)

			return test{
				acc: acc,
				db: &db.MockNoSQLDB{
					MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
						assert.Equals(t, bucket, accountTable)
						assert.Equals(t, key, []byte(acc.ID))
						assert.Equals(t, old, oldb)
						return nil, true, nil
					},
				},
			}
		},
	}
	for name, run := range tests {
		t.Run(name, func(t *testing.T) {
			tc := run(t)
			acc, err := tc.acc.deactivate(tc.db)
			if err != nil {
				if assert.NotNil(t, tc.err) {
					ae, ok := err.(*Error)
					assert.True(t, ok)
					assert.HasPrefix(t, ae.Error(), tc.err.Error())
					assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
					assert.Equals(t, ae.Type, tc.err.Type)
				}
			} else {
				if assert.Nil(t, tc.err) {
					assert.Equals(t, acc.ID, tc.acc.ID)
					assert.Equals(t, acc.Contact, tc.acc.Contact)
					assert.Equals(t, acc.Status, StatusDeactivated)
					assert.Equals(t, acc.Key.KeyID, tc.acc.Key.KeyID)
					assert.Equals(t, acc.Created, tc.acc.Created)

					assert.True(t, acc.Deactivated.Before(time.Now().Add(time.Minute)))
					assert.True(t, acc.Deactivated.After(time.Now().Add(-time.Minute)))
				}
			}
		})
	}
}

func TestNewAccount(t *testing.T) {
	jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
	assert.FatalError(t, err)
	kid, err := keyToID(jwk)
	assert.FatalError(t, err)
	ops := AccountOptions{
		Key:     jwk,
		Contact: []string{"foo", "bar"},
	}
	type test struct {
		ops AccountOptions
		db  nosql.DB
		err *Error
		id  *string
	}
	tests := map[string]func(t *testing.T) test{
		"fail/store-error": func(t *testing.T) test {
			return test{
				ops: ops,
				db: &db.MockNoSQLDB{
					MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
						return nil, false, errors.New("force")
					},
				},
				err: ServerInternalErr(errors.New("error setting key-id to account-id index: force")),
			}
		},
		"ok": func(t *testing.T) test {
			var _id string
			id := &_id
			count := 0
			return test{
				ops: ops,
				db: &db.MockNoSQLDB{
					MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {
						switch count {
						case 0:
							assert.Equals(t, bucket, accountByKeyIDTable)
							assert.Equals(t, key, []byte(kid))
						case 1:
							assert.Equals(t, bucket, accountTable)
							*id = string(key)
						}
						count++
						return nil, true, nil
					},
				},
				id: id,
			}
		},
	}
	for name, run := range tests {
		tc := run(t)
		t.Run(name, func(t *testing.T) {
			acc, err := newAccount(tc.db, tc.ops)
			if err != nil {
				if assert.NotNil(t, tc.err) {
					ae, ok := err.(*Error)
					assert.True(t, ok)
					assert.HasPrefix(t, ae.Error(), tc.err.Error())
					assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
					assert.Equals(t, ae.Type, tc.err.Type)
				}
			} else {
				if assert.Nil(t, tc.err) {
					assert.Equals(t, acc.ID, *tc.id)
					assert.Equals(t, acc.Status, StatusValid)
					assert.Equals(t, acc.Contact, ops.Contact)
					assert.Equals(t, acc.Key.KeyID, ops.Key.KeyID)

					assert.True(t, acc.Deactivated.IsZero())

					assert.True(t, acc.Created.Before(time.Now().UTC().Add(time.Minute)))
					assert.True(t, acc.Created.After(time.Now().UTC().Add(-1*time.Minute)))
				}
			}
		})
	}
}