// Copyright 2021 Google LLC.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package impersonate_test

import (
	"context"
	"flag"
	"fmt"
	"math/rand"
	"os"
	"testing"
	"time"

	admin "google.golang.org/api/admin/directory/v1"
	"google.golang.org/api/idtoken"
	"google.golang.org/api/impersonate"
	"google.golang.org/api/option"

	"google.golang.org/api/storage/v1"
)

var (
	baseKeyFile   string
	readerKeyFile string
	readerEmail   string
	writerEmail   string
	projectID     string
	domain        string
	domainAdmin   string
)

func TestMain(m *testing.M) {
	flag.Parse()
	rand.Seed(time.Now().UnixNano())
	baseKeyFile = os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")
	projectID = os.Getenv("GOOGLE_CLOUD_PROJECT")
	readerKeyFile = os.Getenv("GCLOUD_TESTS_IMPERSONATE_READER_KEY")
	readerEmail = os.Getenv("GCLOUD_TESTS_IMPERSONATE_READER_EMAIL")
	writerEmail = os.Getenv("GCLOUD_TESTS_IMPERSONATE_WRITER_EMAIL")
	domain = os.Getenv("GCLOUD_TESTS_IMPERSONATE_DOMAIN")
	domainAdmin = os.Getenv("GCLOUD_TESTS_IMPERSONATE_DOMAIN_ADMIN")

	os.Exit(m.Run())
}

func validateEnvVars(t *testing.T) {
	t.Helper()
	if baseKeyFile == "" ||
		readerKeyFile == "" ||
		readerEmail == "" ||
		writerEmail == "" ||
		projectID == "" {
		t.Skip("required environment variable not set, skipping")
	}
}

func TestCredentialsTokenSourceIntegration(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test")
	}
	validateEnvVars(t)

	ctx := context.Background()
	tests := []struct {
		name        string
		baseKeyFile string
		delegates   []string
	}{
		{
			name:        "SA -> SA",
			baseKeyFile: readerKeyFile,
		},
		{
			name:        "SA -> Delegate -> SA",
			baseKeyFile: baseKeyFile,
			delegates:   []string{readerEmail},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ts, err := impersonate.CredentialsTokenSource(ctx,
				impersonate.CredentialsConfig{
					TargetPrincipal: writerEmail,
					Scopes:          []string{"https://www.googleapis.com/auth/devstorage.full_control"},
					Delegates:       tt.delegates,
				},
				option.WithAuthCredentialsFile(option.ServiceAccount, tt.baseKeyFile),
			)
			if err != nil {
				t.Fatalf("failed to create ts: %v", err)
			}
			svc, err := storage.NewService(ctx, option.WithTokenSource(ts))
			if err != nil {
				t.Fatalf("failed to create client: %v", err)
			}
			bucketName := fmt.Sprintf("%s-%d", projectID, rand.Int63())
			if _, err := svc.Buckets.Insert(projectID, &storage.Bucket{
				Name: bucketName,
			}).Do(); err != nil {
				t.Fatalf("error creating bucket: %v", err)
			}
			if err := svc.Buckets.Delete(bucketName).Do(); err != nil {
				t.Fatalf("unable to cleanup bucket %q: %v", bucketName, err)
			}
		})
	}
}

func TestIDTokenSourceIntegration(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration test")
	}
	validateEnvVars(t)

	ctx := context.Background()
	tests := []struct {
		name        string
		baseKeyFile string
		delegates   []string
	}{
		{
			name:        "SA -> SA",
			baseKeyFile: readerKeyFile,
		},
		{
			name:        "SA -> Delegate -> SA",
			baseKeyFile: baseKeyFile,
			delegates:   []string{readerEmail},
		},
	}

	for _, tt := range tests {
		name := tt.name
		t.Run(name, func(t *testing.T) {
			aud := "http://example.com/"
			ts, err := impersonate.IDTokenSource(ctx,
				impersonate.IDTokenConfig{
					TargetPrincipal: writerEmail,
					Audience:        aud,
					Delegates:       tt.delegates,
					IncludeEmail:    true,
				},
				option.WithAuthCredentialsFile(option.ServiceAccount, tt.baseKeyFile),
			)
			if err != nil {
				t.Fatalf("failed to create ts: %v", err)
			}
			tok, err := ts.Token()
			if err != nil {
				t.Fatalf("unable to retrieve Token: %v", err)
			}
			validTok, err := idtoken.Validate(context.Background(), tok.AccessToken, aud)
			if err != nil {
				t.Fatalf("token validation failed: %v", err)
			}
			if validTok.Audience != aud {
				t.Fatalf("got %q, want %q", validTok.Audience, aud)
			}
			if validTok.Claims["email"] != writerEmail {
				t.Fatalf("got %q, want %q", validTok.Claims["email"], writerEmail)
			}
		})
	}
}

func TestTokenSourceIntegration_user(t *testing.T) {
	t.Skip("https://github.com/googleapis/google-api-go-client/issues/948")
	if testing.Short() {
		t.Skip("skipping integration test")
	}
	validateEnvVars(t)
	ctx := context.Background()
	tests := []struct {
		name        string
		baseKeyFile string
		delegates   []string
	}{
		{
			name:        "SA -> SA",
			baseKeyFile: readerKeyFile,
		},
		{
			name:        "SA -> Delegate -> SA",
			baseKeyFile: baseKeyFile,
			delegates:   []string{readerEmail},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ts, err := impersonate.CredentialsTokenSource(ctx,
				impersonate.CredentialsConfig{
					TargetPrincipal: writerEmail,
					Delegates:       tt.delegates,
					Scopes:          []string{"https://www.googleapis.com/auth/admin.directory.user", "https://www.googleapis.com/auth/admin.directory.group"},
					Subject:         domainAdmin,
				},
				option.WithAuthCredentialsFile(option.ServiceAccount, baseKeyFile),
			)
			if err != nil {
				t.Fatalf("failed to create ts: %v", err)
			}
			svc, err := admin.NewService(ctx, option.WithTokenSource(ts))
			if err != nil {
				t.Fatalf("failed to create client: %v", err)
			}
			if _, err := svc.Users.List().Domain(domain).Do(); err != nil {
				t.Fatalf("failed to list users: %v", err)
			}
		})
	}
}
