package auth

import (
	"bytes"
	"encoding/json"
	"fmt"
	"github.com/gorilla/mux"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"net/url"
	"sort"
	"strings"
	"testing"
	"time"

	"code.secondbit.org/uuid.hg"
)

const (
	clientChangeSecret = 1 << iota
	clientChangeOwnerID
	clientChangeName
	clientChangeLogo
	clientChangeWebsite
)

var clientStores = []clientStore{NewMemstore()}

func compareClients(client1, client2 Client) (success bool, field string, val1, val2 interface{}) {
	if !client1.ID.Equal(client2.ID) {
		return false, "ID", client1.ID, client2.ID
	}
	if client1.Secret != client2.Secret {
		return false, "secret", client1.Secret, client2.Secret
	}
	if !client1.OwnerID.Equal(client2.OwnerID) {
		return false, "owner ID", client1.OwnerID, client2.OwnerID
	}
	if client1.Name != client2.Name {
		return false, "name", client1.Name, client2.Name
	}
	if client1.Logo != client2.Logo {
		return false, "logo", client1.Logo, client2.Logo
	}
	if client1.Website != client2.Website {
		return false, "website", client1.Website, client2.Website
	}
	if client1.Type != client2.Type {
		return false, "type", client1.Type, client2.Type
	}
	return true, "", nil, nil
}

func compareEndpoints(endpoint1, endpoint2 Endpoint) (success bool, field string, val1, val2 interface{}) {
	if !endpoint1.ID.Equal(endpoint2.ID) {
		return false, "ID", endpoint1.ID, endpoint2.ID
	}
	if !endpoint1.ClientID.Equal(endpoint2.ClientID) {
		return false, "OwnerID", endpoint1.ClientID, endpoint2.ClientID
	}
	if !endpoint1.Added.Equal(endpoint2.Added) {
		return false, "Added", endpoint1.Added, endpoint2.Added
	}
	if endpoint1.URI != endpoint2.URI {
		return false, "URI", endpoint1.URI, endpoint2.URI
	}
	return true, "", nil, nil
}

func TestClientStoreSuccess(t *testing.T) {
	t.Parallel()
	client := Client{
		ID:      uuid.NewID(),
		Secret:  "secret",
		OwnerID: uuid.NewID(),
		Name:    "name",
		Logo:    "logo",
		Website: "website",
	}
	for _, store := range clientStores {
		context := Context{clients: store}
		err := context.SaveClient(client)
		if err != nil {
			t.Fatalf("Error saving client to %T: %s", store, err)
		}
		err = context.SaveClient(client)
		if err != ErrClientAlreadyExists {
			t.Fatalf("Expected ErrClientAlreadyExists, got %v from %T", err, store)
		}
		retrieved, err := context.GetClient(client.ID)
		if err != nil {
			t.Fatalf("Error retrieving client from %T: %s", store, err)
		}
		success, field, expectation, result := compareClients(client, retrieved)
		if !success {
			t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
		}
		clients, err := context.ListClientsByOwner(client.OwnerID, 25, 0)
		if err != nil {
			t.Fatalf("Error retrieving clients by owner from %T: %s", store, err)
		}
		if len(clients) != 1 {
			t.Fatalf("Expected 1 client in response from %T, got %+v", store, clients)
		}
		success, field, expectation, result = compareClients(client, clients[0])
		if !success {
			t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
		}
		err = context.DeleteClient(client.ID)
		if err != nil {
			t.Fatalf("Error deleting client from %T: %s", store, err)
		}
		err = context.DeleteClient(client.ID)
		if err != ErrClientNotFound {
			t.Fatalf("Expected ErrClientNotFound, got %s from %T", err, store)
		}
		retrieved, err = context.GetClient(client.ID)
		if err != ErrClientNotFound {
			t.Fatalf("Expected ErrClientNotFound from %T, got %+v and %s", store, retrieved, err)
		}
		clients, err = context.ListClientsByOwner(client.OwnerID, 25, 0)
		if err != nil {
			t.Fatalf("Error listing clients by owner from %T: %s", store, err)
		}
		if len(clients) != 0 {
			t.Fatalf("Expected 0 clients in response from %T, got %+v", store, clients)
		}
	}
}

