<?php
namespace Aws\Test;

use Aws\Command;
use Aws\CommandInterface;
use Aws\HandlerList;
use Aws\Middleware;
use GuzzleHttp\Psr7\Request;
use Yoast\PHPUnitPolyfills\TestCases\TestCase;

/**
 * @covers Aws\HandlerList
 */
class HandlerListTest extends TestCase
{
    public function testEnsuresHandlerIsSet()
    {
        $this->expectException(\LogicException::class);
        $list = new HandlerList();
        $this->assertFalse($list->hasHandler());
        $list->resolve();
    }

    public function testHandlerCanBeSetInCtor()
    {
        $handler = function () {};
        $list = new HandlerList($handler);
        $this->assertTrue($list->hasHandler());
        $this->assertSame($handler, $list->resolve());
    }

    public function testHandlerCanBeSetInSetter()
    {
        $handler = function () {};
        $list = new HandlerList();
        $list->setHandler($handler);
        $this->assertTrue($list->hasHandler());
        $this->assertSame($handler, $list->resolve());
    }

    public function testCanPrependWithName()
    {
        $list = new HandlerList();
        $list->prependInit(function () {}, 'foo');
        $this->assertCount(1, $list);
    }

    public function testCanRemoveByInstance()
    {
        $handler = function () {};
        $list = new HandlerList($handler);
        $middleware = function () { return function () {}; };
        $list->appendInit($middleware);
        $this->assertCount(1, $list);
        $this->assertNotSame($handler, $list->resolve());
        $list->remove($middleware);
        $this->assertCount(0, $list);
        $this->assertSame($handler, $list->resolve());
    }

    /** @doesNotPerformAssertions */
    public function testIgnoreWhenNameNotFound()
    {
        $list = new HandlerList();
        $list->remove('foo');
    }

    public function testCanRemoveByName()
    {
        $handler = function () {};
        $list = new HandlerList($handler);
        $middleware = function () { return function () {}; };
        $list->appendInit($middleware, 'foo');
        $this->assertCount(1, $list);
        $this->assertNotSame($handler, $list->resolve());
        $list->remove('foo');
        $this->assertCount(0, $list);
        $this->assertSame($handler, $list->resolve());
    }

    private function createMiddleware(array &$history, $name)
    {
        return function (callable $next) use (&$history, $name) {
            return function (CommandInterface $cmd, $request = null) use ($next, &$history, $name) {
                $history[] = $name;
                return $next($cmd, $request);
            };
        };
    }

    public function testWrapsInStepOrderWithAppend()
    {
        $handler = function (CommandInterface $cmd, $request = null) {
            return 'baz';
        };
        $list = new HandlerList($handler);
        $h = [];
        $steps = ['Init', 'Validate', 'Build', 'Sign', 'Attempt'];
        foreach ($steps as $step) {
            $m = $this->createMiddleware($h, $step);
            $list->{'append' . $step}($m);
        }
        $built = $list->resolve();
        $cmd = new Command('foo');
        $this->assertSame('baz', $built($cmd));
        $this->assertEquals($steps, $h);
    }

    public function testWrapsInStepOrderWithPrepend()
    {
        $handler = function (CommandInterface $cmd, $request = null) {
            return 'baz';
        };
        $list = new HandlerList($handler);
        $h = [];
        $steps = ['Init', 'Validate', 'Build', 'Sign', 'Attempt'];
        foreach ($steps as $step) {
            $m = $this->createMiddleware($h, $step);
            $list->{'prepend' . $step}($m);
        }
        $built = $list->resolve();
        $cmd = new Command('foo');
        $this->assertSame('baz', $built($cmd));
        $this->assertEquals($steps, $h);
    }

    public function testCanPrintStack()
    {
        $list = new HandlerList();
        $list->appendInit(function () {}, 'foo');
        $list->appendInit([$this, 'bar'], 'bar');
        $list->appendValidate(__CLASS__ . '::foo');
        $list->appendSign([Middleware::class, 'tap'], 'baz');
        $list->setHandler(function () {});
        $lines = explode("\n", (string) $list);
        $this->assertCount(6, $lines);
        $this->assertStringContainsString('0) Step: init, Name: foo, Function: callable(', $lines[0]);
        $this->assertSame("1) Step: init, Name: bar, Function: callable(['" . HandlerListTest::class . "', 'bar'])", $lines[1]);
        $this->assertSame('2) Step: validate, Function: callable(' . HandlerListTest::class . '::foo)', $lines[2]);
        $this->assertSame("3) Step: sign, Name: baz, Function: callable(['" . Middleware::class . "', 'tap'])", $lines[3]);
        $this->assertStringContainsString('4) Handler: callable(', $lines[4]);
    }

    public static function foo() {}
    public function bar() {}

    public function testCanAddBefore()
    {
        $list = new HandlerList();
        $list->appendInit(function () {});
        $list->appendBuild(function () {}, 'test');
        $list->before('test', 'a', function () {});
        $lines = explode("\n", (string) $list);
        $this->assertStringContainsString("1) Step: build, Name: a", $lines[1]);
        $this->assertStringContainsString("2) Step: build, Name: test", $lines[2]);
    }

    public function testCanAddAfter()
    {
        $list = new HandlerList();
        $list->appendBuild(function () {}, 'test');
        $list->appendBuild(function () {}, 'after_test');
        $list->appendInit(function () {});
        $list->after('test', 'a', function () {});
        $lines = explode("\n", (string) $list);
        $this->assertStringContainsString("1) Step: build, Name: test", $lines[1]);
        $this->assertStringContainsString("2) Step: build, Name: a", $lines[2]);
        $this->assertStringContainsString("3) Step: build, Name: after_test", $lines[3]);
    }

    public function testMustExistByNameToPrependOrAppend()
    {
        $this->expectException(\InvalidArgumentException::class);
        $list = new HandlerList();
        $list->before('foo', '', function () {});
    }

    public function testCanInterposeMiddleware()
    {
        $list = new HandlerList(function () {});
        $list->appendInit(Middleware::tap(function () {}), 'a');
        $list->appendValidate(Middleware::tap(function () {}), 'b');
        $list->appendBuild(Middleware::tap(function () {}), 'c');
        $list->appendSign(Middleware::tap(function () {}), 'd');
        $list->appendAttempt(Middleware::tap(function (){}), 'e');

        $list->interpose(function ($step, $name) use (&$res) {
            return function (callable $h) use ($step, $name, &$res) {
                return function ($c, $r) use ($h, $step, $name, &$res) {
                    $res[] = "$step:$name";
                    return $h($c, $r);
                };
            };
        });

        $handler = $list->resolve();
        $handler(new Command('foo'), new Request('GET', 'http://foo.com'));
        $this->assertEquals(['init:a', 'validate:b', 'build:c', 'sign:d', 'attempt:e'], $res);
    }
}
