package auth

import (
	"bytes"
	"encoding/json"
	"html/template"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"
	"time"

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

const (
	stateSet = 1 << iota
	uriSet
)

func stripParam(param string, u *url.URL) {
	q := u.Query()
	q.Del(param)
	u.RawQuery = q.Encode()
}

func TestGetAuthorizationCodeCodeSuccess(t *testing.T) {
	t.Parallel()
	store := NewMemstore()
	testContext := Context{
		template:  template.Must(template.New(getAuthorizationCodeTemplateName).Parse("{{ if not .error }}Get auth grant{{ else }}{{ .error }}{{ end }}")),
		clients:   store,
		authCodes: store,
		profiles:  store,
		tokens:    store,
		sessions:  store,
		scopes:    store,
	}
	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:    "public",
	}
	endpoint := Endpoint{
		ID:       uuid.NewID(),
		ClientID: client.ID,
		URI:      "https://test.secondbit.org/redirect",
		Added:    time.Now(),
	}
	err := testContext.SaveClient(client)
	if err != nil {
		t.Fatal("Can't store client:", err)
	}
	err = testContext.AddEndpoints(client.ID, []Endpoint{endpoint})
	if err != nil {
		t.Fatal("Can't store endpoint:", err)
	}
	profile := Profile{
		ID: uuid.NewID(),
	}
	err = testContext.SaveProfile(profile)
	if err != nil {
		t.Fatal("Can't store profile:", err)
	}
	session := Session{
		ID:        "testsession",
		Active:    true,
		ProfileID: profile.ID,
		CSRFToken: "nosurf",
		Expires:   time.Now().Add(time.Hour),
	}
	err = testContext.CreateSession(session)
	if err != nil {
		t.Fatal("Can't store session:", err)
	}
	scope := Scope{
		ID:          "testscope",
		Name:        "Test Scope",
		Description: "Hug dispensation.",
	}
	err = testContext.CreateScopes([]Scope{scope})
	if err != nil {
		t.Fatal("Can't store scope:", err)
	}
	req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
	if err != nil {
		t.Fatal("Can't build request:", err)
	}
	cookie := &http.Cookie{
		Name:  authCookieName,
		Value: session.ID,
	}
	req.AddCookie(cookie)
	for i := 0; i < 1<<2; i++ {
		w := httptest.NewRecorder()
		params := url.Values{}
		// see OAuth 2.0 spec, section 4.1.1
		params.Set("response_type", "code")
		params.Set("client_id", client.ID.String())
		params.Set("scope", "testscope")
		if i&uriSet != 0 {
			params.Set("redirect_uri", endpoint.URI)
		}
		if i&stateSet != 0 {
			params.Set("state", "my super secure state string")
		}
		req.URL.RawQuery = params.Encode()
		req.Method = "GET"
		req.Body = nil
		req.Header.Del("Content-Type")
		GetAuthorizationCodeHandler(w, req, testContext)
		if w.Code != http.StatusOK {
			t.Log("Response body:", w.Body.String())
			t.Errorf("Expected status code to be %d, got %d for %s", http.StatusOK, w.Code, req.URL.String())
		}
		if w.Body.String() != "Get auth grant" {
			t.Errorf("Expected body to be `%s`, got `%s` for %s", "Get auth grant", w.Body.String(), req.URL.String())
		}
		req.Method = "POST"
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		w = httptest.NewRecorder()
		data := url.Values{}
		data.Set("grant", "approved")
		data.Set("csrftoken", session.CSRFToken)
		body := bytes.NewBufferString(data.Encode())
		req.Body = ioutil.NopCloser(body)
		GetAuthorizationCodeHandler(w, req, testContext)
		if w.Code != http.StatusFound {
			t.Errorf("Expected status code to be %d, got %d for %s", http.StatusFound, w.Code, req.URL.String())
		}
		redirectedTo := w.Header().Get("Location")
		red, err := url.Parse(redirectedTo)
		if err != nil {
			t.Fatalf(`Being redirected to a non-URL "%s" threw error "%s" for "%s"\n`, redirectedTo, err, req.URL.String())
		}
		t.Log("Redirected to", redirectedTo)
		t.Log("Body", w.Body.String())
		if red.Query().Get("code") == "" {
			t.Fatalf(`Expected code param in redirect URL to be set, but it wasn't for %s`, req.URL.String())
		}
		if _, err := testContext.GetAuthorizationCode(red.Query().Get("code")); err != nil {
			t.Fatalf(`Unexpected error "%s: retrieving the grant "%s" supplied in the redirect URL for %s`, err, red.Query().Get("code"), req.URL.String())
		}
		err = testContext.DeleteAuthorizationCode(red.Query().Get("code"))
		if err != nil {
			t.Logf(`Unexpected error "%s" deleting grant "%s" for %s`, err, red.Query().Get("code"), req.URL.String())
		}
		stripParam("code", red)
		if red.Query().Get("state") != params.Get("state") {
			t.Errorf(`Expected state param in redirect URL to be "%s", got "%s" for %s`, params.Get("state"), red.Query().Get("state"), req.URL.String())
		}
		stripParam("state", red)
		if red.String() != endpoint.URI {
			t.Errorf(`Expected redirect URL to be "%s", got "%s"`, endpoint.URI, red.String())
		}
	}
}

