package api

import (
	"bytes"
	"context"
	"crypto/x509"
	"encoding/json"
	"encoding/pem"
	"fmt"
	"io/ioutil"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/go-chi/chi"
	"github.com/pkg/errors"
	"github.com/smallstep/assert"
	"github.com/smallstep/certificates/acme"
	"github.com/smallstep/certificates/authority/provisioner"
	"github.com/smallstep/certificates/db"
	"github.com/smallstep/cli/crypto/pemutil"
	"github.com/smallstep/cli/jose"
)

type mockAcmeAuthority struct {
	deactivateAccount   func(provisioner.Interface, string) (*acme.Account, error)
	finalizeOrder       func(p provisioner.Interface, accID string, id string, csr *x509.CertificateRequest) (*acme.Order, error)
	getAccount          func(p provisioner.Interface, id string) (*acme.Account, error)
	getAccountByKey     func(provisioner.Interface, *jose.JSONWebKey) (*acme.Account, error)
	getAuthz            func(p provisioner.Interface, accID string, id string) (*acme.Authz, error)
	getCertificate      func(accID string, id string) ([]byte, error)
	getChallenge        func(p provisioner.Interface, accID string, id string) (*acme.Challenge, error)
	getDirectory        func(provisioner.Interface) *acme.Directory
	getLink             func(acme.Link, string, bool, ...string) string
	getOrder            func(p provisioner.Interface, accID string, id string) (*acme.Order, error)
	getOrdersByAccount  func(p provisioner.Interface, id string) ([]string, error)
	loadProvisionerByID func(string) (provisioner.Interface, error)
	newAccount          func(provisioner.Interface, acme.AccountOptions) (*acme.Account, error)
	newNonce            func() (string, error)
	newOrder            func(provisioner.Interface, acme.OrderOptions) (*acme.Order, error)
	updateAccount       func(provisioner.Interface, string, []string) (*acme.Account, error)
	useNonce            func(string) error
	validateChallenge   func(p provisioner.Interface, accID string, id string, jwk *jose.JSONWebKey) (*acme.Challenge, error)
	ret1                interface{}
	err                 error
}

func (m *mockAcmeAuthority) DeactivateAccount(p provisioner.Interface, id string) (*acme.Account, error) {
	if m.deactivateAccount != nil {
		return m.deactivateAccount(p, id)
	} else if m.err != nil {
		return nil, m.err
	}
	return m.ret1.(*acme.Account), m.err
}

func (m *mockAcmeAuthority) FinalizeOrder(p provisioner.Interface, accID, id string, csr *x509.CertificateRequest) (*acme.Order, error) {
	if m.finalizeOrder != nil {
		return m.finalizeOrder(p, accID, id, csr)
	} else if m.err != nil {
		return nil, m.err
	}
	return m.ret1.(*acme.Order), m.err
}

func (m *mockAcmeAuthority) GetAccount(p provisioner.Interface, id string) (*acme.Account, error) {
	if m.getAccount != nil {
		return m.getAccount(p, id)
	} else if m.err != nil {
		return nil, m.err
	}
	return m.ret1.(*acme.Account), m.err
}

func (m *mockAcmeAuthority) GetAccountByKey(p provisioner.Interface, jwk *jose.JSONWebKey) (*acme.Account, error) {
	if m.getAccountByKey != nil {
		return m.getAccountByKey(p, jwk)
	} else if m.err != nil {
		return nil, m.err
	}
	return m.ret1.(*acme.Account), m.err
}

func (m *mockAcmeAuthority) GetAuthz(p provisioner.Interface, accID, id string) (*acme.Authz, error) {
	if m.getAuthz != nil {
		return m.getAuthz(p, accID, id)
	} else if m.err != nil {
		return nil, m.err
	}
	return m.ret1.(*acme.Authz), m.err
}

func (m *mockAcmeAuthority) GetCertificate(accID, id string) ([]byte, error) {
	if m.getCertificate != nil {
		return m.getCertificate(accID, id)
	} else if m.err != nil {
		return nil, m.err
	}
	return m.ret1.([]byte), m.err
}

