<?php
/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

namespace Google\Tests;

use Google\Client;
use Google\Service\Drive;
use Google\AuthHandler\AuthHandlerFactory;
use Google\Auth\FetchAuthTokenCache;
use Google\Auth\GCECache;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Exception\ClientException;
use Prophecy\Argument;
use Psr\Http\Message\RequestInterface;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use ReflectionClass;
use ReflectionMethod;
use InvalidArgumentException;
use Exception;

class ClientTest extends BaseTest
{
  public function testClientConstructor()
  {
    $this->assertInstanceOf(Client::class, $this->getClient());
  }

  public function testSignAppKey()
  {
    $client = $this->getClient();
    $client->setDeveloperKey('devKey');

    $http = new GuzzleClient();
    $client->authorize($http);

    $this->checkAuthHandler($http, 'Simple');
  }

  private function checkAuthHandler($http, $className)
  {
    if ($this->isGuzzle6() || $this->isGuzzle7()) {
      $stack = $http->getConfig('handler');
      $class = new ReflectionClass(get_class($stack));
      $property = $class->getProperty('stack');
      $property->setAccessible(true);
      $middlewares = $property->getValue($stack);
      $middleware = array_pop($middlewares);

      if (null === $className) {
        // only the default middlewares have been added
        $this->assertCount(3, $middlewares);
      } else {
        $authClass = sprintf('Google\Auth\Middleware\%sMiddleware', $className);
        $this->assertInstanceOf($authClass, $middleware[0]);
      }
    } else {
      $listeners = $http->getEmitter()->listeners('before');

      if (null === $className) {
        $this->assertCount(0, $listeners);
      } else {
        $authClass = sprintf('Google\Auth\Subscriber\%sSubscriber', $className);
        $this->assertCount(1, $listeners);
        $this->assertCount(2, $listeners[0]);
        $this->assertInstanceOf($authClass, $listeners[0][0]);
      }
    }
  }

  private function checkCredentials($http, $fetcherClass, $sub = null)
  {
    if ($this->isGuzzle6() || $this->isGuzzle7()) {
      $stack = $http->getConfig('handler');
      $class = new ReflectionClass(get_class($stack));
      $property = $class->getProperty('stack');
      $property->setAccessible(true);
      $middlewares = $property->getValue($stack); // Works
      $middleware = array_pop($middlewares);
      $auth = $middleware[0];
    } else {
      // access the protected $fetcher property
      $listeners = $http->getEmitter()->listeners('before');
      $auth = $listeners[0][0];
    }

    $class = new ReflectionClass(get_class($auth));
    $property = $class->getProperty('fetcher');
    $property->setAccessible(true);
    $cacheFetcher = $property->getValue($auth);
    $this->assertInstanceOf(FetchAuthTokenCache::class, $cacheFetcher);

    $class = new ReflectionClass(get_class($cacheFetcher));
    $property = $class->getProperty('fetcher');
    $property->setAccessible(true);
    $fetcher = $property->getValue($cacheFetcher);
    $this->assertInstanceOf($fetcherClass, $fetcher);

    if ($sub) {
      // access the protected $auth property
      $class = new ReflectionClass(get_class($fetcher));
      $property = $class->getProperty('auth');
      $property->setAccessible(true);
      $auth = $property->getValue($fetcher);

      $this->assertEquals($sub, $auth->getSub());
    }
  }

  public function testSignAccessToken()
  {
    $client = $this->getClient();

    $http = new GuzzleClient();
    $client->setAccessToken([
      'access_token' => 'test_token',
      'expires_in'   => 3600,
      'created'      => time(),
    ]);
    $client->setScopes('test_scope');
    $client->authorize($http);

    $this->checkAuthHandler($http, 'ScopedAccessToken');
  }

