<?php
declare(strict_types=1);

/**
 * BEdita, API-first content management framework
 * Copyright 2019 ChannelWeb Srl, Chialab Srl
 *
 * This file is part of BEdita: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
 */

namespace BEdita\API\Test\TestCase\Error;

use BEdita\Core\Exception\InvalidDataException;
use Cake\Core\Configure;
use Cake\Core\Exception\CakeException;
use Cake\Core\Plugin;
use Cake\Http\Exception\BadRequestException;
use Cake\Http\Exception\InternalErrorException;
use Cake\Http\Exception\NotFoundException;
use Cake\Http\Response;
use Cake\TestSuite\TestCase;
use Exception;

/**
 * @coversDefaultClass \BEdita\API\Error\ExceptionRenderer
 */
class ExceptionRendererTest extends TestCase
{
    /**
     * The configuration to restore at the end of every unit test
     *
     * @var array
     */
    protected $backupConf = [];

    /**
     * @inheritDoc
     */
    public function setUp(): void
    {
        parent::setUp();
        $this->backupConf = [
            'debug' => Configure::read('debug'),
            'Accept.html' => Configure::read('Accept.html'),
            'App.paths.templates' => Configure::read('App.paths.templates'),
        ];
    }

    /**
     * @inheritDoc
     */
    public function tearDown(): void
    {
        foreach ($this->backupConf as $key => $val) {
            Configure::write($key, $val);
        }

        parent::tearDown();
    }

    /**
     * Data provider for `testErrorDetails` test case.
     *
     * @return array
     */
    public function errorDetailsProvider()
    {
        return [
            'simple' => [
                new Exception('err msg'),
                'err msg',
            ],
            'detail' => [
                new InvalidDataException('err title', ['err detail']),
                'err title',
                '[0]: err detail',
            ],
            'detailArray' => [
                new InvalidDataException('new title', [['field' => ['cause' => 'err detail']]]),
                'new title',
                '[0.field.cause]: err detail',
            ],
            'detailArray2' => [
                new InvalidDataException('new title', [
                    'field' => ['cause' => 'err detail'],
                    'nestedFields' => [
                        'field2' => ['cause2' => 'err detail2'],
                        'field3' => ['cause3' => 'err detail3'],
                    ],
                ]),
                'new title',
                '[field.cause]: err detail [nestedFields.field2.cause2]: err detail2 [nestedFields.field3.cause3]: err detail3',
            ],
            'code' => [
                new CakeException(['title' => 'err title', 'code' => 'err-code']),
                'err title',
                null,
                'err-code',
            ],
            'badCode' => [
                new CakeException(['title' => 'err title', 'code' => ['err-code']]),
                'err title',
            ],
            'previous exception' => [
                new BadRequestException('Bad', 400, new \LogicException('Logic Error')),
                'Bad',
                'Logic Error',
            ],
            'not a Cake exception' => [
                new \LogicException('hello'),
                'hello',
            ],
        ];
    }

    /**
     * Test error detail on response
     *
     * @param \Exception $exception Expected error.
     * @param string $title Expected error title.
     * @param string $detail Additional details.
     * @param string $code Error code.
     * @return void
     * @dataProvider errorDetailsProvider
     * @covers ::render()
     * @covers ::_message()
     * @covers ::_template()
     * @covers ::errorDetail()
     * @covers ::appErrorCode()
     */
    public function testErrorDetails($exception, $title, $detail = '', $code = '')
    {
        $renderer = new MyExceptionRenderer($exception);
        $renderer->getController()->setRequest($renderer->getController()->getRequest()->withEnv('HTTP_ACCEPT', 'application/json'));
        $response = $renderer->render();

        $responseBody = json_decode((string)$response->getBody(), true);
        static::assertEquals('error', $renderer->getTemplate());
        static::assertEquals($title, $responseBody['error']['title']);
        if ($detail) {
            static::assertEquals($detail, $responseBody['error']['detail']);
        } else {
            static::assertArrayNotHasKey('detail', $responseBody['error']);
        }
        if ($code) {
            static::assertEquals($code, $responseBody['error']['code']);
        } else {
            static::assertArrayNotHasKey('code', $responseBody['error']);
        }
    }