func TestEndpointStoreSuccess(t *testing.T) {
	t.Parallel()
	client := Client{
		ID:      uuid.NewID(),
		Secret:  "secret",
		OwnerID: uuid.NewID(),
		Name:    "name",
		Logo:    "logo",
		Website: "website",
	}
	endpoint1 := Endpoint{
		ID:       uuid.NewID(),
		ClientID: client.ID,
		Added:    time.Now().Round(time.Millisecond),
		URI:      "https://www.example.com/",
	}
	endpoint2 := Endpoint{
		ID:       uuid.NewID(),
		ClientID: client.ID,
		Added:    time.Now().Round(time.Millisecond),
		URI:      "https://www.example.com/my/full/path",
	}
	for _, store := range clientStores {
		context := Context{clients: store}
		err := context.SaveClient(client)
		if err != nil {
			t.Fatalf("Error saving client to %T: %s", store, err)
		}
		err = context.AddEndpoints(client.ID, []Endpoint{endpoint1})
		if err != nil {
			t.Fatalf("Error adding endpoint to client in %T: %s", store, err)
		}
		endpoints, err := context.ListEndpoints(client.ID, 10, 0)
		if err != nil {
			t.Fatalf("Error retrieving endpoints from %T: %s", store, err)
		}
		if len(endpoints) != 1 {
			t.Fatalf("Expected %d endpoints, got %+v from %T", 1, endpoints, store)
		}
		success, field, expectation, result := compareEndpoints(endpoint1, endpoints[0])
		if !success {
			t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
		}
		err = context.AddEndpoints(client.ID, []Endpoint{endpoint2})
		if err != nil {
			t.Fatalf("Error adding endpoint to client in %T: %s", store, err)
		}
		endpoints, err = context.ListEndpoints(client.ID, 10, 0)
		if err != nil {
			t.Fatalf("Error retrieving endpoints from %T: %s", store, err)
		}
		if len(endpoints) != 2 {
			t.Fatalf("Expected %d endpoints, got %+v from %T", 2, endpoints, store)
		}
		sortedEnd := sortedEndpoints(endpoints)
		sort.Sort(sortedEnd)
		endpoints = []Endpoint(sortedEnd)
		success, field, expectation, result = compareEndpoints(endpoint1, endpoints[0])
		if !success {
			t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
		}
		success, field, expectation, result = compareEndpoints(endpoint2, endpoints[1])
		if !success {
			t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
		}
		err = context.RemoveEndpoint(client.ID, endpoint1.ID)
		if err != nil {
			t.Fatalf("Error removing endpoint from client in %T: %s", store, err)
		}
		endpoints, err = context.ListEndpoints(client.ID, 10, 0)
		if err != nil {
			t.Fatalf("Error listing endpoints in %T: %s", store, err)
		}
		if len(endpoints) != 1 {
			t.Fatalf("Expected %d endpoints, got %+v from %T", 1, endpoints, store)
		}
		success, field, expectation, result = compareEndpoints(endpoint2, endpoints[0])
		if !success {
			t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
		}
		err = context.RemoveEndpoint(client.ID, endpoint2.ID)
		if err != nil {
			t.Fatalf("Error removing endpoint from client in %T: %s", store, err)
		}
		endpoints, err = context.ListEndpoints(client.ID, 10, 0)
		if err != nil {
			t.Fatalf("Error listing endpoints in %T: %s", store, err)
		}
		if len(endpoints) != 0 {
			t.Fatalf("Expected %d endpoints, got %+v from %T", 0, endpoints, store)
		}
	}
}

func TestClientUpdates(t *testing.T) {
	t.Parallel()
	variations := 1 << 5
	client := Client{
		ID:      uuid.NewID(),
		Secret:  "secret",
		OwnerID: uuid.NewID(),
		Name:    "name",
		Logo:    "logo",
		Website: "website",
	}
	for i := 0; i < variations; i++ {
		var secret, name, logo, website string
		change := ClientChange{}
		expectation := client
		result := client
		if i&clientChangeSecret != 0 {
			secret = fmt.Sprintf("secret-%d", i)
			change.Secret = &secret
			expectation.Secret = secret
		}
		if i&clientChangeOwnerID != 0 {
			change.OwnerID = uuid.NewID()
			expectation.OwnerID = change.OwnerID
		}
		if i&clientChangeName != 0 {
			name = fmt.Sprintf("name-%d", i)
			change.Name = &name
			expectation.Name = name
		}
		if i&clientChangeLogo != 0 {
			logo = fmt.Sprintf("logo-%d", i)
			change.Logo = &logo
			expectation.Logo = logo
		}
		if i&clientChangeWebsite != 0 {
			website = fmt.Sprintf("website-%d", i)
			change.Website = &website
			expectation.Website = website
		}
		result.ApplyChange(change)
		match, field, expected, got := compareClients(expectation, result)
		if !match {
			t.Fatalf("Expected field `%s` to be `%v`, got `%v`", field, expected, got)
		}
		for _, store := range clientStores {
			context := Context{clients: store}
			err := context.SaveClient(client)
			if err != nil {
				t.Fatalf("Error saving client in %T: %s", store, err)
			}
			err = context.UpdateClient(client.ID, change)
			if err != nil {
				t.Fatalf("Error updating client in %T: %s", store, err)
			}
			retrieved, err := context.GetClient(client.ID)
			if err != nil {
				t.Fatalf("Error getting client from %T: %s", store, err)
			}
			match, field, expected, got = compareClients(expectation, retrieved)
			if !match {
				t.Fatalf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
			}
			err = context.DeleteClient(client.ID)
			if err != nil {
				t.Fatalf("Error deleting client from %T: %s", store, err)
			}
			err = context.UpdateClient(client.ID, change)
			if err != ErrClientNotFound {
				t.Fatalf("Expected ErrClientNotFound, got %v from %T", err, store)
			}
		}
	}
}

