<?php
/**
 * core.framework
 *
 * @category   Core
 * @package    Core_Response
 * @subpackage UnitTests
 * @copyright  Copyright (c) 2011. Burza d.o.o. (http://web.burza.hr/en/)
 * @license    proprietary
 */

/**
 * @category   Core
 * @package    Core_Response
 * @subpackage UnitTests
 * @copyright  Copyright (c) 2011. Burza d.o.o. (http://web.burza.hr/en/)
 * @license    proprietary
 * @group      Core_Response
 */
class Core_ResponseTest extends CoreTest_Container_TestCase
{
    /**
     * @var Core_Response
     */
    protected $_object;
    protected $_error;

    protected function setUp()
    {
        $this->_object = new Core_Response;
        $this->_object
            ->setCaching(true)
            ->setSuppressCode(false)
            ->setInlineHeaders(false);
    }

    protected function tearDown()
    {
        Core_Application::resetInstance();
    }

    /**
     * @covers Core_Response::setRequest
     * @covers Core_Response::getRequest
     */
    public function testCanSetAndGetRequest()
    {
        $request = $this->getMock('Core_Request');
        $this->_object->setRequest($request);

        $this->assertSame($request, $this->_object->getRequest());
    }

    /**
     * @covers Core_Response::getRequest
     */
    public function testIfNoRequestIsSetGetRequestWillPullItFromApplicationInstanceAsRequest()
    {
        $request   = $this->getMock('Core_Request');
        $container = $this->_mockContainer('Request', $request);
        Core_Application::getInstance()->setContainer($container);

        $this->assertSame($request, $this->_object->getRequest());
    }

    /**
     * @covers Core_Response::setBody
     * @covers Core_Response::getBody
     */
    public function testCanSetAndGetBody()
    {
        $this->_object->setBody('bla-bla');
        $this->assertEquals('bla-bla', $this->_object->getBody());
    }

    /**
     * @covers Core_Response::getBody
     */
    public function testWillAutoEncodeBodyToJson()
    {
        $this->_object
            ->setFormat(Core_Response::FORMAT_JSON)
            ->setBody(array('a' => 'b'));

        $this->assertEquals("{\n    \"a\": \"b\"\n}", $this->_object->getBody());
    }

    /**
     * @covers Core_Response::getBody
     * @covers Core_Response::<protected>
     */
    public function testWillAutoEncodeBodyToXml()
    {
        $this->_object
            ->setFormat(Core_Response::FORMAT_XML)
            ->setBody(array('a' => 'b', 'c' => array('d' => 1)));

        $this->assertEquals(
            '<?xml version="1.0" encoding="UTF-8"?>'."\n".'<root><a>b</a><c><d>1</d></c></root>'. "\n",
            $this->_object->getBody()
        );
    }

    /**
     * @covers Core_Response::getBody
     */
    public function testWillAutoEncodeBodyToUrlencoded()
    {
        $this->_object
            ->setFormat(Core_Response::FORMAT_URLENCODED)
            ->setBody(array('a' => 'b', 'c' => array('d' => 1)));

        $this->assertEquals('a=b&c%5Bd%5D=1', $this->_object->getBody());
    }

    /**
     * @covers Core_Response::getBody
     */
    public function testWillAutoEncodeBodyOnlyIfSet()
    {
        // proper format is set, but not the body
        $this->_object->setFormat(Core_Response::FORMAT_XML);

        $this->assertNull($this->_object->getBody());
    }

    /**
     * @covers Core_Response::__construct
     */
    public function testCanSetBodyViaConstructor()
    {
        $object = new Core_Response('bla-bla');
        $this->assertEquals('bla-bla', $object->getBody());
    }

    /**
     * @covers Core_Response::setCode
     * @covers Core_Response::getCode
     */
    public function testCanSetAndGetCode()
    {
        $this->_object->setCode(204);
        $this->assertEquals(204, $this->_object->getCode());
    }

    /**
     * @covers Core_Response::__construct
     */
    public function testCanSetCodeViaConstructor()
    {
        $object = new Core_Response('bla-bla', 301);
        $this->assertEquals(301, $object->getCode());
    }