func TestGetAuthorizationCodeCodeInvalidClient(t *testing.T) {
	t.Parallel()
	store := NewMemstore()
	testContext := Context{
		template:  template.Must(template.New(getAuthorizationCodeTemplateName).Parse("{{ .error }}")),
		clients:   store,
		authCodes: store,
		profiles:  store,
		tokens:    store,
		sessions:  store,
	}
	client := Client{
		ID:      uuid.NewID(),
		Secret:  "super secret!",
		OwnerID: uuid.NewID(),
		Name:    "My test client",
		Type:    "public",
	}
	err := testContext.SaveClient(client)
	if err != nil {
		t.Fatal("Can't store client:", err)
	}
	session := Session{
		ID:        "testsession",
		Active:    true,
		CSRFToken: "nosurf",
		Expires:   time.Now().Add(time.Hour),
	}
	err = testContext.CreateSession(session)
	if err != nil {
		t.Fatal("Can't store session:", err)
	}
	req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
	if err != nil {
		t.Fatal("Can't build request:", err)
	}
	w := httptest.NewRecorder()
	params := url.Values{}
	params.Set("response_type", "code")
	params.Set("redirect_uri", "https://test.secondbit.org/")
	req.URL.RawQuery = params.Encode()
	cookie := &http.Cookie{
		Name:  authCookieName,
		Value: session.ID,
	}
	req.AddCookie(cookie)
	GetAuthorizationCodeHandler(w, req, testContext)
	if w.Code != http.StatusBadRequest {
		t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
	}
	if w.Body.String() != "Client ID must be specified in the request." {
		t.Errorf(`Expected output to be "%s", got "%s" instead.`, "Client ID must be specified in the request.", w.Body.String())
	}
	w = httptest.NewRecorder()
	params.Set("client_id", "Not an ID")
	req.URL.RawQuery = params.Encode()
	GetAuthorizationCodeHandler(w, req, testContext)
	if w.Code != http.StatusBadRequest {
		t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
	}
	if w.Body.String() != "client_id is not a valid Client ID." {
		t.Errorf(`Expected output to be "%s", got "%s" instead.`, "client_id is not a valid Client ID.", w.Body.String())
	}
	w = httptest.NewRecorder()
	params.Set("client_id", uuid.NewID().String())
	req.URL.RawQuery = params.Encode()
	GetAuthorizationCodeHandler(w, req, testContext)
	if w.Code != http.StatusBadRequest {
		t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
	}
	if w.Body.String() != "The specified Client couldn&rsquo;t be found." {
		t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The specified Client couldn&rsquo;t be found.", w.Body.String())
	}
}

