// Copyright 2016 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 lro supports Long Running Operations for the Google Cloud Libraries.
//
// This package is still experimental and subject to change.
package longrunning

import (
	"context"
	"errors"
	"testing"
	"time"

	pb "cloud.google.com/go/longrunning/autogen/longrunningpb"
	gax "github.com/googleapis/gax-go/v2"
	"github.com/googleapis/gax-go/v2/apierror"
	"google.golang.org/genproto/googleapis/rpc/errdetails"
	rpcstatus "google.golang.org/genproto/googleapis/rpc/status"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/anypb"
	"google.golang.org/protobuf/types/known/durationpb"
)

type getterService struct {
	operationsClient

	// clock represents the fake current time of the service.
	// It is the running sum of the of the duration we have slept.
	clock time.Duration

	// getTimes records the times at which GetOperation is called.
	getTimes []time.Duration

	// results are the fake results that GetOperation should return.
	results []*pb.Operation
}

func (s *getterService) GetOperation(context.Context, *pb.GetOperationRequest, ...gax.CallOption) (*pb.Operation, error) {
	i := len(s.getTimes)
	s.getTimes = append(s.getTimes, s.clock)
	if i >= len(s.results) {
		return nil, errors.New("unexpected call")
	}
	return s.results[i], nil
}

func (s *getterService) sleeper() sleeper {
	return func(_ context.Context, d time.Duration) error {
		s.clock += d
		return nil
	}
}

func TestWait(t *testing.T) {
	responseDur := durationpb.New(42 * time.Second)
	responseAny, err := anypb.New(responseDur)
	if err != nil {
		t.Fatal(err)
	}

	s := &getterService{
		results: []*pb.Operation{
			{Name: "foo"},
			{Name: "foo"},
			{Name: "foo"},
			{Name: "foo"},
			{Name: "foo"},
			{
				Name: "foo",
				Done: true,
				Result: &pb.Operation_Response{
					Response: responseAny,
				},
			},
		},
	}
	op := &Operation{
		c:     s,
		proto: &pb.Operation{Name: "foo"},
	}
	if op.Done() {
		t.Fatal("operation should not have completed yet")
	}

	var resp durationpb.Duration
	bo := gax.Backoff{
		Initial: 1 * time.Second,
		Max:     3 * time.Second,
	}
	if err := op.wait(context.Background(), &resp, &bo, s.sleeper()); err != nil {
		t.Fatal(err)
	}
	if !proto.Equal(&resp, responseDur) {
		t.Errorf("response, got %v, want %v", &resp, responseDur)
	}
	if !op.Done() {
		t.Errorf("operation should have completed")
	}

	maxWait := []time.Duration{
		1 * time.Second,
		2 * time.Second,
		3 * time.Second,
		3 * time.Second,
		3 * time.Second,
	}
	for i := 0; i < len(s.getTimes)-1; i++ {
		w := s.getTimes[i+1] - s.getTimes[i]
		if mw := maxWait[i]; w > mw {
			t.Errorf("backoff, waited %s, max %s", w, mw)
		}
	}
}

func TestPollRequestError(t *testing.T) {
	const opName = "foo"

	// All calls error.
	s := &getterService{}
	op := &Operation{
		c:     s,
		proto: &pb.Operation{Name: opName},
	}
	if err := op.Poll(context.Background(), nil); err == nil {
		t.Fatalf("Poll should error")
	}
	if n := op.Name(); n != opName {
		t.Errorf("operation name, got %q, want %q", n, opName)
	}
	if op.Done() {
		t.Errorf("operation should not have completed; we failed to fetch state")
	}
}

func TestPollErrorResult(t *testing.T) {
	const (
		errCode = codes.NotFound
		errMsg  = "my error"
	)
	details := &errdetails.ErrorInfo{Reason: "things happen"}
	a, err := anypb.New(details)
	if err != nil {
		t.Fatalf("anypb.New() = %v", err)
	}
	op := &Operation{
		proto: &pb.Operation{
			Name: "foo",
			Done: true,
			Result: &pb.Operation_Error{
				Error: &rpcstatus.Status{
					Code:    int32(errCode),
					Message: errMsg,
					Details: []*anypb.Any{a},
				},
			},
		},
	}
	err = op.Poll(context.Background(), nil)
	if got := status.Code(err); got != errCode {
		t.Errorf("error code, want %s, got %s", errCode, got)
	}
	if got := grpc.ErrorDesc(err); got != errMsg {
		t.Errorf("error code, want %s, got %s", errMsg, got)
	}
	if !op.Done() {
		t.Errorf("operation should have completed")
	}
	var ae *apierror.APIError
	errors.As(err, &ae)
	if got := ae.Details().ErrorInfo.Reason; got != details.Reason {
		t.Errorf("got %q, want %q", got, details.Reason)
	}
}

type errService struct {
	operationsClient
	errCancel, errDelete error
}

func (s *errService) CancelOperation(context.Context, *pb.CancelOperationRequest, ...gax.CallOption) error {
	return s.errCancel
}

func (s *errService) DeleteOperation(context.Context, *pb.DeleteOperationRequest, ...gax.CallOption) error {
	return s.errDelete
}

func TestCancelReturnsError(t *testing.T) {
	s := &errService{
		errCancel: errors.New("cancel error"),
	}
	op := &Operation{
		c:     s,
		proto: &pb.Operation{Name: "foo"},
	}
	if got, want := op.Cancel(context.Background()), s.errCancel; got != want {
		t.Errorf("cancel, got error %s, want %s", got, want)
	}
}

func TestDeleteReturnsError(t *testing.T) {
	s := &errService{
		errDelete: errors.New("delete error"),
	}
	op := &Operation{
		c:     s,
		proto: &pb.Operation{Name: "foo"},
	}
	if got, want := op.Delete(context.Background()), s.errDelete; got != want {
		t.Errorf("cancel, got error %s, want %s", got, want)
	}
}
