package auth

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

	"code.secondbit.org/uuid"
)

const (
	scopeSet = 1 << iota
	stateSet
	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("Get auth grant")),
		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",
	}
	uri, err := url.Parse("https://test.secondbit.org/redirect")
	if err != nil {
		t.Fatal("Can't parse URL:", err)
	}
	endpoint := Endpoint{
		ID:       uuid.NewID(),
		ClientID: client.ID,
		URI:      *uri,
		Added:    time.Now(),
	}
	err = testContext.SaveClient(client)
	if err != nil {
		t.Fatal("Can't store client:", err)
	}
	err = testContext.AddEndpoint(client.ID, 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,
	}
	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)
	for i := 0; i < 1<<3; 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())
		if i&uriSet != 0 {
			params.Set("redirect_uri", endpoint.URI.String())
		}
		if i&scopeSet != 0 {
			params.Set("scope", "testscope")
		}
		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.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")
		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)
		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.String() {
			t.Errorf(`Expected redirect URL to be "%s", got "%s"`, endpoint.URI.String(), 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,
	}
	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",
	}
	uri, err := url.Parse("https://test.secondbit.org/redirect")
	if err != nil {
		t.Fatal("Can't parse URL:", err)
	}
	err = testContext.SaveClient(client)
	if err != nil {
		t.Fatal("Can't store client:", err)
	}
	session := Session{
		ID:     "testsession",
		Active: true,
	}
	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:      *uri,
		Added:    time.Now(),
	}
	err = testContext.AddEndpoint(client.ID, 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:      *uri,
		Added:    time.Now(),
	}
	err = testContext.AddEndpoint(client.ID, 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())
	}
}

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",
	}
	uri, err := url.Parse("https://test.secondbit.org/redirect")
	if err != nil {
		t.Fatal("Can't parse URL:", err)
	}
	endpoint := Endpoint{
		ID:       uuid.NewID(),
		ClientID: client.ID,
		URI:      *uri,
		Added:    time.Now(),
	}
	err = testContext.SaveClient(client)
	if err != nil {
		t.Fatal("Can't store client:", err)
	}
	err = testContext.AddEndpoint(client.ID, endpoint)
	if err != nil {
		t.Fatal("Can't store endpoint:", err)
	}
	session := Session{
		ID:     "testsession",
		Active: true,
	}
	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.String())
	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.String() {
		t.Errorf(`Expected redirect URL to be "%s", got "%s"`, endpoint.URI.String(), 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.String() {
		t.Errorf(`Expected redirect URL to be "%s", got "%s"`, endpoint.URI.String(), 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,
	}
	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",
	}
	uri, err := url.Parse("https://test.secondbit.org/redirect")
	if err != nil {
		t.Fatal("Can't parse URL:", err)
	}
	endpoint := Endpoint{
		ID:       uuid.NewID(),
		ClientID: client.ID,
		URI:      *uri,
		Added:    time.Now(),
	}
	err = testContext.SaveClient(client)
	if err != nil {
		t.Fatal("Can't store client:", err)
	}
	err = testContext.AddEndpoint(client.ID, endpoint)
	if err != nil {
		t.Fatal("Can't store endpoint:", err)
	}
	session := Session{
		ID:     "testsession",
		Active: true,
	}
	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", "code")
	params.Set("client_id", client.ID.String())
	params.Set("redirect_uri", endpoint.URI.String())
	params.Set("scope", "testscope")
	params.Set("state", "my super secure state string")
	data := url.Values{}
	data.Set("grant", "denied")
	req.URL.RawQuery = params.Encode()
	req.Body = ioutil.NopCloser(bytes.NewBufferString(data.Encode()))
	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.String() {
		t.Errorf(`Expected redirect URL to be "%s", got "%s"`, endpoint.URI.String(), 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)
	}
}

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,
	}
	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:             "febcbe74b9555ab3dd0135bdc3aa86d2ee5c38dd7fd44f7b6e2ea908e93b1362",
		Iterations:             1048576,
		Salt:                   "c0feab6ae682e7f7d14343b669b8afaa3b17ed72e9bb18a73f002be4c6b21686",
		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,
		Scope:       "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)
	}
	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)
	}
}
