// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package impersonate_test

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

	"cloud.google.com/go/auth"
	"cloud.google.com/go/auth/credentials"
	"cloud.google.com/go/auth/credentials/idtoken"
	"cloud.google.com/go/auth/credentials/impersonate"
	"cloud.google.com/go/auth/internal/credsfile"
	"cloud.google.com/go/auth/internal/testutil"
	"cloud.google.com/go/auth/internal/testutil/testgcs"
)

const (
	envProjectID   = "GCLOUD_TESTS_GOLANG_PROJECT_ID"
	envReaderCreds = "GCLOUD_TESTS_IMPERSONATE_READER_KEY"
	envReaderEmail = "GCLOUD_TESTS_IMPERSONATE_READER_EMAIL"
	envWriterEmail = "GCLOUD_TESTS_IMPERSONATE_WRITER_EMAIL"
)

var (
	baseKeyFile   string
	readerKeyFile string
	readerEmail   string
	writerEmail   string
	projectID     string
	random        *rand.Rand
)

func TestMain(m *testing.M) {
	flag.Parse()
	random = rand.New(rand.NewSource(time.Now().UnixNano()))
	baseKeyFile = os.Getenv(credsfile.GoogleAppCredsEnvVar)
	projectID = os.Getenv(envProjectID)
	readerKeyFile = os.Getenv(envReaderCreds)
	readerEmail = os.Getenv(envReaderEmail)
	writerEmail = os.Getenv(envWriterEmail)

	if !testing.Short() {
		missing := []string{}
		if baseKeyFile == "" {
			missing = append(missing, credsfile.GoogleAppCredsEnvVar)
		}
		if projectID == "" {
			missing = append(missing, envProjectID)
		}
		if readerKeyFile == "" {
			missing = append(missing, envReaderCreds)
		}
		if readerEmail == "" {
			missing = append(missing, envReaderEmail)
		}
		if writerEmail == "" {
			missing = append(missing, envWriterEmail)
		}
		if len(missing) > 0 {
			log.Printf("skipping, required environment variable(s) not set: %s\n", missing)
			os.Exit(0)
		}
	}
	os.Exit(m.Run())
}

func TestNewCredentialsIntegration(t *testing.T) {
	testutil.IntegrationTestCheck(t)
	tests := []struct {
		name            string
		baseKeyFile     string
		delegates       []string
		useDefaultCreds bool
	}{
		{
			name:        "SA -> SA",
			baseKeyFile: readerKeyFile,
		},
		{
			name:            "SA -> SA (Default)",
			baseKeyFile:     readerKeyFile,
			useDefaultCreds: true,
		},
		{
			name:        "SA -> Delegate -> SA",
			baseKeyFile: baseKeyFile,
			delegates:   []string{readerEmail},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := context.Background()
			var baseCreds *auth.Credentials
			if !tt.useDefaultCreds {
				var err error
				baseCreds, err = credentials.DetectDefault(&credentials.DetectOptions{
					Scopes:          []string{"https://www.googleapis.com/auth/cloud-platform"},
					CredentialsFile: tt.baseKeyFile,
				})
				if err != nil {
					t.Fatalf("credentials.DetectDefault() = %v", err)
				}
			}

			opts := &impersonate.CredentialsOptions{
				TargetPrincipal: writerEmail,
				Scopes:          []string{"https://www.googleapis.com/auth/devstorage.full_control"},
				Delegates:       tt.delegates,
			}
			if !tt.useDefaultCreds {
				opts.Credentials = baseCreds
			}
			creds, err := impersonate.NewCredentials(opts)
			if err != nil {
				t.Fatalf("failed to create ts: %v", err)
			}
			client := testgcs.NewClient(creds)
			bucketName := fmt.Sprintf("%s-impersonate-test-%d", projectID, random.Int63())
			if err := client.CreateBucket(ctx, projectID, bucketName); err != nil {
				t.Fatalf("error creating bucket: %v", err)
			}
			if err := client.DeleteBucket(ctx, bucketName); err != nil {
				t.Fatalf("unable to cleanup bucket %q: %v", bucketName, err)
			}
		})
	}
}

func TestNewIDTokenCredentialsIntegration(t *testing.T) {
	testutil.IntegrationTestCheck(t)

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

	for _, tt := range tests {
		name := tt.name
		t.Run(name, func(t *testing.T) {
			var baseCreds *auth.Credentials
			if !tt.useDefaultCreds {
				var err error
				baseCreds, err = credentials.DetectDefault(&credentials.DetectOptions{
					Scopes:          []string{"https://www.googleapis.com/auth/cloud-platform"},
					CredentialsFile: tt.baseKeyFile,
				})
				if err != nil {
					t.Fatalf("credentials.DetectDefault() = %v", err)
				}
			}
			aud := "http://example.com/"
			opts := &impersonate.IDTokenOptions{
				TargetPrincipal: writerEmail,
				Audience:        aud,
				Delegates:       tt.delegates,
				IncludeEmail:    true,
			}
			if !tt.useDefaultCreds {
				opts.Credentials = baseCreds
			}
			creds, err := impersonate.NewIDTokenCredentials(opts)
			if err != nil {
				t.Fatalf("failed to create ts: %v", err)
			}
			tok, err := creds.Token(ctx)
			if err != nil {
				t.Fatalf("unable to retrieve Token: %v", err)
			}
			validTok, err := idtoken.Validate(ctx, tok.Value, 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)
			}
		})
	}
}