func (m *mockAcmeAuthority) GetChallenge(p provisioner.Interface, accID, id string) (*acme.Challenge, error) {
	if m.getChallenge != nil {
		return m.getChallenge(p, accID, id)
	} else if m.err != nil {
		return nil, m.err
	}
	return m.ret1.(*acme.Challenge), m.err
}

func (m *mockAcmeAuthority) GetDirectory(p provisioner.Interface) *acme.Directory {
	if m.getDirectory != nil {
		return m.getDirectory(p)
	}
	return m.ret1.(*acme.Directory)
}

func (m *mockAcmeAuthority) GetLink(typ acme.Link, provID string, abs bool, in ...string) string {
	if m.getLink != nil {
		return m.getLink(typ, provID, abs, in...)
	}
	return m.ret1.(string)
}

func (m *mockAcmeAuthority) GetOrder(p provisioner.Interface, accID, id string) (*acme.Order, error) {
	if m.getOrder != nil {
		return m.getOrder(p, accID, id)
	} else if m.err != nil {
		return nil, m.err
	}
	return m.ret1.(*acme.Order), m.err
}

func (m *mockAcmeAuthority) GetOrdersByAccount(p provisioner.Interface, id string) ([]string, error) {
	if m.getOrdersByAccount != nil {
		return m.getOrdersByAccount(p, id)
	} else if m.err != nil {
		return nil, m.err
	}
	return m.ret1.([]string), m.err
}

func (m *mockAcmeAuthority) LoadProvisionerByID(provID string) (provisioner.Interface, error) {
	if m.loadProvisionerByID != nil {
		return m.loadProvisionerByID(provID)
	} else if m.err != nil {
		return nil, m.err
	}
	return m.ret1.(provisioner.Interface), m.err
}

func (m *mockAcmeAuthority) NewAccount(p provisioner.Interface, ops acme.AccountOptions) (*acme.Account, error) {
	if m.newAccount != nil {
		return m.newAccount(p, ops)
	} else if m.err != nil {
		return nil, m.err
	}
	return m.ret1.(*acme.Account), m.err
}

func (m *mockAcmeAuthority) NewNonce() (string, error) {
	if m.newNonce != nil {
		return m.newNonce()
	} else if m.err != nil {
		return "", m.err
	}
	return m.ret1.(string), m.err
}

func (m *mockAcmeAuthority) NewOrder(p provisioner.Interface, ops acme.OrderOptions) (*acme.Order, error) {
	if m.newOrder != nil {
		return m.newOrder(p, ops)
	} else if m.err != nil {
		return nil, m.err
	}
	return m.ret1.(*acme.Order), m.err
}

func (m *mockAcmeAuthority) UpdateAccount(p provisioner.Interface, id string, contact []string) (*acme.Account, error) {
	if m.updateAccount != nil {
		return m.updateAccount(p, id, contact)
	} else if m.err != nil {
		return nil, m.err
	}
	return m.ret1.(*acme.Account), m.err
}

func (m *mockAcmeAuthority) UseNonce(nonce string) error {
	if m.useNonce != nil {
		return m.useNonce(nonce)
	}
	return m.err
}

func (m *mockAcmeAuthority) ValidateChallenge(p provisioner.Interface, accID string, id string, jwk *jose.JSONWebKey) (*acme.Challenge, error) {
	switch {
	case m.validateChallenge != nil:
		return m.validateChallenge(p, accID, id, jwk)
	case m.err != nil:
		return nil, m.err
	default:
		return m.ret1.(*acme.Challenge), m.err
	}
}

