<?php

namespace Aws;

use Aws\Exception\AwsException;
use Countable;
use Exception;
use GuzzleHttp\Promise;
use GuzzleHttp\Promise\RejectedPromise;
use InvalidArgumentException;
use Psr\Http\Message\RequestInterface;
use RuntimeException;
use Throwable;

/**
 * Returns promises that are rejected or fulfilled using a queue of
 * Aws\ResultInterface and Aws\Exception\AwsException objects.
 */
class MockHandler implements Countable {
	private $queue;
	private $lastCommand;
	private $lastRequest;
	private $onFulfilled;
	private $onRejected;

	/**
	 * The passed in value must be an array of {@see Aws\ResultInterface} or
	 * {@see AwsException} objects that acts as a queue of results or
	 * exceptions to return each time the handler is invoked.
	 *
	 * @param array $resultOrQueue
	 * @param callable $onFulfilled Callback to invoke when the return value is fulfilled.
	 * @param callable $onRejected Callback to invoke when the return value is rejected.
	 */
	public function __construct(
		array $resultOrQueue = [],
		callable $onFulfilled = null,
		callable $onRejected = null
	) {
		$this->onFulfilled = $onFulfilled;
		$this->onRejected  = $onRejected;

		if ( $resultOrQueue ) {
			call_user_func_array( [ $this, 'append' ], $resultOrQueue );
		}
	}

	/**
	 * Adds one or more variadic ResultInterface or AwsException objects to the
	 * queue.
	 */
	public function append() {
		foreach ( func_get_args() as $value ) {
			if ( $value instanceof ResultInterface
			     || $value instanceof AwsException
			     || is_callable( $value )
			) {
				$this->queue[] = $value;
			} else {
				throw new InvalidArgumentException( 'Expected an Aws\ResultInterface or Aws\Exception\AwsException.' );
			}
		}
	}

	/**
	 * Adds one or more \Exception or \Throwable to the queue
	 */
	public function appendException() {
		foreach ( func_get_args() as $value ) {
			if ( $value instanceof Exception || $value instanceof Throwable ) {
				$this->queue[] = $value;
			} else {
				throw new InvalidArgumentException( 'Expected an \Exception or \Throwable.' );
			}
		}
	}

	public function __invoke(
		CommandInterface $command,
		RequestInterface $request
	) {
		if ( ! $this->queue ) {
			$last = $this->lastCommand
				? ' The last command sent was ' . $this->lastCommand->getName() . '.'
				: '';
			throw new RuntimeException( 'Mock queue is empty. Trying to send a '
			                             . $command->getName() . ' command failed.' . $last );
		}

		$this->lastCommand = $command;
		$this->lastRequest = $request;

		$result = array_shift( $this->queue );

		if ( is_callable( $result ) ) {
			$result = $result( $command, $request );
		}

		if ( $result instanceof Exception ) {
			$result = new RejectedPromise( $result );
		} else {
			// Add an effective URI and statusCode if not present.
			$meta = $result['@metadata'];
			if ( ! isset( $meta['effectiveUri'] ) ) {
				$meta['effectiveUri'] = (string) $request->getUri();
			}
			if ( ! isset( $meta['statusCode'] ) ) {
				$meta['statusCode'] = 200;
			}
			$result['@metadata'] = $meta;
			$result              = Promise\promise_for( $result );
		}

		$result->then( $this->onFulfilled, $this->onRejected );

		return $result;
	}

	/**
	 * Get the last received request.
	 *
	 * @return RequestInterface
	 */
	public function getLastRequest() {
		return $this->lastRequest;
	}

	/**
	 * Get the last received command.
	 *
	 * @return CommandInterface
	 */
	public function getLastCommand() {
		return $this->lastCommand;
	}

	/**
	 * Returns the number of remaining items in the queue.
	 *
	 * @return int
	 */
	public function count() {
		return count( $this->queue );
	}
}
