// Copyright 2016 The go-github AUTHORS. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build integration

package integration

import (
	"context"
	"math/rand"
	"os"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/google/go-github/v28/github"
)

const msgEnvMissing = "Skipping test because the required environment variable (%v) is not present."
const envKeyGitHubUsername = "GITHUB_USERNAME"
const envKeyGitHubPassword = "GITHUB_PASSWORD"
const envKeyClientID = "GITHUB_CLIENT_ID"
const envKeyClientSecret = "GITHUB_CLIENT_SECRET"
const InvalidTokenValue = "iamnotacroken"

// TestAuthorizationsBasicOperations tests the basic CRUD operations of the API (mostly for
// the Personal Access Token scenario).
func TestAuthorizationsBasicOperations(t *testing.T) {

	client := getUserPassClient(t)

	auths, resp, err := client.Authorizations.List(context.Background(), nil)
	failOnError(t, err)
	failIfNotStatusCode(t, resp, 200)

	initialAuthCount := len(auths)

	authReq := generatePersonalAuthTokenRequest()

	createdAuth, resp, err := client.Authorizations.Create(context.Background(), authReq)
	failOnError(t, err)
	failIfNotStatusCode(t, resp, 201)

	if *authReq.Note != *createdAuth.Note {
		t.Fatal("Returned Authorization does not match the requested Authorization.")
	}

	auths, resp, err = client.Authorizations.List(context.Background(), nil)
	failOnError(t, err)
	failIfNotStatusCode(t, resp, 200)

	if len(auths) != initialAuthCount+1 {
		t.Fatalf("The number of Authorizations should have increased. Expected [%v], was [%v]", initialAuthCount+1, len(auths))
	}

	// Test updating the authorization
	authUpdate := new(github.AuthorizationUpdateRequest)
	authUpdate.Note = github.String("Updated note: " + randString())

	updatedAuth, resp, err := client.Authorizations.Edit(context.Background(), *createdAuth.ID, authUpdate)
	failOnError(t, err)
	failIfNotStatusCode(t, resp, 200)

	if *updatedAuth.Note != *authUpdate.Note {
		t.Fatal("The returned Authorization does not match the requested updated value.")
	}

	// Verify that the Get operation also reflects the update
	retrievedAuth, resp, err := client.Authorizations.Get(context.Background(), *createdAuth.ID)
	failOnError(t, err)
	failIfNotStatusCode(t, resp, 200)

	if *retrievedAuth.Note != *updatedAuth.Note {
		t.Fatal("The retrieved Authorization does not match the expected (updated) value.")
	}

	// Now, let's delete...
	resp, err = client.Authorizations.Delete(context.Background(), *createdAuth.ID)
	failOnError(t, err)
	failIfNotStatusCode(t, resp, 204)

	// Verify that we can no longer retrieve the auth
	retrievedAuth, resp, err = client.Authorizations.Get(context.Background(), *createdAuth.ID)
	if err == nil {
		t.Fatal("Should have failed due to 404")
	}
	failIfNotStatusCode(t, resp, 404)

	// Verify that our count reset back to the initial value
	auths, resp, err = client.Authorizations.List(context.Background(), nil)
	failOnError(t, err)
	failIfNotStatusCode(t, resp, 200)

	if len(auths) != initialAuthCount {
		t.Fatalf("The number of Authorizations should match the initial count Expected [%v], got [%v]", initialAuthCount, len(auths))
	}

}

// TestAuthorizationsAppOperations tests the application/token related operations, such
// as creating, testing, resetting and revoking application OAuth tokens.
func TestAuthorizationsAppOperations(t *testing.T) {

	userAuthenticatedClient := getUserPassClient(t)

	appAuthenticatedClient := getOAuthAppClient(t)

	// We know these vars are set because getOAuthAppClient would have
	// skipped the test by now
	clientID := os.Getenv(envKeyClientID)
	clientSecret := os.Getenv(envKeyClientSecret)

	authRequest := generateAppAuthTokenRequest(clientID, clientSecret)

	createdAuth, resp, err := userAuthenticatedClient.Authorizations.GetOrCreateForApp(context.Background(), clientID, authRequest)
	failOnError(t, err)
	failIfNotStatusCode(t, resp, 201)

	// Quick sanity check:
	if *createdAuth.Note != *authRequest.Note {
		t.Fatal("The returned auth does not match expected value.")
	}

	// Let's try the same request again, this time it should return the same
	// auth instead of creating a new one
	secondAuth, resp, err := userAuthenticatedClient.Authorizations.GetOrCreateForApp(context.Background(), clientID, authRequest)
	failOnError(t, err)
	failIfNotStatusCode(t, resp, 200)

	// Verify that the IDs are the same
	if *createdAuth.ID != *secondAuth.ID {
		t.Fatalf("The ID of the second returned auth should be the same as the first. Expected [%v], got [%v]", createdAuth.ID, secondAuth.ID)
	}

	// Verify the token
	appAuth, resp, err := appAuthenticatedClient.Authorizations.Check(context.Background(), clientID, *createdAuth.Token)
	failOnError(t, err)
	failIfNotStatusCode(t, resp, 200)

	// Quick sanity check
	if *appAuth.ID != *createdAuth.ID || *appAuth.Token != *createdAuth.Token {
		t.Fatal("The returned auth/token does not match.")
	}

	// Let's verify that we get a 404 for a non-existent token
	_, resp, err = appAuthenticatedClient.Authorizations.Check(context.Background(), clientID, InvalidTokenValue)
	if err == nil {
		t.Fatal("An error should have been returned because of the invalid token.")
	}
	failIfNotStatusCode(t, resp, 404)

	// Let's reset the token
	resetAuth, resp, err := appAuthenticatedClient.Authorizations.Reset(context.Background(), clientID, *createdAuth.Token)
	failOnError(t, err)
	failIfNotStatusCode(t, resp, 200)

	// Let's verify that we get a 404 for a non-existent token
	_, resp, err = appAuthenticatedClient.Authorizations.Reset(context.Background(), clientID, InvalidTokenValue)
	if err == nil {
		t.Fatal("An error should have been returned because of the invalid token.")
	}
	failIfNotStatusCode(t, resp, 404)

	// Verify that the token has changed
	if resetAuth.Token == createdAuth.Token {
		t.Fatal("The reset token should be different from the original.")
	}

	// Verify that we do have a token value
	if *resetAuth.Token == "" {
		t.Fatal("A token value should have been returned.")
	}

	// Verify that the original token is now invalid
	_, resp, err = appAuthenticatedClient.Authorizations.Check(context.Background(), clientID, *createdAuth.Token)
	if err == nil {
		t.Fatal("The original token should be invalid.")
	}
	failIfNotStatusCode(t, resp, 404)

	// Check that the reset token is valid
	_, resp, err = appAuthenticatedClient.Authorizations.Check(context.Background(), clientID, *resetAuth.Token)
	failOnError(t, err)
	failIfNotStatusCode(t, resp, 200)

	// Let's revoke the token
	resp, err = appAuthenticatedClient.Authorizations.Revoke(context.Background(), clientID, *resetAuth.Token)
	failOnError(t, err)
	failIfNotStatusCode(t, resp, 204)

	// Sleep for two seconds... I've seen cases where the revocation appears not
	// to have take place immediately.
	time.Sleep(time.Second * 2)

	// Now, the reset token should also be invalid
	_, resp, err = appAuthenticatedClient.Authorizations.Check(context.Background(), clientID, *resetAuth.Token)
	if err == nil {
		t.Fatal("The reset token should be invalid.")
	}
	failIfNotStatusCode(t, resp, 404)
}