  public function testCreateAuthUrl()
  {
    $client = $this->getClient();

    $client->setClientId('clientId1');
    $client->setClientSecret('clientSecret1');
    $client->setRedirectUri('http://localhost');
    $client->setDeveloperKey('devKey');
    $client->setState('xyz');
    $client->setAccessType('offline');
    $client->setApprovalPrompt('force');
    $client->setRequestVisibleActions('http://foo');
    $client->setLoginHint('bob@example.org');

    $authUrl = $client->createAuthUrl("http://googleapis.com/scope/foo");
    $expected = "https://accounts.google.com/o/oauth2/auth"
        . "?response_type=code"
        . "&access_type=offline"
        . "&client_id=clientId1"
        . "&redirect_uri=http%3A%2F%2Flocalhost"
        . "&state=xyz"
        . "&scope=http%3A%2F%2Fgoogleapis.com%2Fscope%2Ffoo"
        . "&approval_prompt=force"
        . "&login_hint=bob%40example.org";

    $this->assertEquals($expected, $authUrl);

    // Again with a blank login hint (should remove all traces from authUrl)
    $client->setLoginHint('');
    $client->setHostedDomain('example.com');
    $client->setOpenIdRealm('example.com');
    $client->setPrompt('select_account');
    $client->setIncludeGrantedScopes(true);
    $authUrl = $client->createAuthUrl("http://googleapis.com/scope/foo");
    $expected = "https://accounts.google.com/o/oauth2/auth"
        . "?response_type=code"
        . "&access_type=offline"
        . "&client_id=clientId1"
        . "&redirect_uri=http%3A%2F%2Flocalhost"
        . "&state=xyz"
        . "&scope=http%3A%2F%2Fgoogleapis.com%2Fscope%2Ffoo"
        . "&hd=example.com"
        . "&include_granted_scopes=true"
        . "&openid.realm=example.com"
        . "&prompt=select_account";

    $this->assertEquals($expected, $authUrl);
  }

  public function testPrepareNoScopes()
  {
    $client = new Client();

    $scopes = $client->prepareScopes();
    $this->assertNull($scopes);
  }

  public function testNoAuthIsNull()
  {
    $client = new Client();

    $this->assertNull($client->getAccessToken());
  }

  public function testPrepareService()
  {
    $this->onlyGuzzle6Or7();

    $client = new Client();
    $client->setScopes(array("scope1", "scope2"));
    $scopes = $client->prepareScopes();
    $this->assertEquals("scope1 scope2", $scopes);

    $client->setScopes(array("", "scope2"));
    $scopes = $client->prepareScopes();
    $this->assertEquals(" scope2", $scopes);

    $client->setScopes("scope2");
    $client->addScope("scope3");
    $client->addScope(array("scope4", "scope5"));
    $scopes = $client->prepareScopes();
    $this->assertEquals("scope2 scope3 scope4 scope5", $scopes);

    $client->setClientId('test1');
    $client->setRedirectUri('http://localhost/');
    $client->setState('xyz');
    $client->setScopes(array("http://test.com", "scope2"));
    $scopes = $client->prepareScopes();
    $this->assertEquals("http://test.com scope2", $scopes);
    $this->assertEquals(
        ''
        . 'https://accounts.google.com/o/oauth2/auth'
        . '?response_type=code'
        . '&access_type=online'
        . '&client_id=test1'
        . '&redirect_uri=http%3A%2F%2Flocalhost%2F'
        . '&state=xyz'
        . '&scope=http%3A%2F%2Ftest.com%20scope2'
        . '&approval_prompt=auto',

        $client->createAuthUrl()
    );

    $stream = $this->prophesize('GuzzleHttp\Psr7\Stream');
    $stream->__toString()->willReturn('');

    $response = $this->prophesize('Psr\Http\Message\ResponseInterface');
    $response->getBody()
        ->shouldBeCalledTimes(1)
        ->willReturn($stream->reveal());

    $response->getStatusCode()->willReturn(200);

    $http = $this->prophesize('GuzzleHttp\ClientInterface');

    $http->send(Argument::type('Psr\Http\Message\RequestInterface'), [])
        ->shouldBeCalledTimes(1)
        ->willReturn($response->reveal());

    $client->setHttpClient($http->reveal());
    $dr_service = new Drive($client);
    $this->assertInstanceOf('Google\Model', $dr_service->files->listFiles());
  }

  public function testDefaultLogger()
  {
    $client = new Client();
    $logger = $client->getLogger();
    $this->assertInstanceOf('Monolog\Logger', $logger);
    $handler = $logger->popHandler();
    $this->assertInstanceOf('Monolog\Handler\StreamHandler', $handler);
  }