    /**
     * @covers Core_Response::getCode
     */
    public function testDefaultCodeIs200()
    {
        $this->assertEquals(200, $this->_object->getCode());
    }

    /**
     * @covers Core_Response::setHeader
     * @covers Core_Response::getHeader
     */
    public function testCanSetAndGetHeader()
    {
        $this->_object->setHeader('HeADer1', 14);
        $this->assertEquals(14, $this->_object->getHeader('HEADER1'));
    }

    /**
     * @covers Core_Response::getHeader
     * @covers Core_Response::<protected>
     */
    public function testWillNormalizeHeaderName()
    {
        $this->_object->setHeader('HeADer1', 14);
        $this->assertArrayHasKey('Header1', $this->_object->getHeaders());
    }

    /**
     * @covers Core_Response::getHeaders
     */
    public function testHasMandatoryHeaders()
    {
        $headers = $this->_object->setBody('a')->getHeaders();
        $this->assertArrayHasKey('Content-type', $headers);
        $this->assertArrayHasKey('Content-md5',  $headers);
    }

    /**
     * @covers Core_Response::getHeaders
     */
    public function testWillNotContainContentTypeHeadersIfNoBodySet()
    {
        $headers = $this->_object->setBody(null)->getHeaders();

        $this->assertArrayNotHasKey('Content-type', $headers);
        $this->assertArrayNotHasKey('Content-md5',  $headers);
    }


    /**
     * @covers Core_Response::getHeader
     */
    public function testFetchingAnUnknownHeaderWillReturnNull()
    {
        $this->assertNull($this->_object->getHeader('No-such-header'));
    }

    /**
     * @covers Core_Response::setHeaders
     * @covers Core_Response::getHeaders
     * @depends testCanSetAndGetHeader
     * @depends testWillNormalizeHeaderName
     * @depends testHasMandatoryHeaders
     */
    public function testCanSetAndGetHeaders()
    {
        $headers = array('Header1' => 14, 'Header2' => 24);
        $this->_object->setHeaders($headers);
        $this->assertEmpty(array_diff($headers, $this->_object->getHeaders()));
    }

    /**
     * @covers Core_Response::setCharset
     * @covers Core_Response::getCharset
     */
    public function testCanSetAndGetEncoding()
    {
        $this->_object->setCharset('win1250');
        $this->assertEquals('win1250', $this->_object->getCharset());
    }

    /**
     * @covers Core_Response::getCharset
     */
    public function testDefaultEncodingIsUtf8()
    {
        $this->assertEquals('UTF-8', $this->_object->getCharset());
    }

    /**
     * @covers Core_Response::setType
     * @covers Core_Response::getType
     */
    public function testCanSetAndGetType()
    {
        $this->_object->setType('text/plain');
        $this->assertEquals('text/plain', $this->_object->getType());
    }

    /**
     * @covers Core_Response::getType
     */
    public function testContentTypeWillBeDeterminedFromFormatIfKnwon()
    {
        $this->_object
            ->setFormat(Core_Response::FORMAT_XML);
        $this->assertEquals('text/xml', $this->_object->getType('Content-Type'));
    }

    /**
     * @covers Core_Response::getType
     */
    public function testDefaultTypeIsTextHtml()
    {
        $this->assertEquals('text/html', $this->_object->getType());
    }

    /**
     * @covers Core_Response::setFormat
     * @covers Core_Response::getFormat
     */
    public function testCanSetAndGetFormat()
    {
        $this->_object->setFormat('xml');
        $this->assertEquals('xml', $this->_object->getFormat());
    }

    /**
     * @covers Core_Response::getFormat
     */
    public function testDefaultFormatIsRaw()
    {
        $this->assertEquals(Core_Response::FORMAT_RAW, $this->_object->getFormat());
    }

    /**
     * @covers Core_Response::setFormat
     */
    public function testSettingAnUnsupportedFormatWillThrowAnInvalidArgumentException()
    {
        $this->setExpectedException('InvalidArgumentException');

        $this->_object->setFormat('no-such-format');
    }