func TestGetAuthorizationCodeCodeInvalidURI(t *testing.T) {
	t.Parallel()
	store := NewMemstore()
	testContext := Context{
		template:  template.Must(template.New(getAuthorizationCodeTemplateName).Parse("{{ .error }}")),
		clients:   store,
		authCodes: store,
		profiles:  store,
		tokens:    store,
		sessions:  store,
	}
	client := Client{
		ID:      uuid.NewID(),
		Secret:  "super secret!",
		OwnerID: uuid.NewID(),
		Name:    "My test client",
		Type:    "public",
	}
	err := testContext.SaveClient(client)
	if err != nil {
		t.Fatal("Can't store client:", err)
	}
	session := Session{
		ID:        "testsession",
		Active:    true,
		CSRFToken: "nosurf",
		Expires:   time.Now().Add(time.Hour),
	}
	err = testContext.CreateSession(session)
	if err != nil {
		t.Fatal("Can't store session:", err)
	}
	req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
	if err != nil {
		t.Fatal("Can't build request:", err)
	}
	cookie := &http.Cookie{
		Name:  authCookieName,
		Value: session.ID,
	}
	req.AddCookie(cookie)
	w := httptest.NewRecorder()
	params := url.Values{}
	params.Set("response_type", "code")
	params.Set("client_id", client.ID.String())
	req.URL.RawQuery = params.Encode()
	GetAuthorizationCodeHandler(w, req, testContext)
	if w.Code != http.StatusBadRequest {
		t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
	}
	if w.Body.String() != "The redirect_uri specified is not valid." {
		t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The redirect_uri specified is not valid.", w.Body.String())
	}
	endpoint := Endpoint{
		ID:       uuid.NewID(),
		ClientID: client.ID,
		URI:      "https://test.secondbit.org/redirect",
		Added:    time.Now(),
	}
	err = testContext.AddEndpoints(client.ID, []Endpoint{endpoint})
	if err != nil {
		t.Fatal("Can't store endpoint:", err)
	}
	w = httptest.NewRecorder()
	params.Set("redirect_uri", "https://test.secondbit.org/wrong")
	req.URL.RawQuery = params.Encode()
	GetAuthorizationCodeHandler(w, req, testContext)
	if w.Code != http.StatusBadRequest {
		t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
	}
	if w.Body.String() != "The redirect_uri specified is not valid." {
		t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The redirect_uri specified is not valid.", w.Body.String())
	}
	endpoint2 := Endpoint{
		ID:       uuid.NewID(),
		ClientID: client.ID,
		URI:      "https://test.secondbit.org/redirect",
		Added:    time.Now(),
	}
	err = testContext.AddEndpoints(client.ID, []Endpoint{endpoint2})
	if err != nil {
		t.Fatal("Can't store endpoint:", err)
	}
	w = httptest.NewRecorder()
	params.Set("redirect_uri", "")
	req.URL.RawQuery = params.Encode()
	GetAuthorizationCodeHandler(w, req, testContext)
	if w.Code != http.StatusBadRequest {
		t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
	}
	if w.Body.String() != "The redirect_uri specified is not valid." {
		t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The redirect_uri specified is not valid.", w.Body.String())
	}
	w = httptest.NewRecorder()
	params.Set("redirect_uri", "://not a URL")
	req.URL.RawQuery = params.Encode()
	GetAuthorizationCodeHandler(w, req, testContext)
	if w.Code != http.StatusBadRequest {
		t.Errorf("Expected status code to be %d, got %d", http.StatusBadRequest, w.Code)
	}
	if w.Body.String() != "The redirect_uri specified is not valid." {
		t.Errorf(`Expected output to be "%s", got "%s" instead.`, "The redirect_uri specified is not valid.", w.Body.String())
	}
	// BUG(paddy): Need to test that setting redirect_uri to a non-URL redirect_uri returns the correct error.
}