  public function testDefaultLoggerAppEngine()
  {
    $_SERVER['SERVER_SOFTWARE'] = 'Google App Engine';
    $client = new Client();
    $logger = $client->getLogger();
    $handler = $logger->popHandler();
    unset($_SERVER['SERVER_SOFTWARE']);

    $this->assertInstanceOf('Monolog\Logger', $logger);
    $this->assertInstanceOf('Monolog\Handler\SyslogHandler', $handler);
  }

  public function testSettersGetters()
  {
    $client = new Client();
    $client->setClientId("client1");
    $client->setClientSecret('client1secret');
    $client->setState('1');
    $client->setApprovalPrompt('force');
    $client->setAccessType('offline');

    $client->setRedirectUri('localhost');
    $client->setConfig('application_name', 'me');

    $cache = $this->prophesize(CacheItemPoolInterface::class);
    $client->setCache($cache->reveal());
    $this->assertInstanceOf(CacheItemPoolInterface::class, $client->getCache());

    try {
      $client->setAccessToken(null);
      $this->fail('Should have thrown an Exception.');
    } catch (InvalidArgumentException $e) {
      $this->assertEquals('invalid json token', $e->getMessage());
    }

    $token = array('access_token' => 'token');
    $client->setAccessToken($token);
    $this->assertEquals($token, $client->getAccessToken());
  }

  public function testDefaultConfigOptions()
  {
      $client = new Client();
      if ($this->isGuzzle6() || $this->isGuzzle7()) {
          $this->assertArrayHasKey('http_errors', $client->getHttpClient()->getConfig());
          $this->assertArrayNotHasKey('exceptions', $client->getHttpClient()->getConfig());
          $this->assertFalse($client->getHttpClient()->getConfig()['http_errors']);
      }
      if ($this->isGuzzle5()) {
          $this->assertArrayHasKey('exceptions', $client->getHttpClient()->getDefaultOption());
          $this->assertArrayNotHasKey('http_errors', $client->getHttpClient()->getDefaultOption());
          $this->assertFalse($client->getHttpClient()->getDefaultOption()['exceptions']);
      }
  }

  public function testAppEngineStreamHandlerConfig()
  {
    $this->onlyGuzzle5();

    $_SERVER['SERVER_SOFTWARE'] = 'Google App Engine';
    $client = new Client();

    // check Stream Handler is used
    $http = $client->getHttpClient();
    $class = new ReflectionClass(get_class($http));
    $property = $class->getProperty('fsm');
    $property->setAccessible(true);
    $fsm = $property->getValue($http);

    $class = new ReflectionClass(get_class($fsm));
    $property = $class->getProperty('handler');
    $property->setAccessible(true);
    $handler = $property->getValue($fsm);

    $this->assertInstanceOf('GuzzleHttp\Ring\Client\StreamHandler', $handler);

    unset($_SERVER['SERVER_SOFTWARE']);
  }

  public function testAppEngineVerifyConfig()
  {
    $this->onlyGuzzle5();

    $_SERVER['SERVER_SOFTWARE'] = 'Google App Engine';
    $client = new Client();

    $this->assertEquals(
      '/etc/ca-certificates.crt',
      $client->getHttpClient()->getDefaultOption('verify')
    );

    unset($_SERVER['SERVER_SOFTWARE']);
  }