func TestClientEndpointChecks(t *testing.T) {
	t.Parallel()
	client := Client{
		ID:      uuid.NewID(),
		Secret:  "secret",
		OwnerID: uuid.NewID(),
		Name:    "name",
		Logo:    "logo",
		Website: "website",
	}
	endpoint1 := Endpoint{
		ID:       uuid.NewID(),
		ClientID: client.ID,
		Added:    time.Now().Round(time.Millisecond),
		URI:      "https://www.example.com/first",
	}
	endpoint2 := Endpoint{
		ID:       uuid.NewID(),
		ClientID: client.ID,
		Added:    time.Now().Round(time.Millisecond),
		URI:      "https://www.example.com/my/full/path",
	}
	candidates := map[string]bool{
		"https://www.example.com/":                 false,
		"https://www.example.com/first":            true,
		"https://www.example.com/first/extra/path": false,
		"https://www.example.com/my":               false,
		"https://www.example.com/my/full/path":     true,
	}
	for _, store := range clientStores {
		context := Context{clients: store}
		err := context.SaveClient(client)
		if err != nil {
			t.Fatalf("Error saving client in %T: %s", store, err)
		}
		err = context.AddEndpoints(client.ID, []Endpoint{endpoint1})
		if err != nil {
			t.Fatalf("Error saving endpoint in %T: %s", store, err)
		}
		err = context.AddEndpoints(client.ID, []Endpoint{endpoint2})
		if err != nil {
			t.Fatalf("Error saving endpoint in %T: %s", store, err)
		}
		for candidate, expectation := range candidates {
			result, err := context.CheckEndpoint(client.ID, candidate)
			if err != nil {
				t.Fatalf("Error checking endpoint %s in %T: %s", candidate, store, err)
			}
			if result != expectation {
				expectStr := "no"
				resultStr := "a"
				if expectation {
					expectStr = "a"
					resultStr = "no"
				}
				t.Errorf("Expected %s match for %s in %T, got %s match", expectStr, candidate, store, resultStr)
			}
		}
	}
}

func TestClientEndpointChecksStrict(t *testing.T) {
	t.Parallel()
	client := Client{
		ID:      uuid.NewID(),
		Secret:  "secret",
		OwnerID: uuid.NewID(),
		Name:    "name",
		Logo:    "logo",
		Website: "website",
	}
	endpoint1 := Endpoint{
		ID:       uuid.NewID(),
		ClientID: client.ID,
		Added:    time.Now().Round(time.Millisecond),
		URI:      "https://www.example.com/first",
	}
	endpoint2 := Endpoint{
		ID:       uuid.NewID(),
		ClientID: client.ID,
		Added:    time.Now().Round(time.Millisecond),
		URI:      "https://www.example.com/my/full/path",
	}
	candidates := map[string]bool{
		"https://www.example.com/":                 false,
		"https://www.example.com/first":            true,
		"https://www.example.com/first/extra/path": false,
		"https://www.example.com/my":               false,
		"https://www.example.com/my/full/path":     true,
	}
	for _, store := range clientStores {
		context := Context{clients: store}
		err := context.SaveClient(client)
		if err != nil {
			t.Fatalf("Error saving client in %T: %s", store, err)
		}
		err = context.AddEndpoints(client.ID, []Endpoint{endpoint1})
		if err != nil {
			t.Fatalf("Error saving endpoint in %T: %s", store, err)
		}
		err = context.AddEndpoints(client.ID, []Endpoint{endpoint2})
		if err != nil {
			t.Fatalf("Error saving endpoint in %T: %s", store, err)
		}
		for candidate, expectation := range candidates {
			result, err := context.CheckEndpoint(client.ID, candidate)
			if err != nil {
				t.Fatalf("Error checking endpoint %s in %T: %s", candidate, store, err)
			}
			if result != expectation {
				expectStr := "no"
				resultStr := "a"
				if expectation {
					expectStr = "a"
					resultStr = "no"
				}
				t.Errorf("Expected %s match for %s in %T, got %s match", expectStr, candidate, store, resultStr)
			}
		}
	}
}

func TestClientChangeValidation(t *testing.T) {
	t.Parallel()
	change := ClientChange{}
	if err := change.Validate(); err[0] != ErrEmptyChange {
		t.Errorf("Expected %s to give an error of %s, gave %s", "empty change", ErrEmptyChange, err)
	}
	names := map[string][]error{
		"a":   []error{ErrClientNameTooShort},
		"ab":  []error{},
		"abc": []error{},
		"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopq": []error{ErrClientNameTooLong},
	}
	for name, expectation := range names {
		change = ClientChange{Name: &name}
		errs := change.Validate()
		if len(errs) != len(expectation) {
			t.Errorf("Expected %s to give %d errors, gave %d", name, len(expectation), len(errs))
			t.Logf("%+v", errs)
		}
		for pos, err := range errs {
			if err != expectation[pos] {
				t.Errorf("Expected %s to give an error of %s in position %d, gave %s", name, expectation[pos], pos, err)
			}
		}
	}
	longPath := ""
	for i := 0; i < 1025; i++ {
		longPath = fmt.Sprintf("%s%d", longPath, i)
	}
	logos := map[string][]error{
		"https://www.example.com/" + longPath: []error{ErrClientLogoTooLong},
		"https://www.example.com/ab":          []error{},
		"www.example.com/ab":                  []error{ErrClientLogoNotURL},
		"test":                                []error{ErrClientLogoNotURL},
		"":                                    []error{},
	}
	for logo, expectation := range logos {
		change = ClientChange{Logo: &logo}
		errs := change.Validate()
		if len(errs) != len(expectation) {
			t.Errorf("Expected %s to give %d errors, gave %d", logo, len(expectation), len(errs))
		}
		for pos, err := range errs {
			if err != expectation[pos] {
				t.Errorf("Expected %s to give an error of %s in positiong %d, gave %s", logo, expectation[pos], pos, err)
			}
		}
	}
	websites := map[string][]error{
		"https://www.example.com/" + longPath: []error{ErrClientWebsiteTooLong},
		"https://www.example.com/ab":          []error{},
		"www.example.com/ab":                  []error{ErrClientWebsiteNotURL},
		"test":                                []error{ErrClientWebsiteNotURL},
		"":                                    []error{},
	}
	for website, expectation := range websites {
		change = ClientChange{Website: &website}
		errs := change.Validate()
		if len(errs) != len(expectation) {
			t.Errorf("Expected %s to give %d errors, gave %d", website, len(expectation), len(errs))
		}
		for pos, err := range errs {
			if err != expectation[pos] {
				t.Errorf("Expected %s to give an error of %s in position %d, gave %s", website, expectation[pos], pos, err)
			}
		}
	}
}