    /**
     * @covers Core_Response::setParams
     * @covers Core_Response::getParams
     */
    public function testCanSetAndGetParams()
    {
        $params = array('a' => 'b', 'c' => 'd');
        $this->_object->setParams($params);
        $this->assertEquals($params, $this->_object->getParams());
    }

    /**
     * @covers Core_Response::setParam
     * @covers Core_Response::getParam
     */
    public function testCanSetAndGetParam()
    {
        $this->_object->setParam('g', 23);
        $this->assertEquals(23, $this->_object->getParam('g'));
    }

    /**
     * @covers Core_Response::getParam
     */
    public function testGetParamWillReturnDefaultValueIfNotFound()
    {
        $this->assertEquals('default', $this->_object->getParam('g', 'default'));
    }

    /**
     * @covers Core_Response::getHeader
     */
    public function testContentTypeHeaderIsConstructedFromEncodingAndType()
    {
        $this->_object
            ->setType('text/xml')
            ->setCharset('win1250');

        $this->assertEquals('text/xml; charset=win1250', $this->_object->getHeader('Content-type'));
    }

    /**
     * @covers Core_Response::getHeader
     */
    public function testContentTypeHeaderWillContainAllSpecifiedParams()
    {
        $this->_object
            ->setParam('a', 999)
            ->setParam('blue', 'yes')
            ->setParam('charset', 'win1250')
            ->setType('text/xml');

        $this->assertEquals('text/xml; charset=win1250; a=999; blue=yes', $this->_object->getHeader('Content-type'));
    }

    /**
     * @covers Core_Response::getHeader
     */
    public function testContentTypeHeaderWillHaveFormatAppendedIfNotRaw()
    {
        $this->_object
            ->setType('text/xml')
            ->setFormat('json')
            ->setCharset('win1250');

        $this->assertEquals('text/xml+json; charset=win1250', $this->_object->getHeader('Content-type'));
    }

    /**
     * @covers Core_Response::getHeader
     */
    public function testContentTypeHeaderWillNotHaveFormatAppendedIfTypeIsOfKnownFormat()
    {
        $this->_object
            ->setType('application/json')
            ->setFormat('json')
            ->setCharset('win1250');

        $this->assertEquals('application/json; charset=win1250', $this->_object->getHeader('Content-type'));
    }

    /**
     * @covers Core_Response::getHeader
     */
    public function testContentMd5HeaderWillBeHashTheBody()
    {
        $this->_object
            ->setBody(array('a' => 'b'))
            ->setFormat('json');

        $this->assertEquals('MWRkODc3ZDFhNGQ3MGI5ZmYyY2U0OTcyZjdiMmQ2OWQ=', $this->_object->getHeader('Content-MD5'));
    }

    /**
     * @runInSeparateProcess
     * @covers Core_Response::render
     * @depends testContentTypeHeaderIsConstructedFromEncodingAndType
     * @group xdebug
     * @group pecl
     */
    public function testWillOutputHeadersWhenDoingARender()
    {
        if (!function_exists('xdebug_get_headers')) {
            $this->markTestSkipped('Requires the xdebug_get_headers() provided by "xdebug" PECL');
        }

        $this->_object
            ->setCaching(false)
            ->setBody('some body')
            ->setType('text/xml')
            ->setCharset('win1250');

        $this->_object->render();
        $headers = xdebug_get_headers();
        // xdebug_get_headers() does not yet return the status header
        // $this->assertContains('HTTP/1.1 200 OK', $headers);
        $this->assertContains('Content-type: text/xml; charset=win1250', $headers);
    }

    /**
     * @runInSeparateProcess
     * @covers Core_Response::render
     * @covers Core_Response::getHeader
     * @depends testContentTypeHeaderIsConstructedFromEncodingAndType
     * @group xdebug
     * @group pecl
     */
    public function testWillSuppressStatusCodeAndAddCustomHeaderXHTTPSuppressedStatusCode()
    {
        if (!function_exists('xdebug_get_headers')) {
            $this->markTestSkipped('Requires the xdebug_get_headers() provided by "xdebug" PECL');
        }

        $this->_object
            ->setCaching(false)
            ->setSuppressCode(true)
            ->setCode(Core_Response::STATUS_INTERNAL_SERVER_ERROR);

        $this->_object->render();
        $headers = xdebug_get_headers();
        // xdebug_get_headers() does not yet return the status header
        // $this->assertContains('HTTP/1.1 200 OK', $headers);
        $this->assertContains('X-HTTP-Suppressed-Status-Code: 500', $headers);
        $this->assertContains('X-HTTP-Suppressed-Status-Message: Internal server error', $headers);
    }