  public function testJsonConfig()
  {
    // Device config
    $client = new Client();
    $device =
    '{"installed":{"auth_uri":"https://accounts.google.com/o/oauth2/auth","client_secret"'.
    ':"N0aHCBT1qX1VAcF5J1pJAn6S","token_uri":"https://oauth2.googleapis.com/token",'.
    '"client_email":"","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","oob"],"client_x509_cert_url"'.
    ':"","client_id":"123456789.apps.googleusercontent.com","auth_provider_x509_cert_url":'.
    '"https://www.googleapis.com/oauth2/v1/certs"}}';
    $dObj = json_decode($device, true);
    $client->setAuthConfig($dObj);
    $this->assertEquals($client->getClientId(), $dObj['installed']['client_id']);
    $this->assertEquals($client->getClientSecret(), $dObj['installed']['client_secret']);
    $this->assertEquals($client->getRedirectUri(), $dObj['installed']['redirect_uris'][0]);

    // Web config
    $client = new Client();
    $web = '{"web":{"auth_uri":"https://accounts.google.com/o/oauth2/auth","client_secret"' .
      ':"lpoubuib8bj-Fmke_YhhyHGgXc","token_uri":"https://oauth2.googleapis.com/token"' .
      ',"client_email":"123456789@developer.gserviceaccount.com","client_x509_cert_url":'.
      '"https://www.googleapis.com/robot/v1/metadata/x509/123456789@developer.gserviceaccount.com"'.
      ',"client_id":"123456789.apps.googleusercontent.com","auth_provider_x509_cert_url":'.
      '"https://www.googleapis.com/oauth2/v1/certs"}}';
    $wObj = json_decode($web, true);
    $client->setAuthConfig($wObj);
    $this->assertEquals($client->getClientId(), $wObj['web']['client_id']);
    $this->assertEquals($client->getClientSecret(), $wObj['web']['client_secret']);
    $this->assertEquals($client->getRedirectUri(), '');
  }

  public function testIniConfig()
  {
    $config = parse_ini_file(__DIR__ . '/../config/test.ini');
    $client = new Client($config);

    $this->assertEquals('My Test application', $client->getConfig('application_name'));
    $this->assertEquals(
        'gjfiwnGinpena3',
        $client->getClientSecret()
    );
  }

  public function testNoAuth()
  {
    /** @var $noAuth Google_Auth_Simple */
    $client = new Client();
    $client->setDeveloperKey(null);

    // unset application credentials
    $GOOGLE_APPLICATION_CREDENTIALS = getenv('GOOGLE_APPLICATION_CREDENTIALS');
    $HOME = getenv('HOME');
    putenv('GOOGLE_APPLICATION_CREDENTIALS=');
    putenv('HOME='.sys_get_temp_dir());
    $http = new GuzzleClient();
    $client->authorize($http);

    putenv("GOOGLE_APPLICATION_CREDENTIALS=$GOOGLE_APPLICATION_CREDENTIALS");
    putenv("HOME=$HOME");
    $this->checkAuthHandler($http, null);
  }

  public function testApplicationDefaultCredentials()
  {
    $this->checkServiceAccountCredentials();
    $credentialsFile = getenv('GOOGLE_APPLICATION_CREDENTIALS');

    $client = new Client();
    $client->setAuthConfig($credentialsFile);

    $http = new GuzzleClient();
    $client->authorize($http);

    $this->checkAuthHandler($http, 'AuthToken');
    $this->checkCredentials($http, 'Google\Auth\Credentials\ServiceAccountCredentials');
  }

  public function testApplicationDefaultCredentialsWithSubject()
  {
    $this->checkServiceAccountCredentials();
    $credentialsFile = getenv('GOOGLE_APPLICATION_CREDENTIALS');

    $sub = 'sub123';
    $client = new Client();
    $client->setAuthConfig($credentialsFile);
    $client->setSubject($sub);

    $http = new GuzzleClient();
    $client->authorize($http);

    $this->checkAuthHandler($http, 'AuthToken');
    $this->checkCredentials($http, 'Google\Auth\Credentials\ServiceAccountCredentials', $sub);
  }

  /**
   * Test that the ID token is properly refreshed.
   */
  public function testRefreshTokenSetsValues()
  {
    $token = json_encode([
        'access_token' => 'xyz',
        'id_token' => 'ID_TOKEN',
    ]);
    $postBody = $this->prophesize('GuzzleHttp\Psr7\Stream');
    $postBody->__toString()
        ->shouldBeCalledTimes(1)
        ->willReturn($token);

    if ($this->isGuzzle5()) {
      $response = $this->getGuzzle5ResponseMock();
      $response->getStatusCode()
          ->shouldBeCalledTimes(1)
          ->willReturn(200);
    } else {
      $response = $this->prophesize('Psr\Http\Message\ResponseInterface');
    }

    $response->getBody()
        ->shouldBeCalledTimes(1)
        ->willReturn($postBody->reveal());

    $response->hasHeader('Content-Type')->willReturn(false);

    $http = $this->prophesize('GuzzleHttp\ClientInterface');

    if ($this->isGuzzle5()) {
      $guzzle5Request = new \GuzzleHttp\Message\Request('POST', '/', ['body' => $token]);
      $http->createRequest(Argument::any(), Argument::any(), Argument::any())
          ->shouldBeCalledTimes(1)
          ->willReturn($guzzle5Request);

      $http->send(Argument::type('GuzzleHttp\Message\Request'))
          ->shouldBeCalledTimes(1)
          ->willReturn($response->reveal());
    } else {
      $http->send(Argument::type('Psr\Http\Message\RequestInterface'), [])
          ->shouldBeCalledTimes(1)
          ->willReturn($response->reveal());
    }

    $client = $this->getClient();
    $client->setHttpClient($http->reveal());
    $client->fetchAccessTokenWithRefreshToken("REFRESH_TOKEN");
    $token = $client->getAccessToken();
    $this->assertEquals("ID_TOKEN", $token['id_token']);
  }