func TestHandlerGetNonce(t *testing.T) {
	tests := []struct {
		name       string
		statusCode int
	}{
		{"GET", 204},
		{"HEAD", 200},
	}

	// Request with chi context
	req := httptest.NewRequest("GET", "http://ca.smallstep.com/nonce", nil)

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			h := New(nil).(*Handler)
			w := httptest.NewRecorder()
			req.Method = tt.name
			h.GetNonce(w, req)
			res := w.Result()

			if res.StatusCode != tt.statusCode {
				t.Errorf("Handler.GetNonce StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
			}
		})
	}
}

func TestHandlerGetDirectory(t *testing.T) {
	auth, err := acme.NewAuthority(new(db.MockNoSQLDB), "ca.smallstep.com", "acme", nil)
	assert.FatalError(t, err)
	prov := newProv()
	url := fmt.Sprintf("http://ca.smallstep.com/acme/%s/directory", acme.URLSafeProvisionerName(prov))

	expDir := acme.Directory{
		NewNonce:   fmt.Sprintf("https://ca.smallstep.com/acme/%s/new-nonce", acme.URLSafeProvisionerName(prov)),
		NewAccount: fmt.Sprintf("https://ca.smallstep.com/acme/%s/new-account", acme.URLSafeProvisionerName(prov)),
		NewOrder:   fmt.Sprintf("https://ca.smallstep.com/acme/%s/new-order", acme.URLSafeProvisionerName(prov)),
		RevokeCert: fmt.Sprintf("https://ca.smallstep.com/acme/%s/revoke-cert", acme.URLSafeProvisionerName(prov)),
		KeyChange:  fmt.Sprintf("https://ca.smallstep.com/acme/%s/key-change", acme.URLSafeProvisionerName(prov)),
	}

	type test struct {
		ctx        context.Context
		statusCode int
		problem    *acme.Error
	}
	var tests = map[string]func(t *testing.T) test{
		"fail/no-provisioner": func(t *testing.T) test {
			return test{
				ctx:        context.Background(),
				statusCode: 500,
				problem:    acme.ServerInternalErr(errors.New("provisioner expected in request context")),
			}
		},
		"fail/nil-provisioner": func(t *testing.T) test {
			return test{
				ctx:        context.WithValue(context.Background(), provisionerContextKey, nil),
				statusCode: 500,
				problem:    acme.ServerInternalErr(errors.New("provisioner expected in request context")),
			}
		},
		"ok": func(t *testing.T) test {
			return test{
				ctx:        context.WithValue(context.Background(), provisionerContextKey, prov),
				statusCode: 200,
			}
		},
	}
	for name, run := range tests {
		tc := run(t)
		t.Run(name, func(t *testing.T) {
			h := New(auth).(*Handler)
			req := httptest.NewRequest("GET", url, nil)
			req = req.WithContext(tc.ctx)
			w := httptest.NewRecorder()
			h.GetDirectory(w, req)
			res := w.Result()

			assert.Equals(t, res.StatusCode, tc.statusCode)

			body, err := ioutil.ReadAll(res.Body)
			res.Body.Close()
			assert.FatalError(t, err)

			if res.StatusCode >= 400 && assert.NotNil(t, tc.problem) {
				var ae acme.AError
				assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
				prob := tc.problem.ToACME()

				assert.Equals(t, ae.Type, prob.Type)
				assert.Equals(t, ae.Detail, prob.Detail)
				assert.Equals(t, ae.Identifier, prob.Identifier)
				assert.Equals(t, ae.Subproblems, prob.Subproblems)
				assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
			} else {
				var dir acme.Directory
				json.Unmarshal(bytes.TrimSpace(body), &dir)
				assert.Equals(t, dir, expDir)
				assert.Equals(t, res.Header["Content-Type"], []string{"application/json"})
			}
		})
	}
}