func TestGetClientAuth(t *testing.T) {
	t.Parallel()
	type clientAuthRequest struct {
		username                 string
		pass                     string
		clientID                 string
		allowPublic              bool
		expectedClientID         uuid.ID
		expectedClientSecret     string
		expectedValid            bool
		expectedCode             int
		expectedBody             string
		expectAuthenticateHeader bool
	}
	id := uuid.NewID()
	tests := []clientAuthRequest{
		{"", "", "", false, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
		{"", "", "", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
		{"", "no clientID set", "", false, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
		{"", "no clientID set", "", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
		{"not an actual id", "invalid client ID set", "", false, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
		{"not an actual id", "invalid client ID set", "", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
		{"", "", "not an actual id", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
		{id.String(), "secret", "", true, id, "secret", true, http.StatusOK, "", false},
		{id.String(), "secret", "", false, id, "secret", true, http.StatusOK, "", false},
		{"", "", id.String(), true, id, "", true, http.StatusOK, "", false},
		{"", "", id.String(), false, nil, "", false, http.StatusBadRequest, `{"error":"unauthorized_client"}`, false},
	}
	for pos, test := range tests {
		t.Logf("Running test #%d, with request %+v", pos, test)
		w := httptest.NewRecorder()
		r, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
		if err != nil {
			t.Fatal("Can't build request:", err)
		}
		if test.username != "" || test.pass != "" {
			r.SetBasicAuth(test.username, test.pass)
		}
		r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		params := url.Values{}
		params.Set("client_id", test.clientID)
		body := bytes.NewBufferString(params.Encode())
		r.Body = ioutil.NopCloser(body)
		respID, respSecret, success := getClientAuth(w, r, test.allowPublic)
		if (respID == nil && test.expectedClientID != nil) || (respID != nil && test.expectedClientID == nil) || !respID.Equal(test.expectedClientID) {
			t.Errorf("Expected response ID to be %v, got %v", test.expectedClientID, respID)
		}
		if test.expectedClientSecret != respSecret {
			t.Errorf("Expected response secret to be '%s', got '%s'", test.expectedClientSecret, respSecret)
		}
		if test.expectedValid != success {
			t.Errorf("Expected success result to be %v, got %v", test.expectedValid, success)
		}
		if test.expectedCode != w.Code {
			t.Errorf("Expected response code to be %d, got %d", test.expectedCode, w.Code)
		}
		if test.expectedBody != strings.TrimSpace(w.Body.String()) {
			t.Errorf("Expected body to be '%s', got '%s'", test.expectedBody, strings.TrimSpace(w.Body.String()))
		}
		if test.expectAuthenticateHeader && w.Header().Get("WWW-Authenticate") != "Basic" {
			t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
		}
	}
}

func TestVerifyClient(t *testing.T) {
	t.Parallel()
	type verifyClientRequest struct {
		username                 string
		pass                     string
		clientID                 string
		allowPublic              bool
		expectedClientID         uuid.ID
		expectedValid            bool
		expectedCode             int
		expectedBody             string
		expectAuthenticateHeader bool
	}
	memstore := NewMemstore()
	context := Context{
		clients: memstore,
	}
	client := Client{
		ID:      uuid.NewID(),
		Secret:  "super secret!",
		OwnerID: uuid.NewID(),
		Name:    "My test client",
		Logo:    "https://secondbit.org/logo.png",
		Website: "https://secondbit.org/",
		Type:    "confidential",
	}
	err := context.SaveClient(client)
	if err != nil {
		t.Fatal("Could not save client:", err)
	}
	publicClient := Client{
		ID:      uuid.NewID(),
		Secret:  "",
		OwnerID: uuid.NewID(),
		Name:    "A public client",
		Logo:    "https://secondbit.org/logo.png",
		Website: "https://secondbit.org/",
		Type:    "public",
	}
	err = context.SaveClient(publicClient)
	if err != nil {
		t.Fatal("Could not save client:", err)
	}
	id := uuid.NewID()
	tests := []verifyClientRequest{
		{"", "", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
		{"", "", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
		{"", "no clientID set", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
		{"", "no clientID set", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
		{"not an actual id", "invalid client ID set", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
		{"not an actual id", "invalid client ID set", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
		{id.String(), "unsaved client ID set", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
		{id.String(), "unsaved client ID set", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
		{client.ID.String(), "wrong secret", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
		{client.ID.String(), "wrong secret", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
		{"", "", "not an actual id", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
		{"", "", id.String(), true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
		{client.ID.String(), client.Secret, "", true, client.ID, true, http.StatusOK, "", false},
		{client.ID.String(), client.Secret, "", false, client.ID, true, http.StatusOK, "", false},
		{"", "", publicClient.ID.String(), true, publicClient.ID, true, http.StatusOK, "", false},
		{"", "", publicClient.ID.String(), false, nil, false, http.StatusBadRequest, `{"error":"unauthorized_client"}`, false},
	}

	for pos, test := range tests {
		t.Logf("Running test #%d, with request %+v", pos, test)
		w := httptest.NewRecorder()
		r, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
		if err != nil {
			t.Fatal("Can't build request:", err)
		}
		if test.username != "" || test.pass != "" {
			r.SetBasicAuth(test.username, test.pass)
		}
		r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		params := url.Values{}
		params.Set("client_id", test.clientID)
		body := bytes.NewBufferString(params.Encode())
		r.Body = ioutil.NopCloser(body)
		respID, success := verifyClient(w, r, test.allowPublic, context)
		if (respID == nil && test.expectedClientID != nil) || (respID != nil && test.expectedClientID == nil) || !respID.Equal(test.expectedClientID) {
			t.Errorf("Expected response ID to be %v, got %v", test.expectedClientID, respID)
		}
		if test.expectedValid != success {
			t.Errorf("Expected success result to be %v, got %v", test.expectedValid, success)
		}
		if test.expectedCode != w.Code {
			t.Errorf("Expected response code to be %d, got %d", test.expectedCode, w.Code)
		}
		if test.expectedBody != strings.TrimSpace(w.Body.String()) {
			t.Errorf("Expected body to be '%s', got '%s'", test.expectedBody, strings.TrimSpace(w.Body.String()))
		}
		if test.expectAuthenticateHeader && w.Header().Get("WWW-Authenticate") != "Basic" {
			t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
		}
	}
}

func TestCreateClientHandler(t *testing.T) {
	t.Parallel()
	memstore := NewMemstore()
	c := Context{
		clients:  memstore,
		profiles: memstore,
	}
	w := httptest.NewRecorder()
	r, err := http.NewRequest("POST", "https://test.auth.secondbit.org/clients", nil)
	if err != nil {
		t.Fatal("Can't build request:", err)
	}
	r.Header.Set("Content-Type", "application/json")
	CreateClientHandler(w, r, c)
	if w.Code != http.StatusUnauthorized {
		t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
	}
	expected := `{"errors":[{"error":"access_denied"}]}`
	result := strings.TrimSpace(w.Body.String())
	if result != expected {
		t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
	}
	w = httptest.NewRecorder()
	r.Header.Set("Authorization", "Not basic at all...")
	CreateClientHandler(w, r, c)
	if w.Code != http.StatusUnauthorized {
		t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
	}
	expected = `{"errors":[{"error":"access_denied"}]}`
	result = strings.TrimSpace(w.Body.String())
	if result != expected {
		t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
	}
	w = httptest.NewRecorder()
	r.Header.Set("Authorization", "Basic TotallyNotBase64Encoded")
	CreateClientHandler(w, r, c)
	if w.Code != http.StatusUnauthorized {
		t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
	}
	expected = `{"errors":[{"error":"access_denied"}]}`
	result = strings.TrimSpace(w.Body.String())
	if result != expected {
		t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
	}
	w = httptest.NewRecorder()
	r.Header.Set("Authorization", "Basic dGhpc2hhc25vY29sb24=")
	CreateClientHandler(w, r, c)
	if w.Code != http.StatusUnauthorized {
		t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
	}
	expected = `{"errors":[{"error":"access_denied"}]}`
	result = strings.TrimSpace(w.Body.String())
	if result != expected {
		t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
	}
	profile := Profile{
		ID:                     uuid.NewID(),
		Name:                   "Test User",
		Passphrase:             "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0",
		Iterations:             1,
		Salt:                   "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b",
		PassphraseScheme:       1,
		Compromised:            false,
		LockedUntil:            time.Time{},
		PassphraseReset:        "",
		PassphraseResetCreated: time.Time{},
		Created:                time.Now().Round(time.Millisecond),
		LastSeen:               time.Time{},
	}
	login := Login{
		Type:      "email",
		Value:     "test@example.com",
		ProfileID: profile.ID,
		Created:   time.Now().Round(time.Millisecond),
		LastUsed:  time.Time{},
	}
	w = httptest.NewRecorder()
	r.SetBasicAuth("test@example.com", "mysecurepassphrase")
	CreateClientHandler(w, r, c)
	if w.Code != http.StatusUnauthorized {
		t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
	}
	expected = `{"errors":[{"error":"access_denied"}]}`
	result = strings.TrimSpace(w.Body.String())
	if result != expected {
		t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
	}
	err = c.SaveProfile(profile)
	if err != nil {
		t.Error("Error saving profile:", err)
	}
	err = c.AddLogin(login)
	if err != nil {
		t.Error("Error adding login:", err)
	}
	r.SetBasicAuth("test@example.com", "mysecurepassphrase")
	type testStruct struct {
		request string
		code    int
		resp    response
	}
	tests := []testStruct{
		{``, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidFormat, Field: "/"}}}},
		{`{}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/type"}, {Slug: requestErrMissing, Field: "/name"}}}},
		{`{"type":"notarealtype"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrMissing, Field: "/name"}}}},
		{`{"type":"notarealtype","name":"myreallylongnameislongerthatthemaximumnamelength"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrOverflow, Field: "/name"}}}},
		{`{"type":"notarealtype","name":"a"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrInsufficient, Field: "/name"}}}},
		{`{"type":"public"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/name"}}}},
		{`{"type":"public","name":"myreallylongnameislongerthatthemaximumnamelength"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrOverflow, Field: "/name"}}}},
		{`{"type":"public","name":"a"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInsufficient, Field: "/name"}}}},
		{`{"name":"My Client"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/type"}}}},
		{`{"type":"notarealtype","name":"My Client"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}}}},
		{`{"type":"public","name":"My Client"}`, http.StatusCreated, response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}}},
		{`{"type":"public","name":"My Client", "endpoints": ["https://test.secondbit.org/", "https://paddy.io"]}`, http.StatusCreated, response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}, Endpoints: []Endpoint{{URI: "https://test.secondbit.org/"}, {URI: "https://paddy.io"}}}},
		{`{"type":"public","name":"My Client", "endpoints": [":/not a url", "https://paddy.io"]}`, http.StatusCreated, response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}, Endpoints: []Endpoint{{URI: "https://paddy.io"}}, Errors: []requestError{{Slug: requestErrInvalidFormat, Field: "/endpoints/0"}}}},
		{`{"type":"public","name":"My Client", "endpoints": [":/not a url", "/relative/uri", "https://paddy.io"]}`, http.StatusCreated, response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}, Endpoints: []Endpoint{{URI: "https://paddy.io"}}, Errors: []requestError{{Slug: requestErrInvalidFormat, Field: "/endpoints/0"}, {Slug: requestErrInvalidValue, Field: "/endpoints/1"}}}},
		{`{"type":"confidential","name":"Secret Client", "endpoints": ["https://secondbit.org"]}`, http.StatusCreated, response{Clients: []Client{{Name: "Secret Client", OwnerID: profile.ID, Type: "confidential"}}, Endpoints: []Endpoint{{URI: "https://secondbit.org"}}}},
	}
	for pos, test := range tests {
		t.Logf("Test #%d: `%s`", pos, test.request)
		w = httptest.NewRecorder()
		body := bytes.NewBufferString(test.request)
		r.Body = ioutil.NopCloser(body)
		CreateClientHandler(w, r, c)
		if w.Code != test.code {
			t.Errorf("Expected response code to be %d, got %d", test.code, w.Code)
		}
		t.Logf("Response: %s", w.Body.String())
		var res response
		err = json.Unmarshal(w.Body.Bytes(), &res)
		if err != nil {
			t.Error("Unexpected error unmarshalling response:", err)
		}
		if len(res.Clients) > 0 {
			if res.Clients[0].Type == "confidential" && res.Clients[0].Secret == "" {
				t.Log("Client:", res.Clients[0])
				t.Error("Expected confidential client to have a secret, but does not.")
			} else if res.Clients[0].Type == "public" && res.Clients[0].Secret != "" {
				t.Log("Client:", res.Clients[0])
				t.Error("Expected public client to not have a secret, but it does.")
			}
		}
		fillInServerGenerated(test.resp, res)
		success, field, expectation, result := compareResponses(test.resp, res)
		if !success {
			t.Errorf("Unexpected result for %s in response: expected %v, got %v", field, expectation, result)
		}
	}
}

func TestGetClientHandler(t *testing.T) {
	t.Parallel()
	memstore := NewMemstore()
	c := Context{
		clients:  memstore,
		profiles: memstore,
	}
	client := Client{
		ID:      uuid.NewID(),
		Secret:  "myawesomesecret",
		OwnerID: uuid.NewID(),
		Name:    "Test Client",
		Logo:    "https://auth.secondbit.org/logo.png",
		Website: "https://code.secondbit.org",
		Type:    clientTypeConfidential,
	}
	err := c.SaveClient(client)
	if err != nil {
		t.Fatal("Can't store client in memstore:", err)
	}
	profile := Profile{
		ID:                     uuid.NewID(),
		Name:                   "Test User",
		Passphrase:             "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0",
		Iterations:             1,
		Salt:                   "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b",
		PassphraseScheme:       1,
		Compromised:            false,
		LockedUntil:            time.Time{},
		PassphraseReset:        "",
		PassphraseResetCreated: time.Time{},
		Created:                time.Now().Round(time.Millisecond),
		LastSeen:               time.Time{},
	}
	login := Login{
		Type:      "email",
		Value:     "test@example.com",
		ProfileID: profile.ID,
		Created:   time.Now().Round(time.Millisecond),
		LastUsed:  time.Time{},
	}
	err = c.SaveProfile(profile)
	if err != nil {
		t.Error("Error saving profile:", err)
	}
	err = c.AddLogin(login)
	if err != nil {
		t.Error("Error adding login:", err)
	}
	router := mux.NewRouter()
	RegisterClientHandlers(router, c)
	w := httptest.NewRecorder()
	u := "https://test.auth.secondbit.org/clients/" + client.ID.String()
	r, err := http.NewRequest("GET", u, nil)
	if err != nil {
		t.Fatal("Can't build request:", err)
	}
	r.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(w, r)
	if w.Code != http.StatusOK {
		t.Errorf("Expected response code to be %d, got %d", http.StatusOK, w.Code)
	}
	t.Logf("Response: %s", w.Body.String())
	var res response
	err = json.Unmarshal(w.Body.Bytes(), &res)
	if err != nil {
		t.Error("Unexpected error unmarshalling response:", err)
	}
	if len(res.Clients) != 1 {
		t.Errorf("Expected %d results in response, got %d", 1, len(res.Clients))
	}
	if res.Clients[0].Secret != "" {
		t.Error("Expected secret not to be set, but was set to", res.Clients[0].Secret)
	}
	// fill in the secret, which was omitted in the response
	res.Clients[0].Secret = client.Secret
	success, field, expectation, result := compareClients(client, res.Clients[0])
	if !success {
		t.Errorf("Unexpected result for %s in response: expected %v, got %v", field, expectation, result)
	}

	// test for improperly formatted ID
	u = "https://test.auth.secondbit.org/clients/notanID"
	w = httptest.NewRecorder()
	r, err = http.NewRequest("GET", u, nil)
	if err != nil {
		t.Fatal("Can't build request:", err)
	}
	r.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(w, r)
	if w.Code != http.StatusBadRequest {
		t.Errorf("Expected response code to be %d, got %d", http.StatusBadRequest, w.Code)
	}
	t.Logf("Response: %s", w.Body.String())
	res = response{}
	err = json.Unmarshal(w.Body.Bytes(), &res)
	if err != nil {
		t.Error("Unexpected error unmarshalling response:", err)
	}
	if len(res.Errors) != 1 {
		t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors))
	}
	e := requestError{Slug: requestErrInvalidFormat, Param: "id"}
	success, field, expectation, result = compareErrors(e, res.Errors[0])
	if !success {
		t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result)
	}

	// test for a non-existent client
	u = "https://test.auth.secondbit.org/clients/" + uuid.NewID().String()
	w = httptest.NewRecorder()
	r, err = http.NewRequest("GET", u, nil)
	if err != nil {
		t.Fatal("Can't build request:", err)
	}
	r.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(w, r)
	if w.Code != http.StatusNotFound {
		t.Errorf("Expected response code to be %d, got %d", http.StatusNotFound, w.Code)
	}
	t.Logf("Response: %s", w.Body.String())
	res = response{}
	err = json.Unmarshal(w.Body.Bytes(), &res)
	if err != nil {
		t.Error("Unexpected error unmarshalling response:", err)
	}
	if len(res.Errors) != 1 {
		t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors))
	}
	e = requestError{Slug: requestErrNotFound, Param: "id"}
	success, field, expectation, result = compareErrors(e, res.Errors[0])
	if !success {
		t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result)
	}
}

func TestAuthenticatedGetClientHandler(t *testing.T) {
	t.Parallel()
	memstore := NewMemstore()
	c := Context{
		clients:  memstore,
		profiles: memstore,
	}
	client := Client{
		ID:      uuid.NewID(),
		Secret:  "myawesomesecret",
		OwnerID: uuid.NewID(),
		Name:    "Test Client",
		Logo:    "https://auth.secondbit.org/logo.png",
		Website: "https://code.secondbit.org",
		Type:    clientTypeConfidential,
	}
	err := c.SaveClient(client)
	if err != nil {
		t.Fatal("Can't store client in memstore:", err)
	}
	profile := Profile{
		ID:                     client.OwnerID,
		Name:                   "Test User",
		Passphrase:             "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0",
		Iterations:             1,
		Salt:                   "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b",
		PassphraseScheme:       1,
		Compromised:            false,
		LockedUntil:            time.Time{},
		PassphraseReset:        "",
		PassphraseResetCreated: time.Time{},
		Created:                time.Now().Round(time.Millisecond),
		LastSeen:               time.Time{},
	}
	login := Login{
		Type:      "email",
		Value:     "test@example.com",
		ProfileID: profile.ID,
		Created:   time.Now().Round(time.Millisecond),
		LastUsed:  time.Time{},
	}
	err = c.SaveProfile(profile)
	if err != nil {
		t.Error("Error saving profile:", err)
	}
	err = c.AddLogin(login)
	if err != nil {
		t.Error("Error adding login:", err)
	}
	profile2 := Profile{
		ID:                     uuid.NewID(),
		Name:                   "Test User",
		Passphrase:             "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0",
		Iterations:             1,
		Salt:                   "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b",
		PassphraseScheme:       1,
		Compromised:            false,
		LockedUntil:            time.Time{},
		PassphraseReset:        "",
		PassphraseResetCreated: time.Time{},
		Created:                time.Now().Round(time.Millisecond),
		LastSeen:               time.Time{},
	}
	login2 := Login{
		Type:      "email",
		Value:     "test2@example.com",
		ProfileID: profile2.ID,
		Created:   time.Now().Round(time.Millisecond),
		LastUsed:  time.Time{},
	}
	err = c.SaveProfile(profile2)
	if err != nil {
		t.Error("Error saving profile:", err)
	}
	err = c.AddLogin(login2)
	if err != nil {
		t.Error("Error adding login:", err)
	}
	router := mux.NewRouter()
	RegisterClientHandlers(router, c)
	w := httptest.NewRecorder()
	u := "https://test.auth.secondbit.org/clients/" + client.ID.String()
	r, err := http.NewRequest("GET", u, nil)
	if err != nil {
		t.Fatal("Can't build request:", err)
	}
	r.Header.Set("Content-Type", "application/json")
	r.SetBasicAuth(login.Value, "mysecurepassphrase")
	router.ServeHTTP(w, r)
	if w.Code != http.StatusOK {
		t.Errorf("Expected response code to be %d, got %d", http.StatusOK, w.Code)
	}
	t.Logf("Response: %s", w.Body.String())
	var res response
	err = json.Unmarshal(w.Body.Bytes(), &res)
	if err != nil {
		t.Error("Unexpected error unmarshalling response:", err)
	}
	if len(res.Clients) != 1 {
		t.Errorf("Expected %d results in response, got %d", 1, len(res.Clients))
	}
	success, field, expectation, result := compareClients(client, res.Clients[0])
	if !success {
		t.Errorf("Unexpected result for %s in response: expected %v, got %v", field, expectation, result)
	}

	// test for improperly formatted ID
	u = "https://test.auth.secondbit.org/clients/notanID"
	w = httptest.NewRecorder()
	r, err = http.NewRequest("GET", u, nil)
	if err != nil {
		t.Fatal("Can't build request:", err)
	}
	r.Header.Set("Content-Type", "application/json")
	r.SetBasicAuth(login.Value, "mysecurepassphrase")
	router.ServeHTTP(w, r)
	if w.Code != http.StatusBadRequest {
		t.Errorf("Expected response code to be %d, got %d", http.StatusBadRequest, w.Code)
	}
	t.Logf("Response: %s", w.Body.String())
	res = response{}
	err = json.Unmarshal(w.Body.Bytes(), &res)
	if err != nil {
		t.Error("Unexpected error unmarshalling response:", err)
	}
	if len(res.Errors) != 1 {
		t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors))
	}
	e := requestError{Slug: requestErrInvalidFormat, Param: "id"}
	success, field, expectation, result = compareErrors(e, res.Errors[0])
	if !success {
		t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result)
	}

	// test for a non-existent client
	u = "https://test.auth.secondbit.org/clients/" + uuid.NewID().String()
	w = httptest.NewRecorder()
	r, err = http.NewRequest("GET", u, nil)
	if err != nil {
		t.Fatal("Can't build request:", err)
	}
	r.Header.Set("Content-Type", "application/json")
	r.SetBasicAuth(login.Value, "mysecurepassphrase")
	router.ServeHTTP(w, r)
	if w.Code != http.StatusNotFound {
		t.Errorf("Expected response code to be %d, got %d", http.StatusNotFound, w.Code)
	}
	t.Logf("Response: %s", w.Body.String())
	res = response{}
	err = json.Unmarshal(w.Body.Bytes(), &res)
	if err != nil {
		t.Error("Unexpected error unmarshalling response:", err)
	}
	if len(res.Errors) != 1 {
		t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors))
	}
	e = requestError{Slug: requestErrNotFound, Param: "id"}
	success, field, expectation, result = compareErrors(e, res.Errors[0])
	if !success {
		t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result)
	}

	// test for a wrong password
	u = "https://test.auth.secondbit.org/clients/" + client.ID.String()
	w = httptest.NewRecorder()
	r, err = http.NewRequest("GET", u, nil)
	if err != nil {
		t.Fatal("Can't build request:", err)
	}
	r.Header.Set("Content-Type", "application/json")
	r.SetBasicAuth(login.Value, "notmypassphrase")
	router.ServeHTTP(w, r)
	if w.Code != http.StatusUnauthorized {
		t.Errorf("Expected response code to be %d, got %d", http.StatusUnauthorized, w.Code)
	}
	t.Logf("Response: %s", w.Body.String())
	res = response{}
	err = json.Unmarshal(w.Body.Bytes(), &res)
	if err != nil {
		t.Error("Unexpected error unmarshalling response:", err)
	}
	if len(res.Errors) != 1 {
		t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors))
	}
	e = requestError{Slug: requestErrAccessDenied}
	success, field, expectation, result = compareErrors(e, res.Errors[0])
	if !success {
		t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result)
	}

	// test for a wrong account
	u = "https://test.auth.secondbit.org/clients/" + client.ID.String()
	w = httptest.NewRecorder()
	r, err = http.NewRequest("GET", u, nil)
	if err != nil {
		t.Fatal("Can't build request:", err)
	}
	r.Header.Set("Content-Type", "application/json")
	r.SetBasicAuth(login2.Value, "mysecurepassphrase")
	router.ServeHTTP(w, r)
	if w.Code != http.StatusOK {
		t.Errorf("Expected response code to be %d, got %d", http.StatusOK, w.Code)
	}
	t.Logf("Response: %s", w.Body.String())
	res = response{}
	err = json.Unmarshal(w.Body.Bytes(), &res)
	if err != nil {
		t.Error("Unexpected error unmarshalling response:", err)
	}
	if len(res.Clients) != 1 {
		t.Errorf("Expected %d results in response, got %d", 1, len(res.Clients))
	}
	if res.Clients[0].Secret != "" {
		t.Errorf("Expected client secret to be empty, got %s", res.Clients[0].Secret)
	}
	// fill the client's secret for comparison
	res.Clients[0].Secret = client.Secret
	success, field, expectation, result = compareClients(client, res.Clients[0])
	if !success {
		t.Errorf("Unexpected result for %s in response: expected %v, got %v", field, expectation, result)
	}
}

// BUG(paddy): We need to test the clientCredentialsValidate function.
// BUG(paddy): We need to test the ListClientsHandler.