  /**
   * Test that the Refresh Token is set when refreshed.
   */
  public function testRefreshTokenIsSetOnRefresh()
  {
    $refreshToken = 'REFRESH_TOKEN';
    $token = json_encode(array(
        'access_token' => 'xyz',
        'id_token' => 'ID_TOKEN',
    ));
    $postBody = $this->prophesize('Psr\Http\Message\StreamInterface');
    $postBody->__toString()
        ->shouldBeCalledTimes(1)
        ->willReturn($token);

    if ($this->isGuzzle5()) {
      $response = $this->getGuzzle5ResponseMock();
      $response->getStatusCode()
          ->shouldBeCalledTimes(1)
          ->willReturn(200);
    } else {
      $response = $this->prophesize('Psr\Http\Message\ResponseInterface');
    }

    $response->getBody()
        ->shouldBeCalledTimes(1)
        ->willReturn($postBody->reveal());

    $response->hasHeader('Content-Type')->willReturn(false);

    $http = $this->prophesize('GuzzleHttp\ClientInterface');

    if ($this->isGuzzle5()) {
      $guzzle5Request = new \GuzzleHttp\Message\Request('POST', '/', ['body' => $token]);
      $http->createRequest(Argument::any(), Argument::any(), Argument::any())
          ->willReturn($guzzle5Request);

      $http->send(Argument::type('GuzzleHttp\Message\Request'))
          ->shouldBeCalledTimes(1)
          ->willReturn($response->reveal());
    } else {
      $http->send(Argument::type('Psr\Http\Message\RequestInterface'), [])
          ->shouldBeCalledTimes(1)
          ->willReturn($response->reveal());
    }

    $client = $this->getClient();
    $client->setHttpClient($http->reveal());
    $client->fetchAccessTokenWithRefreshToken($refreshToken);
    $token = $client->getAccessToken();
    $this->assertEquals($refreshToken, $token['refresh_token']);
  }

  /**
   * Test that the Refresh Token is not set when a new refresh token is returned.
   */
  public function testRefreshTokenIsNotSetWhenNewRefreshTokenIsReturned()
  {
    $refreshToken = 'REFRESH_TOKEN';
    $token = json_encode(array(
        'access_token' => 'xyz',
        'id_token' => 'ID_TOKEN',
        'refresh_token' => 'NEW_REFRESH_TOKEN'
    ));

    $postBody = $this->prophesize('GuzzleHttp\Psr7\Stream');
    $postBody->__toString()
        ->wilLReturn($token);

    if ($this->isGuzzle5()) {
      $response = $this->getGuzzle5ResponseMock();
      $response->getStatusCode()
          ->willReturn(200);
    } else {
      $response = $this->prophesize('Psr\Http\Message\ResponseInterface');
    }

    $response->getBody()
        ->willReturn($postBody->reveal());

    $response->hasHeader('Content-Type')->willReturn(false);

    $http = $this->prophesize('GuzzleHttp\ClientInterface');

    if ($this->isGuzzle5()) {
      $guzzle5Request = new \GuzzleHttp\Message\Request('POST', '/', ['body' => $token]);
      $http->createRequest(Argument::any(), Argument::any(), Argument::any())
          ->willReturn($guzzle5Request);

      $http->send(Argument::type('GuzzleHttp\Message\Request'))
          ->willReturn($response->reveal());
    } else {
      $http->send(Argument::type('Psr\Http\Message\RequestInterface'), [])
          ->shouldBeCalledTimes(1)
          ->willReturn($response->reveal());
    }

    $client = $this->getClient();
    $client->setHttpClient($http->reveal());
    $client->fetchAccessTokenWithRefreshToken($refreshToken);
    $token = $client->getAccessToken();
    $this->assertEquals('NEW_REFRESH_TOKEN', $token['refresh_token']);
  }