func TestHandlerGetAuthz(t *testing.T) {
	expiry := time.Now().UTC().Add(6 * time.Hour)
	az := acme.Authz{
		ID: "authzID",
		Identifier: acme.Identifier{
			Type:  "dns",
			Value: "example.com",
		},
		Status:   "pending",
		Expires:  expiry.Format(time.RFC3339),
		Wildcard: false,
		Challenges: []*acme.Challenge{
			{
				Type:    "http-01",
				Status:  "pending",
				Token:   "tok2",
				URL:     "https://ca.smallstep.com/acme/challenge/chHTTPID",
				ID:      "chHTTP01ID",
				AuthzID: "authzID",
			},
			{
				Type:    "dns-01",
				Status:  "pending",
				Token:   "tok2",
				URL:     "https://ca.smallstep.com/acme/challenge/chDNSID",
				ID:      "chDNSID",
				AuthzID: "authzID",
			},
		},
	}
	prov := newProv()

	// Request with chi context
	chiCtx := chi.NewRouteContext()
	chiCtx.URLParams.Add("authzID", az.ID)
	url := fmt.Sprintf("http://ca.smallstep.com/acme/%s/challenge/%s",
		acme.URLSafeProvisionerName(prov), az.ID)

	type test struct {
		auth       acme.Interface
		ctx        context.Context
		statusCode int
		problem    *acme.Error
	}
	var tests = map[string]func(t *testing.T) test{
		"fail/no-provisioner": func(t *testing.T) test {
			return test{
				auth:       &mockAcmeAuthority{},
				ctx:        context.Background(),
				statusCode: 500,
				problem:    acme.ServerInternalErr(errors.New("provisioner expected in request context")),
			}
		},
		"fail/nil-provisioner": func(t *testing.T) test {
			return test{
				auth:       &mockAcmeAuthority{},
				ctx:        context.WithValue(context.Background(), provisionerContextKey, nil),
				statusCode: 500,
				problem:    acme.ServerInternalErr(errors.New("provisioner expected in request context")),
			}
		},
		"fail/no-account": func(t *testing.T) test {
			return test{
				auth:       &mockAcmeAuthority{},
				ctx:        context.WithValue(context.Background(), provisionerContextKey, prov),
				statusCode: 400,
				problem:    acme.AccountDoesNotExistErr(nil),
			}
		},
		"fail/nil-account": func(t *testing.T) test {
			ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
			ctx = context.WithValue(ctx, accContextKey, nil)
			return test{
				auth:       &mockAcmeAuthority{},
				ctx:        ctx,
				statusCode: 400,
				problem:    acme.AccountDoesNotExistErr(nil),
			}
		},
		"fail/getAuthz-error": func(t *testing.T) test {
			acc := &acme.Account{ID: "accID"}
			ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
			ctx = context.WithValue(ctx, accContextKey, acc)
			ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
			return test{
				auth: &mockAcmeAuthority{
					err: acme.ServerInternalErr(errors.New("force")),
				},
				ctx:        ctx,
				statusCode: 500,
				problem:    acme.ServerInternalErr(errors.New("force")),
			}
		},
		"ok": func(t *testing.T) test {
			acc := &acme.Account{ID: "accID"}
			ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
			ctx = context.WithValue(ctx, accContextKey, acc)
			ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
			return test{
				auth: &mockAcmeAuthority{
					getAuthz: func(p provisioner.Interface, accID, id string) (*acme.Authz, error) {
						assert.Equals(t, p, prov)
						assert.Equals(t, accID, acc.ID)
						assert.Equals(t, id, az.ID)
						return &az, nil
					},
					getLink: func(typ acme.Link, provID string, abs bool, in ...string) string {
						assert.Equals(t, provID, acme.URLSafeProvisionerName(prov))
						assert.Equals(t, typ, acme.AuthzLink)
						assert.True(t, abs)
						assert.Equals(t, in, []string{az.ID})
						return url
					},
				},
				ctx:        ctx,
				statusCode: 200,
			}
		},
	}
	for name, run := range tests {
		tc := run(t)
		t.Run(name, func(t *testing.T) {
			h := New(tc.auth).(*Handler)
			req := httptest.NewRequest("GET", url, nil)
			req = req.WithContext(tc.ctx)
			w := httptest.NewRecorder()
			h.GetAuthz(w, req)
			res := w.Result()

			assert.Equals(t, res.StatusCode, tc.statusCode)

			body, err := ioutil.ReadAll(res.Body)
			res.Body.Close()
			assert.FatalError(t, err)

			if res.StatusCode >= 400 && assert.NotNil(t, tc.problem) {
				var ae acme.AError
				assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
				prob := tc.problem.ToACME()

				assert.Equals(t, ae.Type, prob.Type)
				assert.Equals(t, ae.Detail, prob.Detail)
				assert.Equals(t, ae.Identifier, prob.Identifier)
				assert.Equals(t, ae.Subproblems, prob.Subproblems)
				assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
			} else {
				//var gotAz acme.Authz
				//assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &gotAz))
				expB, err := json.Marshal(az)
				assert.FatalError(t, err)
				assert.Equals(t, bytes.TrimSpace(body), expB)
				assert.Equals(t, res.Header["Location"], []string{url})
				assert.Equals(t, res.Header["Content-Type"], []string{"application/json"})
			}
		})
	}
}