    /**
     * @runInSeparateProcess
     * @covers Core_Response::render
     * @depends testWillOutputHeadersWhenDoingARender
     */
    public function testWillOutputBodyWhenDoingARender()
    {
        $this->_object
            ->setCaching(false)
            ->setBody('foo');
        $this->assertEquals('foo', $this->_object->render());
    }

    /**
     * @runInSeparateProcess
     * @covers Core_Response::render
     * @depends testWillOutputHeadersWhenDoingARender
     */
    public function testWillNotOutputBodyIfNoneIsNoneIsGiven()
    {
        $this->_object
            ->setCaching(false)
            ->setInlineHeaders(true)
            ->setHeader('X-Foo', 23);

        $body = "HTTP/1.1 200 OK\r\nX-foo: 23\r\n\r\n";

        $this->assertEquals($body, $this->_object->render());
    }

    /**
     * @runInSeparateProcess
     * @covers Core_Response::render
     * @depends testWillOutputHeadersWhenDoingARender
     */
    public function testWillOutputHeadersInsideBodyWhenDoingARenderIfInlineHeadersIsTrue()
    {
        $this->_object
            ->setCaching(false)
            ->setInlineHeaders(true)
            ->setHeader('X-Foo', 23)
            ->setBody('foo');

        $body = implode(
            "\r\n",
            array(
                'HTTP/1.1 200 OK',
                'Content-type: text/html; charset=UTF-8',
                'Content-md5: YWNiZDE4ZGI0Y2MyZjg1Y2VkZWY2NTRmY2NjNGE0ZDg=',
                'X-foo: 23',
                "\r\nfoo"
            )
        );
        $this->assertEquals($body, $this->_object->render());
    }

    /**
     * @runInSeparateProcess
     * @covers Core_Response::__toString
     * @depends testWillOutputHeadersWhenDoingARender
     */
    public function testCastingAnObjectToStringWillReturnBody()
    {
        $this->_object->setCaching(false)->setBody('bla-bla');
        $this->assertEquals('bla-bla', (string) $this->_object);
    }

    /**
     * @covers Core_Response::__toString
     */
    public function testAnyExceptionRaisedDuringRenderingWillBeThrownAsAnUserError()
    {
        $this->markTestIncomplete('TODO: fix broken');

        // long story short
        // this will:
        // 1. set an object uncastable to string as body
        // 2. in turn, this raises a Catchable fatal error
        // 3. we catch the error with handleErrors() and throw and Exception
        // 4. exception gets caught by __toString()'s catch block which in turn
        //    raises a E_USER_ERROR
        // 5. this AGAIN gets caught by handleErrors() which now only stores the error message
        $this->_object->setBody(new Invalid_Object);
        set_error_handler(array($this, 'handleErrors'));
        $response = (string) $this->_object;
        restore_error_handler();

        $this->assertEmpty($response);
        $this->assertNotNull(strpos($this->error, 'Invalid_Object'));
    }

    /**
     * @covers Core_Response::setSuppressCode
     * @covers Core_Response::isSuppressCode
     */
    public function testCanSetAndGetSuppressCode()
    {
        $this->_object->setSuppressCode(true);
        $this->assertTrue($this->_object->isSuppressCode());
    }

    /**
     * @covers Core_Response::isSuppressCode
     */
    public function testWillFetchSuppressStatusCodeHTTPHeaderIfSet()
    {
        $request = $this->getMock('Core_Request');
        $request
            ->expects($this->once())
            ->method('getHeader')
            ->with($this->equalTo('X-HTTP-Response-Suppress-Status-Code'), $this->equalTo(false))
            ->will($this->returnValue(true));
        $object = new Core_Response;
        $object->setRequest($request);

        $this->assertTrue($object->isSuppressCode());
    }