  /**
   * Test fetching an access token with assertion credentials
   * using "useApplicationDefaultCredentials"
   */
  public function testFetchAccessTokenWithAssertionFromEnv()
  {
    $this->checkServiceAccountCredentials();

    $client = $this->getClient();
    $client->useApplicationDefaultCredentials();
    $token = $client->fetchAccessTokenWithAssertion();

    $this->assertNotNull($token);
    $this->assertArrayHasKey('access_token', $token);
  }

  /**
   * Test fetching an access token with assertion credentials
   * using "setAuthConfig"
   */
  public function testFetchAccessTokenWithAssertionFromFile()
  {
    $this->checkServiceAccountCredentials();

    $client = $this->getClient();
    $client->setAuthConfig(getenv('GOOGLE_APPLICATION_CREDENTIALS'));
    $token = $client->fetchAccessTokenWithAssertion();

    $this->assertNotNull($token);
    $this->assertArrayHasKey('access_token', $token);
  }

  /**
   * Test fetching an access token with assertion credentials
   * populates the "created" field
   */
  public function testFetchAccessTokenWithAssertionAddsCreated()
  {
    $this->checkServiceAccountCredentials();

    $client = $this->getClient();
    $client->useApplicationDefaultCredentials();
    $token = $client->fetchAccessTokenWithAssertion();

    $this->assertNotNull($token);
    $this->assertArrayHasKey('created', $token);
  }

  /**
   * Test fetching an access token with assertion credentials
   * using "setAuthConfig" and "setSubject" but with user credentials
   */
  public function testBadSubjectThrowsException()
  {
    $this->checkServiceAccountCredentials();

    $client = $this->getClient();
    $client->useApplicationDefaultCredentials();
    $client->setSubject('bad-subject');

    $authHandler = AuthHandlerFactory::build();

    // make this method public for testing purposes
    $method = new ReflectionMethod($authHandler, 'createAuthHttp');
    $method->setAccessible(true);
    $authHttp = $method->invoke($authHandler, $client->getHttpClient());

    try {
      $token = $client->fetchAccessTokenWithAssertion($authHttp);
      $this->fail('no exception thrown');
    } catch (ClientException $e) {
      $response = $e->getResponse();
      $this->assertContains('Invalid impersonation', (string) $response->getBody());
    }
  }

  public function testTokenCallback()
  {
    $this->onlyPhp55AndAbove();
    $this->checkToken();

    $client = $this->getClient();
    $accessToken = $client->getAccessToken();

    if (!isset($accessToken['refresh_token'])) {
      $this->markTestSkipped('Refresh Token required');
    }

    // make the auth library think the token is expired
    $accessToken['expires_in'] = 0;
    $cache = $client->getCache();
    $path = sys_get_temp_dir().'/google-api-php-client-tests-'.time();
    $client->setCache($this->getCache($path));
    $client->setAccessToken($accessToken);

    // create the callback function
    $phpunit = $this;
    $called = false;
    $callback = function ($key, $value) use ($client, $cache, $phpunit, &$called) {
      // assert the expected keys and values
      $phpunit->assertNotNull($key);
      $phpunit->assertNotNull($value);
      $called = true;

      // go back to the previous cache
      $client->setCache($cache);
    };

    // set the token callback to the client
    $client->setTokenCallback($callback);

    // make a silly request to obtain a new token (it's ok if it fails)
    $http = $client->authorize();
    try {
      $http->get('https://www.googleapis.com/books/v1/volumes?q=Voltaire');
    } catch (Exception $e) {}
    $newToken = $client->getAccessToken();

    // go back to the previous cache
    // (in case callback wasn't called)
    $client->setCache($cache);

    $this->assertTrue($called);
  }

