<?php

require_once 'Swift/Tests/SwiftUnitTestCase.php';
require_once 'Swift/Transport/LoadBalancedTransport.php';
require_once 'Swift/TransportException.php';
require_once 'Swift/Transport.php';
require_once 'Swift/Events/EventListener.php';

class Swift_Transport_LoadBalancedTransportTest
    extends Swift_Tests_SwiftUnitTestCase
{
    public function testEachTransportIsUsedInTurn()
    {
        $context = new Mockery();
        $message1 = $context->mock('Swift_Mime_Message');
        $message2 = $context->mock('Swift_Mime_Message');
        $t1 = $context->mock('Swift_Transport');
        $t2 = $context->mock('Swift_Transport');
        $con1 = $context->states('Connection 1')->startsAs('off');
        $con2 = $context->states('Connection 2')->startsAs('off');
        $context->checking(Expectations::create()
            -> ignoring($message1)
            -> ignoring($message2)
            -> allowing($t1)->isStarted() -> returns(false) -> when($con1->is('off'))
            -> allowing($t1)->isStarted() -> returns(true) -> when($con1->is('on'))
            -> allowing($t1)->start() -> when($con1->is('off')) -> then($con1->is('on'))
            -> one($t1)->send($message1, optional()) -> returns(1) -> when($con1->is('on'))
            -> never($t1)->send($message2, optional())
            -> ignoring($t1)
            -> allowing($t2)->isStarted() -> returns(false) -> when($con2->is('off'))
            -> allowing($t2)->isStarted() -> returns(true) -> when($con2->is('on'))
            -> allowing($t2)->start() -> when($con2->is('off')) -> then($con2->is('on'))
            -> one($t2)->send($message2, optional()) -> returns(1) -> when($con2->is('on'))
            -> never($t2)->send($message1, optional())
            -> ignoring($t2)
            );

        $transport = $this->_getTransport(array($t1, $t2));
        $transport->start();
        $this->assertEqual(1, $transport->send($message1));
        $this->assertEqual(1, $transport->send($message2));

        $context->assertIsSatisfied();
    }

    public function testTransportsAreReusedInRotatingFashion()
    {
        $context = new Mockery();
        $message1 = $context->mock('Swift_Mime_Message');
        $message2 = $context->mock('Swift_Mime_Message');
        $message3 = $context->mock('Swift_Mime_Message');
        $message4 = $context->mock('Swift_Mime_Message');
        $t1 = $context->mock('Swift_Transport');
        $t2 = $context->mock('Swift_Transport');
        $con1 = $context->states('Connection 1')->startsAs('off');
        $con2 = $context->states('Connection 2')->startsAs('off');
        $context->checking(Expectations::create()
            -> ignoring($message1)
            -> ignoring($message2)
            -> allowing($t1)->isStarted() -> returns(false) -> when($con1->is('off'))
            -> allowing($t1)->isStarted() -> returns(true) -> when($con1->is('on'))
            -> allowing($t1)->start() -> when($con1->is('off')) -> then($con1->is('on'))
            -> one($t1)->send($message1, optional()) -> returns(1) -> when($con1->is('on'))
            -> never($t1)->send($message2, optional())
            -> one($t1)->send($message3, optional()) -> returns(1) -> when($con1->is('on'))
            -> never($t1)->send($message4, optional())
            -> ignoring($t1)
            -> allowing($t2)->isStarted() -> returns(false) -> when($con2->is('off'))
            -> allowing($t2)->isStarted() -> returns(true) -> when($con2->is('on'))
            -> allowing($t2)->start() -> when($con2->is('off')) -> then($con2->is('on'))
            -> one($t2)->send($message2, optional()) -> returns(1) -> when($con2->is('on'))
            -> never($t2)->send($message1, optional())
            -> one($t2)->send($message4, optional()) -> returns(1) -> when($con2->is('on'))
            -> never($t2)->send($message3, optional())
            -> ignoring($t2)
            );

        $transport = $this->_getTransport(array($t1, $t2));
        $transport->start();

        $this->assertEqual(1, $transport->send($message1));
        $this->assertEqual(1, $transport->send($message2));
        $this->assertEqual(1, $transport->send($message3));
        $this->assertEqual(1, $transport->send($message4));

        $context->assertIsSatisfied();
    }

    public function testMessageCanBeTriedOnNextTransportIfExceptionThrown()
    {
        $e = new Swift_TransportException('b0rken');

        $context = new Mockery();
        $message = $context->mock('Swift_Mime_Message');
        $t1 = $context->mock('Swift_Transport');
        $t2 = $context->mock('Swift_Transport');
        $con1 = $context->states('Connection')->startsAs('off');
        $con2 = $context->states('Connection')->startsAs('off');
        $context->checking(Expectations::create()
            -> ignoring($message)
            -> allowing($t1)->isStarted() -> returns(false) -> when($con1->is('off'))
            -> allowing($t1)->isStarted() -> returns(true) -> when($con1->is('on'))
            -> one($t1)->start() -> when($con1->isNot('on')) -> then($con1->is('on'))
            -> one($t1)->send($message, optional()) -> throws($e) -> when($con1->is('on'))
            -> ignoring($t1)
            -> allowing($t2)->isStarted() -> returns(false) -> when($con2->is('off'))
            -> allowing($t2)->isStarted() -> returns(true) -> when($con2->is('on'))
            -> one($t2)->start() -> when($con2->isNot('on')) -> then($con2->is('on'))
            -> one($t2)->send($message, optional()) -> returns(1) -> when($con2->is('on'))
            -> ignoring($t2)
            );

        $transport = $this->_getTransport(array($t1, $t2));
        $transport->start();
        $this->assertEqual(1, $transport->send($message));
        $context->assertIsSatisfied();
    }

    public function testMessageIsTriedOnNextTransportIfZeroReturned()
    {
        $context = new Mockery();
        $message = $context->mock('Swift_Mime_Message');
        $t1 = $context->mock('Swift_Transport');
        $t2 = $context->mock('Swift_Transport');
        $con1 = $context->states('Connection')->startsAs('off');
        $con2 = $context->states('Connection')->startsAs('off');
        $context->checking(Expectations::create()
            -> ignoring($message)
            -> allowing($t1)->isStarted() -> returns(false) -> when($con1->is('off'))
            -> allowing($t1)->isStarted() -> returns(true) -> when($con1->is('on'))
            -> one($t1)->start() -> when($con1->isNot('on')) -> then($con1->is('on'))
            -> one($t1)->send($message, optional()) -> returns(0) -> when($con1->is('on'))
            -> ignoring($t1)
            -> allowing($t2)->isStarted() -> returns(false) -> when($con2->is('off'))
            -> allowing($t2)->isStarted() -> returns(true) -> when($con2->is('on'))
            -> one($t2)->start() -> when($con2->isNot('on')) -> then($con2->is('on'))
            -> one($t2)->send($message, optional()) -> returns(1) -> when($con2->is('on'))
            -> ignoring($t2)
            );

        $transport = $this->_getTransport(array($t1, $t2));
        $transport->start();
        $this->assertEqual(1, $transport->send($message));
        $context->assertIsSatisfied();
    }

    public function testZeroIsReturnedIfAllTransportsReturnZero()
    {
        $context = new Mockery();
        $message = $context->mock('Swift_Mime_Message');
        $t1 = $context->mock('Swift_Transport');
        $t2 = $context->mock('Swift_Transport');
        $con1 = $context->states('Connection')->startsAs('off');
        $con2 = $context->states('Connection')->startsAs('off');
        $context->checking(Expectations::create()
            -> ignoring($message)
            -> allowing($t1)->isStarted() -> returns(false) -> when($con1->is('off'))
            -> allowing($t1)->isStarted() -> returns(true) -> when($con1->is('on'))
            -> one($t1)->start() -> when($con1->isNot('on')) -> then($con1->is('on'))
            -> one($t1)->send($message, optional()) -> returns(0) -> when($con1->is('on'))
            -> ignoring($t1)
            -> allowing($t2)->isStarted() -> returns(false) -> when($con2->is('off'))
            -> allowing($t2)->isStarted() -> returns(true) -> when($con2->is('on'))
            -> one($t2)->start() -> when($con2->isNot('on')) -> then($con2->is('on'))
            -> one($t2)->send($message, optional()) -> returns(0) -> when($con2->is('on'))
            -> ignoring($t2)
            );

        $transport = $this->_getTransport(array($t1, $t2));
        $transport->start();
        $this->assertEqual(0, $transport->send($message));
        $context->assertIsSatisfied();
    }

    public function testTransportsWhichThrowExceptionsAreNotRetried()
    {
        $e = new Swift_TransportException('maur b0rken');

        $context = new Mockery();
        $message1 = $context->mock('Swift_Mime_Message');
        $message2 = $context->mock('Swift_Mime_Message');
        $message3 = $context->mock('Swift_Mime_Message');
        $message4 = $context->mock('Swift_Mime_Message');
        $t1 = $context->mock('Swift_Transport');
        $t2 = $context->mock('Swift_Transport');
        $con1 = $context->states('Connection')->startsAs('off');
        $con2 = $context->states('Connection')->startsAs('off');
        $context->checking(Expectations::create()
            -> ignoring($message1)
            -> ignoring($message2)
            -> ignoring($message3)
            -> ignoring($message4)
            -> allowing($t1)->isStarted() -> returns(false) -> when($con1->is('off'))
            -> allowing($t1)->isStarted() -> returns(true) -> when($con1->is('on'))
            -> one($t1)->start() -> when($con1->isNot('on')) -> then($con1->is('on'))
            -> one($t1)->send($message1, optional()) -> throws($e) -> when($con1->is('on'))
            -> never($t1)->send($message2, optional())
            -> never($t1)->send($message3, optional())
            -> never($t1)->send($message4, optional())
            -> ignoring($t1)
            -> allowing($t2)->isStarted() -> returns(false) -> when($con2->is('off'))
            -> allowing($t2)->isStarted() -> returns(true) -> when($con2->is('on'))
            -> one($t2)->start() -> when($con2->isNot('on')) -> then($con2->is('on'))
            -> one($t2)->send($message1, optional()) -> returns(1) -> when($con2->is('on'))
            -> one($t2)->send($message2, optional()) -> returns(1) -> when($con2->is('on'))
            -> one($t2)->send($message3, optional()) -> returns(1) -> when($con2->is('on'))
            -> one($t2)->send($message4, optional()) -> returns(1) -> when($con2->is('on'))
            -> ignoring($t2)
            );

        $transport = $this->_getTransport(array($t1, $t2));
        $transport->start();
        $this->assertEqual(1, $transport->send($message1));
        $this->assertEqual(1, $transport->send($message2));
        $this->assertEqual(1, $transport->send($message3));
        $this->assertEqual(1, $transport->send($message4));
    }

    public function testExceptionIsThrownIfAllTransportsDie()
    {
        $e = new Swift_TransportException('b0rken');

        $context = new Mockery();
        $message = $context->mock('Swift_Mime_Message');
        $t1 = $context->mock('Swift_Transport');
        $t2 = $context->mock('Swift_Transport');
        $con1 = $context->states('Connection')->startsAs('off');
        $con2 = $context->states('Connection')->startsAs('off');
        $context->checking(Expectations::create()
            -> ignoring($message)
            -> allowing($t1)->isStarted() -> returns(false) -> when($con1->is('off'))
            -> allowing($t1)->isStarted() -> returns(true) -> when($con1->is('on'))
            -> one($t1)->start() -> when($con1->isNot('on')) -> then($con1->is('on'))
            -> one($t1)->send($message, optional()) -> throws($e) -> when($con1->is('on'))
            -> ignoring($t1)
            -> allowing($t2)->isStarted() -> returns(false) -> when($con2->is('off'))
            -> allowing($t2)->isStarted() -> returns(true) -> when($con2->is('on'))
            -> one($t2)->start() -> when($con2->isNot('on')) -> then($con2->is('on'))
            -> one($t2)->send($message, optional()) -> throws($e) -> when($con2->is('on'))
            -> ignoring($t2)
            );

        $transport = $this->_getTransport(array($t1, $t2));
        $transport->start();
        try {
            $transport->send($message);
            $this->fail('All transports failed so Exception should be thrown');
        } catch (Exception $e) {
        }
        $context->assertIsSatisfied();
    }

    public function testStoppingTransportStopsAllDelegates()
    {
        $context = new Mockery();
        $t1 = $context->mock('Swift_Transport');
        $t2 = $context->mock('Swift_Transport');
        $con1 = $context->states('Connection')->startsAs('on');
        $con2 = $context->states('Connection')->startsAs('on');
        $context->checking(Expectations::create()
            -> allowing($t1)->isStarted() -> returns(true) -> when($con1->is('on'))
            -> one($t1)->stop() -> when($con1->is('on')) -> then($con1->is('off'))
            -> ignoring($t1)
            -> allowing($t2)->isStarted() -> returns(true) -> when($con2->is('on'))
            -> one($t2)->stop() -> when($con2->is('on')) -> then($con2->is('off'))
            -> ignoring($t2)
            );

        $transport = $this->_getTransport(array($t1, $t2));
        $transport->start();
        $transport->stop();
        $context->assertIsSatisfied();
    }

    public function testTransportShowsAsNotStartedIfAllDelegatesDead()
    {
        $e = new Swift_TransportException('b0rken');

        $context = new Mockery();
        $message = $context->mock('Swift_Mime_Message');
        $t1 = $context->mock('Swift_Transport');
        $t2 = $context->mock('Swift_Transport');
        $con1 = $context->states('Connection')->startsAs('off');
        $con2 = $context->states('Connection')->startsAs('off');
        $context->checking(Expectations::create()
            -> ignoring($message)
            -> allowing($t1)->isStarted() -> returns(false) -> when($con1->is('off'))
            -> allowing($t1)->isStarted() -> returns(true) -> when($con1->is('on'))
            -> one($t1)->start() -> when($con1->isNot('on')) -> then($con1->is('on'))
            -> one($t1)->send($message, optional()) -> throws($e) -> when($con1->is('on'))
            -> ignoring($t1)
            -> allowing($t2)->isStarted() -> returns(false) -> when($con2->is('off'))
            -> allowing($t2)->isStarted() -> returns(true) -> when($con2->is('on'))
            -> one($t2)->start() -> when($con2->isNot('on')) -> then($con2->is('on'))
            -> one($t2)->send($message, optional()) -> throws($e) -> when($con2->is('on'))
            -> ignoring($t2)
            );

        $transport = $this->_getTransport(array($t1, $t2));
        $transport->start();
        $this->assertTrue($transport->isStarted());
        try {
            $transport->send($message);
            $this->fail('All transports failed so Exception should be thrown');
        } catch (Exception $e) {
            $this->assertFalse($transport->isStarted());
        }
        $context->assertIsSatisfied();
    }

    public function testRestartingTransportRestartsDeadDelegates()
    {
        $e = new Swift_TransportException('b0rken');

        $context = new Mockery();
        $message1 = $context->mock('Swift_Mime_Message');
        $message2 = $context->mock('Swift_Mime_Message');
        $t1 = $context->mock('Swift_Transport');
        $t2 = $context->mock('Swift_Transport');
        $con1 = $context->states('Connection')->startsAs('off');
        $con2 = $context->states('Connection')->startsAs('off');
        $context->checking(Expectations::create()
            -> ignoring($message1)
            -> ignoring($message2)
            -> allowing($t1)->isStarted() -> returns(false) -> when($con1->is('off'))
            -> allowing($t1)->isStarted() -> returns(true) -> when($con1->is('on'))
            -> exactly(2)->of($t1)->start() -> when($con1->isNot('on')) -> then($con1->is('on'))
            -> one($t1)->send($message1, optional()) -> throws($e) -> when($con1->is('on')) -> then($con1->is('off'))
            -> one($t1)->send($message2, optional()) -> returns(10) -> when($con1->is('on'))
            -> ignoring($t1)
            -> allowing($t2)->isStarted() -> returns(false) -> when($con2->is('off'))
            -> allowing($t2)->isStarted() -> returns(true) -> when($con2->is('on'))
            -> one($t2)->start() -> when($con2->isNot('on')) -> then($con2->is('on'))
            -> one($t2)->send($message1, optional()) -> throws($e) -> when($con2->is('on'))
            -> never($t2)->send($message2, optional())
            -> ignoring($t2)
            );

        $transport = $this->_getTransport(array($t1, $t2));
        $transport->start();
        $this->assertTrue($transport->isStarted());
        try {
            $transport->send($message1);
            $this->fail('All transports failed so Exception should be thrown');
        } catch (Exception $e) {
            $this->assertFalse($transport->isStarted());
        }
        //Restart and re-try
        $transport->start();
        $this->assertTrue($transport->isStarted());
        $this->assertEqual(10, $transport->send($message2));
        $context->assertIsSatisfied();
    }

    public function testFailureReferenceIsPassedToDelegates()
    {
        $failures = array();

        $context = new Mockery();
        $message = $context->mock('Swift_Mime_Message');
        $t1 = $context->mock('Swift_Transport');
        $con = $context->states('Connection')->startsAs('off');
        $context->checking(Expectations::create()
            -> ignoring($message)
            -> allowing($t1)->isStarted() -> returns(false) -> when($con->is('off'))
            -> allowing($t1)->isStarted() -> returns(true) -> when($con->is('on'))
            -> one($t1)->start() -> when($con->isNot('on')) -> then($con->is('on'))
            -> one($t1)->send($message, reference($failures)) -> returns(1) -> when($con->is('on'))
            -> ignoring($t1)
            );

        $transport = $this->_getTransport(array($t1));
        $transport->start();
        $transport->send($message, $failures);
        $context->assertIsSatisfied();
    }

    public function testRegisterPluginDelegatesToLoadedTransports()
    {
        $context = new Mockery();

        $plugin = $this->_createPlugin($context);

        $t1 = $context->mock('Swift_Transport');
        $t2 = $context->mock('Swift_Transport');
        $context->checking(Expectations::create()
            -> one($t1)->registerPlugin($plugin)
            -> one($t2)->registerPlugin($plugin)
            -> ignoring($t1)
            -> ignoring($t2)
            );

        $transport = $this->_getTransport(array($t1, $t2));
        $transport->registerPlugin($plugin);

        $context->assertIsSatisfied();
    }

    // -- Private helpers

    private function _getTransport(array $transports)
    {
        $transport = new Swift_Transport_LoadBalancedTransport();
        $transport->setTransports($transports);

        return $transport;
    }

    private function _createPlugin($context)
    {
        return $context->mock('Swift_Events_EventListener');
    }

    private function _createInnerTransport()
    {
        return $this->_mockery()->mock('Swift_Transport');
    }

}
