package auth

import (
	"bytes"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"
	"time"

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

var authCodeStores = []authorizationCodeStore{NewMemstore()}

func compareAuthorizationCodes(authCode1, authCode2 AuthorizationCode) (success bool, field string, authCode1val, authCode2val interface{}) {
	if authCode1.Code != authCode2.Code {
		return false, "code", authCode1.Code, authCode2.Code
	}
	if !authCode1.Created.Equal(authCode2.Created) {
		return false, "created", authCode1.Created, authCode2.Created
	}
	if authCode1.ExpiresIn != authCode2.ExpiresIn {
		return false, "expires in", authCode1.ExpiresIn, authCode2.ExpiresIn
	}
	if !authCode1.ClientID.Equal(authCode2.ClientID) {
		return false, "client ID", authCode1.ClientID, authCode2.ClientID
	}
	if len(authCode1.Scopes) != len(authCode2.Scopes) {
		return false, "scopes", authCode1.Scopes, authCode2.Scopes
	}
	for pos, scope := range authCode1.Scopes {
		if scope != authCode2.Scopes[pos] {
			return false, "scopes", authCode1.Scopes, authCode2.Scopes
		}
	}
	if authCode1.RedirectURI != authCode2.RedirectURI {
		return false, "redirect URI", authCode1.RedirectURI, authCode2.RedirectURI
	}
	if authCode1.State != authCode2.State {
		return false, "state", authCode1.State, authCode2.State
	}
	if !authCode1.ProfileID.Equal(authCode2.ProfileID) {
		return false, "profile ID", authCode1.ProfileID, authCode2.ProfileID
	}
	if authCode1.Used != authCode2.Used {
		return false, "used", authCode1.Used, authCode2.Used
	}
	return true, "", nil, nil
}

func TestAuthorizationCodeStore(t *testing.T) {
	t.Parallel()
	authCode := AuthorizationCode{
		Code:        "code",
		Created:     time.Now().Round(time.Millisecond),
		ExpiresIn:   180,
		ClientID:    uuid.NewID(),
		Scopes:      []string{"scope"},
		RedirectURI: "redirectURI",
		State:       "state",
	}
	for _, store := range authCodeStores {
		context := Context{authCodes: store}
		err := context.SaveAuthorizationCode(authCode)
		if err != nil {
			t.Errorf("Error saving auth code to %T: %s", store, err)
		}
		err = context.SaveAuthorizationCode(authCode)
		if err != ErrAuthorizationCodeAlreadyExists {
			t.Errorf("Expected ErrAuthorizationCodeAlreadyExists from %T, got %+v", store, err)
		}
		retrieved, err := context.GetAuthorizationCode(authCode.Code)
		if err != nil {
			t.Errorf("Error retrieving auth code from %T: %s", store, err)
		}
		match, field, expectation, result := compareAuthorizationCodes(authCode, retrieved)
		if !match {
			t.Errorf("Expected `%v` in the `%s` field of auth code retrieved from %T, got `%v`", expectation, field, store, result)
		}
		err = context.UseAuthorizationCode(authCode.Code)
		if err != nil {
			t.Errorf("Error retrieving auth code from %T: %s", store, err)
		}
		retrieved, err = context.GetAuthorizationCode(authCode.Code)
		if err != nil {
			t.Errorf("Error retrieving auth code from %T: %s", store, err)
		}
		authCode.Used = true
		match, field, expectation, result = compareAuthorizationCodes(authCode, retrieved)
		if !match {
			t.Errorf("Expected `%v` in the `%s` field of auth code retrieved from %T, got `%v`", expectation, field, store, result)
		}
		err = context.DeleteAuthorizationCode(authCode.Code)
		if err != nil {
			t.Errorf("Error removing auth code from %T: %s", store, err)
		}
		retrieved, err = context.GetAuthorizationCode(authCode.Code)
		if err != ErrAuthorizationCodeNotFound {
			t.Errorf("Expected ErrAuthorizationCodeNotFound from %T, got %+v and %+v", store, retrieved, err)
		}
		err = context.DeleteAuthorizationCode(authCode.Code)
		if err != ErrAuthorizationCodeNotFound {
			t.Errorf("Expected ErrAuthorizationCodeNotFound from %T, got %+v", store, err)
		}
		err = context.UseAuthorizationCode(authCode.Code)
		if err != ErrAuthorizationCodeNotFound {
			t.Errorf("Expected ErrAuthorizationCodeNotFound from %T, got %+v", store, err)
		}
	}
}

func TestAuthCodeGrantValidate(t *testing.T) {
	t.Parallel()
	store := NewMemstore()
	testContext := Context{
		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().Round(time.Millisecond),
	}
	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)
	}
	code := AuthorizationCode{
		Code:        "myauthcode",
		Created:     time.Now().Round(time.Millisecond),
		ExpiresIn:   180,
		ClientID:    uuid.NewID(),
		Scopes:      []string{"scope"},
		RedirectURI: "redirectURI",
		State:       "state",
	}
	err = testContext.SaveAuthorizationCode(code)
	if err != nil {
		t.Fatal("Can't add auth code:", err)
	}
	code2 := code
	code2.Code = "otherauthcode"
	code2.ClientID = client.ID
	err = testContext.SaveAuthorizationCode(code2)
	if err != nil {
		t.Fatal("Can't add second auth code:", err)
	}
	req, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
	if err != nil {
		t.Fatal("Can't build request:", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	w := httptest.NewRecorder()
	params := url.Values{}
	body := bytes.NewBufferString(params.Encode())
	req.Body = ioutil.NopCloser(body)
	scope, profileID, valid := authCodeGrantValidate(w, req, testContext)
	if valid {
		t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
	}
	if w.Code != http.StatusBadRequest {
		t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code)
	}
	expectedBody := `{"error":"invalid_request"}`
	if strings.TrimSpace(w.Body.String()) != expectedBody {
		t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
	}

	req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
	if err != nil {
		t.Fatal("Can't build request:", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	w = httptest.NewRecorder()
	params = url.Values{}
	params.Set("code", "notmycode")
	body = bytes.NewBufferString(params.Encode())
	req.Body = ioutil.NopCloser(body)
	err = req.ParseForm()
	if err != nil {
		t.Log(err)
	}
	scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
	if valid {
		t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
	}
	if w.Code != http.StatusUnauthorized {
		t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
	}
	expectedBody = `{"error":"invalid_client"}`
	if expectedBody != strings.TrimSpace(w.Body.String()) {
		t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
	}

	req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
	if err != nil {
		t.Fatal("Can't build request:", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.SetBasicAuth(client.ID.String(), client.Secret)
	w = httptest.NewRecorder()
	params = url.Values{}
	params.Set("code", "notmycode")
	body = bytes.NewBufferString(params.Encode())
	req.Body = ioutil.NopCloser(body)
	err = req.ParseForm()
	if err != nil {
		t.Log(err)
	}
	scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
	if valid {
		t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
	}
	if w.Code != http.StatusBadRequest {
		t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
	}
	expectedBody = `{"error":"invalid_grant"}`
	if expectedBody != strings.TrimSpace(w.Body.String()) {
		t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
	}

	req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
	if err != nil {
		t.Fatal("Can't build request:", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.SetBasicAuth(client.ID.String(), client.Secret)
	w = httptest.NewRecorder()
	params = url.Values{}
	params.Set("code", code.Code)
	params.Set("redirect_uri", "not my redirectURI")
	body = bytes.NewBufferString(params.Encode())
	req.Body = ioutil.NopCloser(body)
	err = req.ParseForm()
	if err != nil {
		t.Log(err)
	}
	scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
	if valid {
		t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
	}
	if w.Code != http.StatusBadRequest {
		t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
	}
	expectedBody = `{"error":"invalid_grant"}`
	if expectedBody != strings.TrimSpace(w.Body.String()) {
		t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
	}

	req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
	if err != nil {
		t.Fatal("Can't build request:", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.SetBasicAuth(client.ID.String(), client.Secret)
	w = httptest.NewRecorder()
	params = url.Values{}
	params.Set("code", code.Code)
	params.Set("redirect_uri", code.RedirectURI)
	body = bytes.NewBufferString(params.Encode())
	req.Body = ioutil.NopCloser(body)
	err = req.ParseForm()
	if err != nil {
		t.Log(err)
	}
	scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
	if valid {
		t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
	}
	if w.Code != http.StatusBadRequest {
		t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
	}
	expectedBody = `{"error":"invalid_grant"}`
	if expectedBody != strings.TrimSpace(w.Body.String()) {
		t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
	}

	req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
	if err != nil {
		t.Fatal("Can't build request:", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.SetBasicAuth(client.ID.String(), client.Secret)
	w = httptest.NewRecorder()
	params = url.Values{}
	params.Set("code", code2.Code)
	params.Set("redirect_uri", code2.RedirectURI)
	body = bytes.NewBufferString(params.Encode())
	req.Body = ioutil.NopCloser(body)
	err = req.ParseForm()
	if err != nil {
		t.Log(err)
	}
	scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
	if !valid {
		t.Fatalf("Expected valid auth code, was not valid.")
	}
}

func TestAuthCodeGrantInvalidate(t *testing.T) {
	t.Parallel()
	store := NewMemstore()
	testContext := Context{
		clients:   store,
		authCodes: store,
		profiles:  store,
		tokens:    store,
		sessions:  store,
	}
	code := AuthorizationCode{
		Code:        "myauthcode",
		Created:     time.Now().Round(time.Millisecond),
		ExpiresIn:   180,
		ClientID:    uuid.NewID(),
		Scopes:      []string{"scope"},
		RedirectURI: "redirectURI",
		State:       "state",
	}
	err := testContext.SaveAuthorizationCode(code)
	if err != nil {
		t.Fatal("Can't add auth code:", err)
	}
	req, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
	if err != nil {
		t.Fatal("Can't build request:", err)
	}
	err = authCodeGrantInvalidate(req, testContext)
	if err != ErrAuthorizationCodeNotFound {
		t.Errorf("Expected `%s`, got `%+v`", ErrAuthorizationCodeNotFound, err)
	}
	req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
	if err != nil {
		t.Fatal("Can't build request:", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	params := url.Values{}
	params.Set("code", "notmycode")
	body := bytes.NewBufferString(params.Encode())
	req.Body = ioutil.NopCloser(body)
	err = authCodeGrantInvalidate(req, testContext)
	if err != ErrAuthorizationCodeNotFound {
		t.Errorf("Expected `%s`, got `%+v`", ErrAuthorizationCodeNotFound, err)
	}
	req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
	if err != nil {
		t.Fatal("Can't build request:", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	params.Set("code", code.Code)
	body = bytes.NewBufferString(params.Encode())
	req.Body = ioutil.NopCloser(body)
	err = authCodeGrantInvalidate(req, testContext)
	if err != nil {
		t.Error("Error invalidating auth code:", err)
	}
	authCode, err := testContext.GetAuthorizationCode(code.Code)
	if err != nil {
		t.Error("Error retrieving auth code:", err)
	}
	if !authCode.Used {
		t.Error("Expected auth code to be used, was not.")
	}
}