func TestGetAuthorizationCodeCodeInvalidResponseType(t *testing.T) {
	t.Parallel()
	store := NewMemstore()
	testContext := Context{
		template:  template.Must(template.New(getAuthorizationCodeTemplateName).Parse("{{ .error }}")),
		clients:   store,
		authCodes: store,
		profiles:  store,
		tokens:    store,
		sessions:  store,
	}
	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:    "public",
	}
	endpoint := Endpoint{
		ID:       uuid.NewID(),
		ClientID: client.ID,
		URI:      "https://test.secondbit.org/redirect",
		Added:    time.Now(),
	}
	err := testContext.SaveClient(client)
	if err != nil {
		t.Fatal("Can't store client:", err)
	}
	err = testContext.AddEndpoints(client.ID, []Endpoint{endpoint})
	if err != nil {
		t.Fatal("Can't store endpoint:", err)
	}
	session := Session{
		ID:        "testsession",
		Active:    true,
		CSRFToken: "nosurf",
		Expires:   time.Now().Add(time.Hour),
	}
	err = testContext.CreateSession(session)
	if err != nil {
		t.Fatal("Can't store session:", err)
	}
	req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
	if err != nil {
		t.Fatal("Can't build request:", err)
	}
	cookie := &http.Cookie{
		Name:  authCookieName,
		Value: session.ID,
	}
	req.AddCookie(cookie)
	params := url.Values{}
	params.Set("response_type", "totally not code")
	params.Set("client_id", client.ID.String())
	params.Set("redirect_uri", endpoint.URI)
	params.Set("scope", "testscope")
	params.Set("state", "my super secure state string")
	req.URL.RawQuery = params.Encode()
	w := httptest.NewRecorder()
	GetAuthorizationCodeHandler(w, req, testContext)
	if w.Code != http.StatusFound {
		t.Errorf("Expected status code to be %d, got %d", http.StatusFound, w.Code)
	}
	redirectedTo := w.Header().Get("Location")
	red, err := url.Parse(redirectedTo)
	if err != nil {
		t.Fatalf("Being redirected to a non-URL (%s) threw error: %s\n", redirectedTo, err)
	}
	if red.Query().Get("error") != "invalid_request" {
		t.Errorf(`Expected error param in redirect URL to be "%s", got "%s"`, "invalid_request", red.Query().Get("error"))
	}
	stripParam("error", red)
	if red.Query().Get("state") != params.Get("state") {
		t.Errorf(`Expected state param in redirect URL to be "%s", got "%s"`, params.Get("state"), red.Query().Get("state"))
	}
	stripParam("state", red)
	if red.String() != endpoint.URI {
		t.Errorf(`Expected redirect URL to be "%s", got "%s"`, endpoint.URI, red.String())
	}
	stripParam("response_type", req.URL)
	w = httptest.NewRecorder()
	GetAuthorizationCodeHandler(w, req, testContext)
	if w.Code != http.StatusFound {
		t.Errorf("Expected status code to be %d, got %d", http.StatusFound, w.Code)
	}
	redirectedTo = w.Header().Get("Location")
	red, err = url.Parse(redirectedTo)
	if err != nil {
		t.Fatalf("Being redirected to a non-URL (%s) threw error: %s\n", redirectedTo, err)
	}
	if red.Query().Get("error") != "invalid_request" {
		t.Errorf(`Expected error param in redirect URL to be "%s", got "%s"`, "invalid_request", red.Query().Get("error"))
	}
	stripParam("error", red)
	if red.Query().Get("state") != params.Get("state") {
		t.Errorf(`Expected state param in redirect URL to be "%s", got "%s"`, params.Get("state"), red.Query().Get("state"))
	}
	stripParam("state", red)
	if red.String() != endpoint.URI {
		t.Errorf(`Expected redirect URL to be "%s", got "%s"`, endpoint.URI, red.String())
	}
}