// generatePersonalAuthTokenRequest is a helper function that generates an
// AuthorizationRequest for a Personal Access Token (no client id).
func generatePersonalAuthTokenRequest() *github.AuthorizationRequest {

	rand := randString()
	auth := github.AuthorizationRequest{
		Note:        github.String("Personal token: Note generated by test: " + rand),
		Scopes:      []github.Scope{github.ScopePublicRepo},
		Fingerprint: github.String("Personal token: Fingerprint generated by test: " + rand),
	}

	return &auth
}

// generatePersonalAuthTokenRequest is a helper function that generates an
// AuthorizationRequest for an OAuth application Token (uses client id).
func generateAppAuthTokenRequest(clientID string, clientSecret string) *github.AuthorizationRequest {

	rand := randString()
	auth := github.AuthorizationRequest{
		Note:         github.String("App token: Note generated by test: " + rand),
		Scopes:       []github.Scope{github.ScopePublicRepo},
		Fingerprint:  github.String("App token: Fingerprint generated by test: " + rand),
		ClientID:     github.String(clientID),
		ClientSecret: github.String(clientSecret),
	}

	return &auth
}

// randString returns a (kinda) random string for uniqueness purposes.
func randString() string {
	return strconv.FormatInt(rand.NewSource(time.Now().UnixNano()).Int63(), 10)
}

// failOnError invokes t.Fatal() if err is present.
func failOnError(t *testing.T, err error) {

	if err != nil {
		t.Fatal(err)
	}
}

// failIfNotStatusCode invokes t.Fatal() if the response's status code doesn't match the expected code.
func failIfNotStatusCode(t *testing.T, resp *github.Response, expectedCode int) {

	if resp.StatusCode != expectedCode {
		t.Fatalf("Expected HTTP status code [%v] but received [%v]", expectedCode, resp.StatusCode)
	}

}

// getUserPassClient returns a GitHub client for authorization testing. The client
// uses BasicAuth via GH username and password passed in environment variables
// (and will skip the calling test if those vars are not present).
func getUserPassClient(t *testing.T) *github.Client {
	username, ok := os.LookupEnv(envKeyGitHubUsername)
	if !ok {
		t.Skipf(msgEnvMissing, envKeyGitHubUsername)
	}

	password, ok := os.LookupEnv(envKeyGitHubPassword)
	if !ok {
		t.Skipf(msgEnvMissing, envKeyGitHubPassword)
	}

	tp := github.BasicAuthTransport{
		Username: strings.TrimSpace(username),
		Password: strings.TrimSpace(password),
	}

	return github.NewClient(tp.Client())
}

// getOAuthAppClient returns a GitHub client for authorization testing. The client
// uses BasicAuth, but instead of username and password, it uses the client id
// and client secret passed in via environment variables
// (and will skip the calling test if those vars are not present). Certain API operations (check
// an authorization; reset an authorization; revoke an authorization for an app)
// require this authentication mechanism.
//
// See GitHub API docs: https://developer.com/v3/oauth_authorizations/#check-an-authorization
func getOAuthAppClient(t *testing.T) *github.Client {

	username, ok := os.LookupEnv(envKeyClientID)
	if !ok {
		t.Skipf(msgEnvMissing, envKeyClientID)
	}

	password, ok := os.LookupEnv(envKeyClientSecret)
	if !ok {
		t.Skipf(msgEnvMissing, envKeyClientSecret)
	}

	tp := github.BasicAuthTransport{
		Username: strings.TrimSpace(username),
		Password: strings.TrimSpace(password),
	}

	return github.NewClient(tp.Client())
}