    /**
     * @covers Core_Response::isSuppressCode
     */
    public function testWillNotSuppressCodeByDefault()
    {
        $this->assertFalse($this->_object->isSuppressCode());
    }

    /**
     * @covers Core_Response::setInlineHeaders
     * @covers Core_Response::isInlineHeaders
     */
    public function testCanSetAndGetInlineHeaders()
    {
        $this->_object->setInlineHeaders(true);
        $this->assertTrue($this->_object->isInlineHeaders());
    }

    /**
     * @covers Core_Response::isInlineHeaders
     */
    public function testWillFetchInlineHeadersHTTPHeaderIfSet()
    {
        $request = $this->getMock('Core_Request');
        $request
            ->expects($this->once())
            ->method('getHeader')
            ->with($this->equalTo('X-HTTP-Response-Inline-Headers'), $this->equalTo(false))
            ->will($this->returnValue(true));
        $object = new Core_Response;
        $object->setRequest($request);

        $this->assertTrue($object->isInlineHeaders());
    }

    /**
     * @covers Core_Response::isInlineHeaders
     */
    public function testWillNotInlineHeadersByDefault()
    {
        $this->assertFalse($this->_object->isInlineHeaders());
    }

    /**
     * @covers Core_Response::setCaching
     * @covers Core_Response::isCaching
     */
    public function testCanSetAndTestIfIsCachingEnabled()
    {
        $this->_object->setCaching(false);
        $this->assertFalse($this->_object->isCaching());
    }

    /**
     * @covers Core_Response::isCaching
     */
    public function testCachingIsEnabledByDefault()
    {
        $this->assertTrue($this->_object->isCaching());
    }

    /**
     * @runInSeparateProcess
     * @covers Core_Response::render
     */
    public function testWillAddCachingHeadersWhenRenderingIfCachingIsEnabled()
    {
        $body = 'abcd';
        $etag = '"'. sha1($body) .'"';
        $this->_object
            ->setRequest($this->getMock('Core_Request'))
            ->setCaching(true)
            ->setBody($body);
        $this->_object->render();

        $this->assertArrayHasKey('Etag', $this->_object->getHeaders());
        $this->assertEquals($etag, $this->_object->getHeader('Etag'));
    }

    /**
     * @runInSeparateProcess
     * @covers Core_Response::render
     */
    public function testWillSetCodeTo304AndBodyToNullIfRequestAndResponseEtagMatch()
    {
        $body = 'abcd';
        $etag = '"'. sha1($body) .'"';
        $request = $this->getMock('Core_Request');
        $request
            ->expects($this->once())
            ->method('getHeader')
            ->with($this->equalTo('If-None-Match'))
            ->will($this->returnValue($etag));

        $this->_object
            ->setRequest($request)
            ->setCaching(true)
            ->setBody($body);

        $this->_object->render();
        $this->assertEquals(Core_Response::STATUS_NOT_MODIFIED, $this->_object->getCode());
        $this->assertNull($this->_object->getBody());
    }
    

    /**
     * @runInSeparateProcess
     * @covers Core_Response::render
     */
    public function testWillNotSetCodeTo304AndBodyToNullIfCurrentResponseIsNot200EvenIfEtagsMatch()
    {
        $body = 'abcd';
        $etag = '"'. sha1($body) .'"';
        $request = $this->getMock('Core_Request');
        $request
            ->expects($this->once())
            ->method('getHeader')
            ->with($this->equalTo('If-None-Match'))
            ->will($this->returnValue($etag));

        $this->_object
            ->setCode(Core_Response::STATUS_NOT_FOUND)
            ->setRequest($request)
            ->setCaching(true)
            ->setBody($body);

        $this->_object->render();
        $this->assertEquals(Core_Response::STATUS_NOT_FOUND, $this->_object->getCode());
        $this->assertEquals($body, $this->_object->getBody());
    }

    public function handleErrors($errno, $errstr)
    {
        if ($errno !== E_USER_ERROR) {
            throw new Exception($errstr);
        } else {
            $this->error = $errstr;
        }
    }
}

class Invalid_Object
{
}