// 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/v30/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 envKeyAccessToken = "GITHUB_ACCESS_TOKEN"
const InvalidTokenValue = "iamnotacroken"

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

	appAuthenticatedClient := getOAuthAppClient(t)

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

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

	// Quick sanity check
	if *appAuth.Token != accessToken {
		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, accessToken)
	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 == accessToken {
		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, accessToken)
	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())
}