  public function testDefaultTokenCallback()
  {
    $this->onlyPhp55AndAbove();
    $this->checkToken();

    $client = $this->getClient();
    $accessToken = $client->getAccessToken();

    if (!isset($accessToken['refresh_token'])) {
      $this->markTestSkipped('Refresh Token required');
    }

    // make the auth library think the token is expired
    $accessToken['expires_in'] = 0;
    $client->setAccessToken($accessToken);

    // make a silly request to obtain a new token (it's ok if it fails)
    $http = $client->authorize();
    try {
      $http->get('https://www.googleapis.com/books/v1/volumes?q=Voltaire');
    } catch (Exception $e) {}

    // Assert the in-memory token has been updated
    $newToken = $client->getAccessToken();
    $this->assertNotEquals(
      $accessToken['access_token'],
      $newToken['access_token']
    );

    $this->assertFalse($client->isAccessTokenExpired());
  }

  /** @runInSeparateProcess */
  public function testOnGceCacheAndCacheOptions()
  {
    if (!class_exists(GCECache::class)) {
      $this->markTestSkipped('Requires google/auth >= 1.12');
    }

    putenv('HOME=');
    putenv('GOOGLE_APPLICATION_CREDENTIALS=');
    $prefix = 'test_prefix_';
    $cacheConfig = ['gce_prefix' => $prefix];

    $mockCacheItem = $this->prophesize(CacheItemInterface::class);
    $mockCacheItem->isHit()
        ->willReturn(true);
    $mockCacheItem->get()
        ->shouldBeCalledTimes(1)
        ->willReturn(true);

    $mockCache = $this->prophesize(CacheItemPoolInterface::class);
    $mockCache->getItem($prefix . GCECache::GCE_CACHE_KEY)
        ->shouldBeCalledTimes(1)
        ->willReturn($mockCacheItem->reveal());

    $client = new Client(['cache_config' => $cacheConfig]);
    $client->setCache($mockCache->reveal());
    $client->useApplicationDefaultCredentials();
    $client->authorize();
  }

  /** @runInSeparateProcess */
  public function testFetchAccessTokenWithAssertionCache()
  {
    $this->checkServiceAccountCredentials();
    $cachedValue = ['access_token' => '2/abcdef1234567890'];
    $mockCacheItem = $this->prophesize(CacheItemInterface::class);
    $mockCacheItem->isHit()
        ->shouldBeCalledTimes(1)
        ->willReturn(true);
    $mockCacheItem->get()
        ->shouldBeCalledTimes(1)
        ->willReturn($cachedValue);

    $mockCache = $this->prophesize(CacheItemPoolInterface::class);
    $mockCache->getItem(Argument::any())
        ->shouldBeCalledTimes(1)
        ->willReturn($mockCacheItem->reveal());

    $client = new Client();
    $client->setCache($mockCache->reveal());
    $client->useApplicationDefaultCredentials();
    $token = $client->fetchAccessTokenWithAssertion();
    $this->assertArrayHasKey('access_token', $token);
    $this->assertEquals($cachedValue['access_token'], $token['access_token']);
  }

  public function testCacheClientOption()
  {
    $mockCache = $this->prophesize(CacheItemPoolInterface::class);
    $client = new Client([
      'cache' => $mockCache->reveal()
    ]);
    $this->assertEquals($mockCache->reveal(), $client->getCache());
  }

  public function testExecuteWithFormat()
  {
    $this->onlyGuzzle6Or7();

    $client = new Client([
      'api_format_v2' => true
    ]);

    $guzzle = $this->prophesize('GuzzleHttp\Client');
    $guzzle
      ->send(Argument::allOf(
        Argument::type('Psr\Http\Message\RequestInterface'),
        Argument::that(function (RequestInterface $request) {
            return $request->getHeaderLine('X-GOOG-API-FORMAT-VERSION') === '2';
        })
      ), [])
      ->shouldBeCalled()
      ->willReturn(new Response(200, [], null));

    $client->setHttpClient($guzzle->reveal());

    $request = new Request('POST', 'http://foo.bar/');
    $client->execute($request);
  }