func TestHandlerGetCertificate(t *testing.T) {
	leaf, err := pemutil.ReadCertificate("../../authority/testdata/certs/foo.crt")
	assert.FatalError(t, err)
	inter, err := pemutil.ReadCertificate("../../authority/testdata/certs/intermediate_ca.crt")
	assert.FatalError(t, err)
	root, err := pemutil.ReadCertificate("../../authority/testdata/certs/root_ca.crt")
	assert.FatalError(t, err)

	certBytes := append(pem.EncodeToMemory(&pem.Block{
		Type:  "CERTIFICATE",
		Bytes: leaf.Raw,
	}), pem.EncodeToMemory(&pem.Block{
		Type:  "CERTIFICATE",
		Bytes: inter.Raw,
	})...)
	certBytes = append(certBytes, pem.EncodeToMemory(&pem.Block{
		Type:  "CERTIFICATE",
		Bytes: root.Raw,
	})...)
	certID := "certID"

	prov := newProv()
	// Request with chi context
	chiCtx := chi.NewRouteContext()
	chiCtx.URLParams.Add("certID", certID)
	url := fmt.Sprintf("http://ca.smallstep.com/acme/%s/certificate/%s",
		acme.URLSafeProvisionerName(prov), certID)

	type test struct {
		auth       acme.Interface
		ctx        context.Context
		statusCode int
		problem    *acme.Error
	}
	var tests = map[string]func(t *testing.T) test{
		"fail/no-account": func(t *testing.T) test {
			return test{
				auth:       &mockAcmeAuthority{},
				ctx:        context.WithValue(context.Background(), provisionerContextKey, prov),
				statusCode: 400,
				problem:    acme.AccountDoesNotExistErr(nil),
			}
		},
		"fail/nil-account": func(t *testing.T) test {
			ctx := context.WithValue(context.Background(), accContextKey, nil)
			return test{
				auth:       &mockAcmeAuthority{},
				ctx:        ctx,
				statusCode: 400,
				problem:    acme.AccountDoesNotExistErr(nil),
			}
		},
		"fail/getCertificate-error": func(t *testing.T) test {
			acc := &acme.Account{ID: "accID"}
			ctx := context.WithValue(context.Background(), accContextKey, acc)
			ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
			return test{
				auth: &mockAcmeAuthority{
					err: acme.ServerInternalErr(errors.New("force")),
				},
				ctx:        ctx,
				statusCode: 500,
				problem:    acme.ServerInternalErr(errors.New("force")),
			}
		},
		"ok": func(t *testing.T) test {
			acc := &acme.Account{ID: "accID"}
			ctx := context.WithValue(context.Background(), accContextKey, acc)
			ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
			return test{
				auth: &mockAcmeAuthority{
					getCertificate: func(accID, id string) ([]byte, error) {
						assert.Equals(t, accID, acc.ID)
						assert.Equals(t, id, certID)
						return certBytes, nil
					},
				},
				ctx:        ctx,
				statusCode: 200,
			}
		},
	}
	for name, run := range tests {
		tc := run(t)
		t.Run(name, func(t *testing.T) {
			h := New(tc.auth).(*Handler)
			req := httptest.NewRequest("GET", url, nil)
			req = req.WithContext(tc.ctx)
			w := httptest.NewRecorder()
			h.GetCertificate(w, req)
			res := w.Result()

			assert.Equals(t, res.StatusCode, tc.statusCode)

			body, err := ioutil.ReadAll(res.Body)
			res.Body.Close()
			assert.FatalError(t, err)

			if res.StatusCode >= 400 && assert.NotNil(t, tc.problem) {
				var ae acme.AError
				assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
				prob := tc.problem.ToACME()

				assert.Equals(t, ae.Type, prob.Type)
				assert.Equals(t, ae.Detail, prob.Detail)
				assert.Equals(t, ae.Identifier, prob.Identifier)
				assert.Equals(t, ae.Subproblems, prob.Subproblems)
				assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
			} else {
				assert.Equals(t, bytes.TrimSpace(body), bytes.TrimSpace(certBytes))
				assert.Equals(t, res.Header["Content-Type"], []string{"application/pem-certificate-chain; charset=utf-8"})
			}
		})
	}
}