    /**
     * Data provider for `testRenderJson` test case.
     *
     * @return array
     */
    public function renderJsonProvider()
    {
        return [
            'debugOn' => [
                'application/vnd.api+json',
                [
                    'debug' => 1,
                ],
            ],
            'debugOff' => [
                'text/html',
                [
                    'debug' => 0,
                    'Accept' => [
                        'html' => false,
                    ],
                ],
            ],
            'debugOnPluginOff' => [
                'application/vnd.api+json',
                [
                    'debug' => 1,
                ],
                true,
            ],
            'debugOffPluginOff' => [
                'text/html',
                [
                    'debug' => 0,
                    'Accept' => [
                        'html' => false,
                    ],
                ],
                true,
            ],
        ];
    }

    /**
     * Test render json error
     *
     * @param string $accept Request's "Accept" header.
     * @param array $config The configuration to use.
     * @param bool $unloadPlugin If unload BEdita/API before render.
     * @return void
     * @dataProvider renderJsonProvider
     * @coversNothing
     */
    public function testRenderJson($accept, $config, $unloadPlugin = false)
    {
        Configure::write($config);

        if ($unloadPlugin) {
            Plugin::getCollection()->clear();
        }

        $renderer = new MyExceptionRenderer(new NotFoundException('test html'));
        $renderer->getController()->setRequest($renderer->getController()->getRequest()->withEnv('HTTP_ACCEPT', $accept));
        $response = $renderer->render();

        $this->checkResponseJson($renderer, $response, $config['debug']);
    }

    /**
     * Test render json error forcing the fallback to ::_outputMessageSafe()
     *
     * @param string $accept Request's "Accept" header.
     * @param array $config The configuration to use.
     * @param bool $unloadPlugin If unload BEdita/API before render.
     * @return void
     * @dataProvider renderJsonProvider
     * @covers ::_outputMessageSafe()
     */
    public function testRenderJsonSafe($accept, $config, $unloadPlugin = false)
    {
        Configure::write($config);

        if ($unloadPlugin) {
            Plugin::getCollection()->clear();
        }

        $renderer = new MyExceptionRenderer(new NotFoundException('test html'));
        $renderer->getController()->setRequest($renderer->getController()->getRequest()->withEnv('HTTP_ACCEPT', $accept));

        $renderer->getController()->getEventManager()->on('Controller.beforeRender', function () {
            throw new InternalErrorException();
        });

        $response = $renderer->render();

        $this->checkResponseJson($renderer, $response, $config['debug']);
    }

    /**
     * Perform some asserts to check JSON response
     *
     * @param \BEdita\API\Error\ExceptionRenderer $renderer
     * @param \Cake\Http\Response $response
     * @param int $debug
     * @return void
     */
    protected function checkResponseJson(MyExceptionRenderer $renderer, Response $response, $debug)
    {
        $accept = $renderer->getController()->getRequest()->getHeaderLine('accept');
        $contentTypeExpected = $accept == 'application/json' ? $accept : 'application/vnd.api+json';
        $this->assertStringStartsWith($contentTypeExpected, $response->getType());
        $responseBody = json_decode((string)$response->getBody(), true);
        $this->assertTrue(is_array($responseBody));

        $this->assertArrayHasKey('error', $responseBody);
        $this->assertArrayHasKey('status', $responseBody['error']);
        $this->assertArrayHasKey('title', $responseBody['error']);
        $this->assertEquals(404, $responseBody['error']['status']);
        $this->assertEquals('test html', $responseBody['error']['title']);
        if (!$debug) {
            $this->assertArrayNotHasKey('meta', $responseBody['error']);
        } else {
            $this->assertArrayHasKey('meta', $responseBody['error']);
            $this->assertNotEmpty($responseBody['error']['meta']);
            $this->assertArrayHasKey('trace', $responseBody['error']['meta']);
            $this->assertNotEmpty($responseBody['error']['meta']['trace']);
        }
    }
}