func TestGetAuthorizationCodeCodeDenied(t *testing.T) {
	t.Parallel()
	store := NewMemstore()
	testContext := Context{
		template:  template.Must(template.New(getAuthorizationCodeTemplateName).Parse("{{ .error }}")),
		clients:   store,
		authCodes: store,
		profiles:  store,
		tokens:    store,
		sessions:  store,
		scopes:    store,
	}
	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:    "public",
	}
	endpoint := Endpoint{
		ID:       uuid.NewID(),
		ClientID: client.ID,
		URI:      "https://test.secondbit.org/redirect",
		Added:    time.Now(),
	}
	err := testContext.SaveClient(client)
	if err != nil {
		t.Fatal("Can't store client:", err)
	}
	err = testContext.AddEndpoints(client.ID, []Endpoint{endpoint})
	if err != nil {
		t.Fatal("Can't store endpoint:", err)
	}
	session := Session{
		ID:        "testsession",
		Active:    true,
		CSRFToken: "nosurf",
		Expires:   time.Now().Add(time.Hour),
	}
	err = testContext.CreateSession(session)
	if err != nil {
		t.Fatal("Can't store session:", err)
	}
	scope := Scope{
		ID:          "testscope",
		Name:        "Test Scope",
		Description: "High five fabrication.",
	}
	err = testContext.CreateScopes([]Scope{scope})
	if err != nil {
		t.Fatal("Can't create scope:", err)
	}
	req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
	if err != nil {
		t.Fatal("Can't build request:", err)
	}
	cookie := &http.Cookie{
		Name:  authCookieName,
		Value: session.ID,
	}
	req.AddCookie(cookie)
	params := url.Values{}
	params.Set("response_type", "code")
	params.Set("client_id", client.ID.String())
	params.Set("redirect_uri", endpoint.URI)
	params.Set("scope", "testscope")
	params.Set("state", "my super secure state string")
	data := url.Values{}
	data.Set("grant", "denied")
	data.Set("csrftoken", session.CSRFToken)
	req.URL.RawQuery = params.Encode()
	req.Body = ioutil.NopCloser(bytes.NewBufferString(data.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Method = "POST"
	w := httptest.NewRecorder()
	GetAuthorizationCodeHandler(w, req, testContext)
	if w.Code != http.StatusFound {
		t.Errorf("Expected status code to be %d, got %d", http.StatusFound, w.Code)
	}
	redirectedTo := w.Header().Get("Location")
	red, err := url.Parse(redirectedTo)
	if err != nil {
		t.Fatalf("Being redirected to a non-URL (%s) threw error: %s\n", redirectedTo, err)
	}
	if red.Query().Get("error") != "access_denied" {
		t.Errorf(`Expected error param in redirect URL to be "%s", got "%s"`, "access_denied", red.Query().Get("error"))
	}
	stripParam("error", red)
	if red.Query().Get("state") != params.Get("state") {
		t.Errorf(`Expected state param in redirect URL to be "%s", got "%s"`, params.Get("state"), red.Query().Get("state"))
	}
	stripParam("state", red)
	if red.String() != endpoint.URI {
		t.Errorf(`Expected redirect URL to be "%s", got "%s"`, endpoint.URI, red.String())
	}
}

func TestGetAuthorizationCodeCodeLoginRedirect(t *testing.T) {
	t.Parallel()
	req, err := http.NewRequest("GET", "https://test.auth.secondbit.org/oauth2/grant", nil)
	if err != nil {
		t.Fatal("Can't build request:", err)
	}
	w := httptest.NewRecorder()
	GetAuthorizationCodeHandler(w, req, Context{template: template.Must(template.New(getAuthorizationCodeTemplateName).Parse("{{ .internal_error }}"))})
	if w.Code != http.StatusInternalServerError {
		t.Errorf("Expected status code to be %d, got %d", http.StatusInternalServerError, w.Code)
	}
	expectation := "Missing login URL."
	if w.Body.String() != expectation {
		t.Errorf(`Expected body to be "%s", got "%s"`, expectation, w.Body.String())
	}
	uri, err := url.Parse("https://client.secondbit.org/")
	if err != nil {
		t.Error("Unexpected error", err)
	}
	testContext := Context{
		loginURI: uri,
	}
	w = httptest.NewRecorder()
	GetAuthorizationCodeHandler(w, req, testContext)
	if w.Code != http.StatusFound {
		t.Errorf("Expected status code to be %d, got %d", http.StatusFound, w.Code)
	}
	redirectedTo := w.Header().Get("Location")
	expectation = "https://client.secondbit.org/?from=https%3A%2F%2Ftest.auth.secondbit.org%2Foauth2%2Fgrant"
	if redirectedTo != expectation {
		t.Errorf("Expected to be redirected to '%s', was redirected to '%s'", expectation, redirectedTo)
	}
}

// BUG(paddy): Need to test for implicit grant flow

func TestCheckCookie(t *testing.T) {
	t.Parallel()
	req, err := http.NewRequest("GET", "https://auth.secondbit.org", nil)
	if err != nil {
		t.Error("Unexpected error creating base request:", err)
	}
	store := NewMemstore()
	testContext := Context{
		sessions: store,
	}
	session, err := checkCookie(req, testContext)
	if err != ErrNoSession {
		t.Errorf("Expected ErrNoSession, got %s", err)
	}
	session = Session{
		ID:        "testsession",
		Active:    true,
		CSRFToken: "nosurf",
		Expires:   time.Now().Add(time.Hour),
	}
	err = testContext.CreateSession(session)
	if err != nil {
		t.Error("Unexpected error persisting session:", err)
	}
	invalidSession := Session{
		ID:     "testsession2",
		Active: false,
	}
	err = testContext.CreateSession(invalidSession)
	if err != nil {
		t.Error("Unexpected error persisting session:", err)
	}
	result, err := checkCookie(req, testContext)
	if err != ErrNoSession {
		t.Errorf("Expected ErrNoSession, got %s", err)
	}
	req.AddCookie(&http.Cookie{
		Name:  "wrongcookie",
		Value: "wrong value",
	})
	result, err = checkCookie(req, testContext)
	if err != ErrNoSession {
		t.Error("Expected ErrNoSession, got", err)
	}
	req, err = http.NewRequest("GET", "https://auth.secondbit.org", nil)
	if err != nil {
		t.Error("Unexpected error creating base request:", err)
	}
	req.AddCookie(&http.Cookie{
		Name:  "Stillwrongcookie",
		Value: session.ID,
	})
	result, err = checkCookie(req, testContext)
	if err != ErrNoSession {
		t.Error("Expected ErrNoSession, got", err)
	}
	req, err = http.NewRequest("GET", "https://auth.secondbit.org", nil)
	if err != nil {
		t.Error("Unexpected error creating base request:", err)
	}
	req.AddCookie(&http.Cookie{
		Name:  authCookieName,
		Value: "wrong value",
	})
	result, err = checkCookie(req, testContext)
	if err != ErrInvalidSession {
		t.Error("Expected ErrInvalidSession, got", err)
	}
	req, err = http.NewRequest("GET", "https://auth.secondbit.org", nil)
	if err != nil {
		t.Error("Unexpected error creating base request:", err)
	}
	req.AddCookie(&http.Cookie{
		Name:  authCookieName,
		Value: invalidSession.ID,
	})
	result, err = checkCookie(req, testContext)
	if err != ErrInvalidSession {
		t.Error("Expected ErrInvalidSession, got", err)
	}
	req, err = http.NewRequest("GET", "https://auth.secondbit.org", nil)
	if err != nil {
		t.Error("Unexpected error creating base request:", err)
	}
	req.AddCookie(&http.Cookie{
		Name:  authCookieName,
		Value: session.ID,
	})
	result, err = checkCookie(req, testContext)
	if err != nil {
		t.Error("Unexpected error:", err)
	}
	success, field, expectation, outcome := compareSessions(session, result)
	if !success {
		t.Errorf(`Expected field %s to be %v, but got %v`, field, expectation, outcome)
	}
}

func TestBuildLoginRedirect(t *testing.T) {
	t.Parallel()
	req, err := http.NewRequest("GET", "https://client.secondbit.org/my/awesome/path?has=query&params=to&screw=this&all=up", nil)
	if err != nil {
		t.Error("Unexpected error creating base request:", err)
	}
	result := buildLoginRedirect(req, Context{})
	if result != "" {
		t.Error("Expected empty string as the result, got", result)
	}
	uri, err := url.Parse("https://auth.secondbit.org/login?query=string&other=param")
	if err != nil {
		t.Error("Unexpected error parsing URL:", err)
	}
	c := Context{loginURI: uri}
	result = buildLoginRedirect(req, c)
	expectation := "https://auth.secondbit.org/login?from=https%3A%2F%2Fclient.secondbit.org%2Fmy%2Fawesome%2Fpath%3Fhas%3Dquery%26params%3Dto%26screw%3Dthis%26all%3Dup&other=param&query=string"
	if result != expectation {
		t.Errorf(`Expected result string to be "%s", was "%s"`, expectation, result)
	}
}

func TestAuthenticateHelper(t *testing.T) {
	t.Parallel()
	store := NewMemstore()
	context := Context{
		profiles: store,
	}
	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(),
		LastSeen:               time.Time{},
	}
	login := Login{
		Type:      "email",
		Value:     "test@example.com",
		ProfileID: profile.ID,
		Created:   time.Now(),
		LastUsed:  time.Time{},
	}
	err := context.SaveProfile(profile)
	if err != nil {
		t.Error("Error saving profile:", err)
	}
	err = context.AddLogin(login)
	if err != nil {
		t.Error("Error adding login:", err)
	}
	response, err := authenticate("test@example.com", "mysecurepassphrase", context)
	if err != nil {
		t.Error("Unexpected error:", err)
	}
	success, field, expectation, result := compareProfiles(profile, response)
	if !success {
		t.Errorf(`Expected field %s to be "%v", got "%v"`, field, expectation, result)
	}
	response, err = authenticate("test2@example.com", "mysecurepassphrase", context)
	if err != ErrIncorrectAuth {
		t.Error("Expected ErrIncorrectAuth, got", err)
	}
	response, err = authenticate("test@example.com", "not the right password", context)
	if err != ErrIncorrectAuth {
		t.Error("Expected ErrIncorrectAuth, got", err)
	}
	scheme := 1000
	phrase := "doesn't really matter, the scheme doesn't exist"
	change := ProfileChange{
		PassphraseScheme: &scheme,
		Passphrase:       &phrase,
	}
	err = context.UpdateProfile(profile.ID, change)
	if err != nil {
		t.Error("Unexpected error:", err)
	}
	response, err = authenticate("test@example.com", "not the right password", context)
	if err != ErrInvalidPassphraseScheme {
		t.Error("Expected ErrIncorrectAuth, got", err)
	}
}