  public function testExecuteSetsCorrectHeaders()
  {
    $this->onlyGuzzle6Or7();

    $client = new Client();

    $guzzle = $this->prophesize('GuzzleHttp\Client');
    $guzzle->send(Argument::that(function (RequestInterface $request) {
      $userAgent = sprintf(
        '%s%s',
        Client::USER_AGENT_SUFFIX,
        Client::LIBVER
      );
      $xGoogApiClient = sprintf(
        'gl-php/%s gdcl/%s',
        phpversion(),
        Client::LIBVER
      );

      if ($request->getHeaderLine('User-Agent') !== $userAgent) {
        return false;
      }

      if ($request->getHeaderLine('x-goog-api-client') !== $xGoogApiClient) {
        return false;
      }

      return true;
    }), [])->shouldBeCalledTimes(1)->willReturn(new Response(200, [], null));

    $client->setHttpClient($guzzle->reveal());

    $request = new Request('POST', 'http://foo.bar/');
    $client->execute($request);
  }

  /**
   * @runInSeparateProcess
   */
  public function testClientOptions()
  {
    // Test credential file
    $tmpCreds = [
      'type' => 'service_account',
      'client_id' => 'foo',
      'client_email' => '',
      'private_key' => ''
    ];
    $tmpCredFile = tempnam(sys_get_temp_dir(), 'creds') . '.json';
    file_put_contents($tmpCredFile, json_encode($tmpCreds));
    $client = new Client([
      'credentials' => $tmpCredFile
    ]);
    $this->assertEquals('foo', $client->getClientId());

    // Test credentials array
    $client = new Client([
      'credentials' => $tmpCredFile
    ]);
    $this->assertEquals('foo', $client->getClientId());

    // Test singular scope
    $client = new Client([
      'scopes' => 'a-scope'
    ]);
    $this->assertEquals(['a-scope'], $client->getScopes());

    // Test multiple scopes
    $client = new Client([
      'scopes' => ['one-scope', 'two-scope']
    ]);
    $this->assertEquals(['one-scope', 'two-scope'], $client->getScopes());

    // Test quota project
    $client = new Client([
      'quota_project' => 'some-quota-project'
    ]);
    $this->assertEquals('some-quota-project', $client->getConfig('quota_project'));
    // Test quota project in google/auth dependency
    putenv('GOOGLE_APPLICATION_CREDENTIALS='.$tmpCredFile);
    $method = new ReflectionMethod($client, 'createApplicationDefaultCredentials');
    $method->setAccessible(true);
    $credentials = $method->invoke($client);
    $this->assertEquals('some-quota-project', $credentials->getQuotaProject());
  }

  public function testCredentialsOptionWithCredentialsLoader()
  {
    $this->onlyGuzzle6Or7();

    $request = null;
    $credentials = $this->prophesize('Google\Auth\CredentialsLoader');
    $credentials->getCacheKey()
        ->willReturn('cache-key');

    // Ensure the access token provided by our credentials loader is used
    $credentials->fetchAuthToken(Argument::any())
      ->shouldBeCalledOnce()
      ->willReturn(['access_token' => 'abc']);

    $client = new Client(['credentials' => $credentials->reveal()]);

    $handler = $this->prophesize('GuzzleHttp\HandlerStack');
    $handler->remove('google_auth')
      ->shouldBeCalledOnce();
    $handler->push(Argument::any(), 'google_auth')
      ->shouldBeCalledOnce()
      ->will(function($args) use (&$request) {
        $middleware = $args[0];
        $callable = $middleware(function ($req, $res) use (&$request) {
          $request = $req; // test later
        });
        $callable(new Request('GET', '/fake-uri'), ['auth' => 'google_auth']);
      });

    $httpClient = $this->prophesize('GuzzleHttp\ClientInterface');
    $httpClient->getConfig()
      ->shouldBeCalledOnce()
      ->willReturn(['handler' => $handler->reveal()]);
    $httpClient->getConfig('base_uri')
      ->shouldBeCalledOnce();
    $httpClient->getConfig('verify')
      ->shouldBeCalledOnce();
    $httpClient->getConfig('proxy')
      ->shouldBeCalledOnce();
    $httpClient->send(Argument::any(), Argument::any())
      ->shouldNotBeCalled();

    $http = $client->authorize($httpClient->reveal());

    $this->assertNotNull($request);
    $authHeader = $request->getHeaderLine('authorization');
    $this->assertNotNull($authHeader);
    $this->assertEquals('Bearer abc', $authHeader);
  }
}