func ch() acme.Challenge {
	return acme.Challenge{
		Type:    "http-01",
		Status:  "pending",
		Token:   "tok2",
		URL:     "https://ca.smallstep.com/acme/challenge/chID",
		ID:      "chID",
		AuthzID: "authzID",
	}
}

func TestHandlerGetChallenge(t *testing.T) {
	chiCtx := chi.NewRouteContext()
	chiCtx.URLParams.Add("chID", "chID")
	url := fmt.Sprintf("http://ca.smallstep.com/acme/challenge/%s", "chID")
	prov := newProv()

	type test struct {
		auth       acme.Interface
		ctx        context.Context
		statusCode int
		ch         acme.Challenge
		problem    *acme.Error
	}
	var tests = map[string]func(t *testing.T) test{
		"fail/no-provisioner": func(t *testing.T) test {
			return test{
				ctx:        context.Background(),
				statusCode: 500,
				problem:    acme.ServerInternalErr(errors.New("provisioner expected in request context")),
			}
		},
		"fail/nil-provisioner": func(t *testing.T) test {
			return test{
				ctx:        context.WithValue(context.Background(), provisionerContextKey, nil),
				statusCode: 500,
				problem:    acme.ServerInternalErr(errors.New("provisioner expected in request context")),
			}
		},
		"fail/no-account": func(t *testing.T) test {
			return test{
				ctx:        context.WithValue(context.Background(), provisionerContextKey, prov),
				statusCode: 400,
				problem:    acme.AccountDoesNotExistErr(nil),
			}
		},
		"fail/nil-account": func(t *testing.T) test {
			ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
			ctx = context.WithValue(ctx, accContextKey, nil)
			return test{
				ctx:        ctx,
				statusCode: 400,
				problem:    acme.AccountDoesNotExistErr(nil),
			}
		},
		"fail/no-payload": func(t *testing.T) test {
			acc := &acme.Account{ID: "accID"}
			ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
			ctx = context.WithValue(ctx, accContextKey, acc)
			return test{
				ctx:        ctx,
				statusCode: 500,
				problem:    acme.ServerInternalErr(errors.New("payload expected in request context")),
			}
		},
		"fail/nil-payload": func(t *testing.T) test {
			acc := &acme.Account{ID: "accID"}
			ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
			ctx = context.WithValue(ctx, accContextKey, acc)
			ctx = context.WithValue(ctx, payloadContextKey, nil)
			return test{
				ctx:        ctx,
				statusCode: 500,
				problem:    acme.ServerInternalErr(errors.New("payload expected in request context")),
			}
		},
		"fail/validate-challenge-error": func(t *testing.T) test {
			acc := &acme.Account{ID: "accID"}
			ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
			ctx = context.WithValue(ctx, accContextKey, acc)
			ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true})
			ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
			return test{
				auth: &mockAcmeAuthority{
					err: acme.UnauthorizedErr(nil),
				},
				ctx:        ctx,
				statusCode: 401,
				problem:    acme.UnauthorizedErr(nil),
			}
		},
		"fail/get-challenge-error": func(t *testing.T) test {
			acc := &acme.Account{ID: "accID"}
			ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
			ctx = context.WithValue(ctx, accContextKey, acc)
			ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isPostAsGet: true})
			ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
			return test{
				auth: &mockAcmeAuthority{
					err: acme.UnauthorizedErr(nil),
				},
				ctx:        ctx,
				statusCode: 401,
				problem:    acme.UnauthorizedErr(nil),
			}
		},
		"ok/validate-challenge": func(t *testing.T) test {
			key, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			assert.FatalError(t, err)
			acc := &acme.Account{ID: "accID", Key: key}
			ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
			ctx = context.WithValue(ctx, accContextKey, acc)
			ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true})
			ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
			ch := ch()
			ch.Status = "valid"
			ch.Validated = time.Now().UTC().Format(time.RFC3339)
			count := 0
			return test{
				auth: &mockAcmeAuthority{
					validateChallenge: func(p provisioner.Interface, accID, id string, jwk *jose.JSONWebKey) (*acme.Challenge, error) {
						assert.Equals(t, p, prov)
						assert.Equals(t, accID, acc.ID)
						assert.Equals(t, id, ch.ID)
						assert.Equals(t, jwk.KeyID, key.KeyID)
						return &ch, nil
					},
					getLink: func(typ acme.Link, provID string, abs bool, in ...string) string {
						var ret string
						switch count {
						case 0:
							assert.Equals(t, typ, acme.AuthzLink)
							assert.Equals(t, provID, acme.URLSafeProvisionerName(prov))
							assert.True(t, abs)
							assert.Equals(t, in, []string{ch.AuthzID})
							ret = fmt.Sprintf("https://ca.smallstep.com/acme/authz/%s", ch.AuthzID)
						case 1:
							assert.Equals(t, typ, acme.ChallengeLink)
							assert.Equals(t, provID, acme.URLSafeProvisionerName(prov))
							assert.True(t, abs)
							assert.Equals(t, in, []string{ch.ID})
							ret = url
						}
						count++
						return ret
					},
				},
				ctx:        ctx,
				statusCode: 200,
				ch:         ch,
			}
		},
	}
	for name, run := range tests {
		tc := run(t)
		t.Run(name, func(t *testing.T) {
			h := New(tc.auth).(*Handler)
			req := httptest.NewRequest("GET", url, nil)
			req = req.WithContext(tc.ctx)
			w := httptest.NewRecorder()
			h.GetChallenge(w, req)
			res := w.Result()

			assert.Equals(t, res.StatusCode, tc.statusCode)

			body, err := ioutil.ReadAll(res.Body)
			res.Body.Close()
			assert.FatalError(t, err)

			if res.StatusCode >= 400 && assert.NotNil(t, tc.problem) {
				var ae acme.AError
				assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
				prob := tc.problem.ToACME()

				assert.Equals(t, ae.Type, prob.Type)
				assert.Equals(t, ae.Detail, prob.Detail)
				assert.Equals(t, ae.Identifier, prob.Identifier)
				assert.Equals(t, ae.Subproblems, prob.Subproblems)
				assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
			} else {
				expB, err := json.Marshal(tc.ch)
				assert.FatalError(t, err)
				assert.Equals(t, bytes.TrimSpace(body), expB)
				assert.Equals(t, res.Header["Link"], []string{fmt.Sprintf("<https://ca.smallstep.com/acme/authz/%s>;rel=\"up\"", tc.ch.AuthzID)})
				assert.Equals(t, res.Header["Location"], []string{url})
				assert.Equals(t, res.Header["Content-Type"], []string{"application/json"})
			}
		})
	}
}