func TestGetTokenHandler(t *testing.T) {
	t.Parallel()
	store := NewMemstore()
	context := Context{
		clients:   store,
		authCodes: store,
		tokens:    store,
	}
	client := Client{
		ID:      uuid.NewID(),
		Secret:  "sometimes I feel like I don't know what I'm doing",
		OwnerID: uuid.NewID(),
		Name:    "A Super Awesome Client!",
		Logo:    "https://logos.secondbit.org/client.png",
		Website: "https://client.secondbit.org/",
		Type:    "confidential",
	}
	authCode := AuthorizationCode{
		Code:        "testcode",
		Created:     time.Now(),
		ExpiresIn:   600,
		ClientID:    client.ID,
		Scopes:      []string{"testscope"},
		RedirectURI: "https://client.secondbit.org/",
		State:       "teststate",
		ProfileID:   uuid.NewID(),
	}
	err := context.SaveAuthorizationCode(authCode)
	if err != nil {
		t.Error("Error saving auth code:", err)
	}
	err = context.SaveClient(client)
	if err != nil {
		t.Error("Error saving client:", err)
	}
	// BUG(paddy): We're only testing that GetTokenHandler returns the right values when we have the right input. But what about when we have the wrong input? We should test for invalid client errors and invalid grant errors to make sure they're triggered.
	data := url.Values{}
	data.Set("grant_type", "authorization_code")
	data.Set("code", authCode.Code)
	data.Set("redirect_uri", authCode.RedirectURI)
	body := bytes.NewBufferString(data.Encode())
	req, err := http.NewRequest("POST", "https://auth.secondbit.org/", ioutil.NopCloser(body))
	if err != nil {
		t.Error("Error constructing request:", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.SetBasicAuth(client.ID.String(), client.Secret)
	w := httptest.NewRecorder()
	GetTokenHandler(w, req, context)
	resp := tokenResponse{}
	err = json.Unmarshal(w.Body.Bytes(), &resp)
	if err != nil {
		t.Error("Error unmarshalling response:", err)
		t.Error("Response:", w.Body.String())
	}
	if resp.AccessToken == "" {
		t.Error("Got blank access token back")
	}
	if resp.RefreshToken == "" {
		t.Error("Got blank refresh token back")
	}
	if resp.TokenType == "" {
		t.Error("Got blank token type back")
	}
	if resp.ExpiresIn == 0 {
		t.Error("Got blank expires in back")
	}
	tokens, err := context.GetTokensByProfileID(authCode.ProfileID, 1, 0)
	if err != nil {
		t.Error("Error retrieving token:", err)
	}
	if len(tokens) != 1 {
		t.Fatalf("Expected %d tokens, got %d", 1, len(tokens))
	}
	if tokens[0].AccessToken != resp.AccessToken {
		t.Errorf(`Expected access token to be "%s", got "%s"`, tokens[0].AccessToken, resp.AccessToken)
	}
	if tokens[0].RefreshToken != resp.RefreshToken {
		t.Errorf(`Expected refresh token to be "%s", got "%s"`, tokens[0].RefreshToken, resp.RefreshToken)
	}
	if tokens[0].ExpiresIn != resp.ExpiresIn {
		t.Errorf(`Expected expires in to be %d, got %d`, tokens[0].ExpiresIn, resp.ExpiresIn)
	}
	if tokens[0].TokenType != resp.TokenType {
		t.Errorf(`Expected token type to be %s, got %s`, tokens[0].TokenType, resp.TokenType)
	}
	// BUG(paddy): We need to test for the refresh_token grant type, too.
	// BUG(paddy): We need to test for the password grant type, too.
	// BUG(paddy): We need to test for the client_credentials grant type, too.
}
