<?php
declare(strict_types=1);

/**
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 * @link          https://cakephp.org CakePHP(tm) Project
 * @license       https://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace Acl\Test\TestCase\Adapter;

use Acl\Adapter\DbAcl;
use Acl\Controller\Component\AclComponent;
use Acl\Model\Entity\Aco;
use Acl\Model\Entity\Aro;
use Acl\Model\Table\AcosTable;
use Acl\Model\Table\ArosTable;
use Acl\Model\Table\PermissionsTable;
use Cake\Controller\ComponentRegistry;
use Cake\Core\Configure;
use Cake\ORM\TableRegistry;
use Cake\TestSuite\TestCase;

/**
 * AroTwoTest class
 */
class AroTwoTest extends ArosTable
{
    /**
     * initialize
     *
     * @param array $config Configuration array
     * @return void
     */
    public function initialize(array $config): void
    {
        parent::initialize($config);
        $this->setAlias('AroTwoTest');
        $this->setTable('aro_twos');
        $this->associations()->removeAll();
        $this->belongsToMany('AcoTwoTest', [
            'through' => __NAMESPACE__ . '\PermissionTwoTest',
            'className' => __NAMESPACE__ . '\AroTwoTest',
        ]);
    }
}

/**
 * AcoTwoTest class
 */
class AcoTwoTest extends AcosTable
{
    /**
     * initialize
     *
     * @param array $config Configuration array
     * @return void
     */
    public function initialize(array $config): void
    {
        parent::initialize($config);
        $this->setAlias('AcoTwoTest');
        $this->setTable('aco_twos');
        $this->associations()->removeAll();
        $this->belongsToMany('AroTwoTest', [
            'through' => __NAMESPACE__ . '\PermissionTwoTest',
            'className' => __NAMESPACE__ . '\AroTwoTest',
        ]);
    }
}

/**
 * PermissionTwoTest class
 */
class PermissionTwoTest extends PermissionsTable
{
    /**
     * initialize
     *
     * @param array $config Configuration array
     * @return void
     */
    public function initialize(array $config): void
    {
        parent::initialize($config);
        $this->setAlias('PermissionTwoTest');
        $this->setEntityClass('Acl\Model\Entity\Permission');
        $this->setTable('aros_aco_twos');
        $this->associations()->removeAll();
        $this->belongsTo('AroTwoTest', [
            'foreignKey' => 'aro_id',
            'className' => __NAMESPACE__ . '\AroTwoTest',
        ]);
        $this->belongsTo('AcoTwoTest', [
            'foreignKey' => 'aco_id',
            'className' => __NAMESPACE__ . '\AcoTwoTest',
        ]);
    }
}

/**
 * DbAclTwoTest class
 */
class DbAclTwoTest extends DbAcl
{
    /**
     * construct method
     */
    public function __construct()
    {
        $this->Permission = TableRegistry::getTableLocator()->get('Permissions');
        $this->Aro = TableRegistry::getTableLocator()->get('AroTwoTest');
        $this->Aro->Permission = $this->Permission;
        $this->Aco = TableRegistry::getTableLocator()->get('AcoTwoTest');
        $this->Aco->Permission = $this->Permission;

        $this->Permission->Aro = $this->Aro;
        $this->Permission->Aco = $this->Aco;
    }
}

/**
 * Test case for AclComponent using the DbAcl implementation.
 */
class DbAclTest extends TestCase
{
    /**
     * fixtures property
     *
     * @var array
     */
    public $fixtures = [
        'app.AcoTwos',
        'app.AroTwos',
        'app.ArosAcoTwos',
    ];

    /**
     * setUp method
     *
     * @return void
     */
    public function setUp(): void
    {
        parent::setUp();
        Configure::write('Acl.classname', __NAMESPACE__ . '\DbAclTwoTest');
        Configure::write('Acl.database', 'test');

        TableRegistry::getTableLocator()->clear();
        TableRegistry::getTableLocator()->get('Permissions', [
            'className' => __NAMESPACE__ . '\PermissionTwoTest',
        ]);
        TableRegistry::getTableLocator()->get('AroTwoTest', [
            'className' => __NAMESPACE__ . '\AroTwoTest',
        ]);
        TableRegistry::getTableLocator()->get('AcoTwoTest', [
            'className' => __NAMESPACE__ . '\AcoTwoTest',
        ]);

        $Collection = new ComponentRegistry();
        $this->Acl = new AclComponent($Collection);
    }

    /**
     * tearDown method
     *
     * @return void
     */
    public function tearDown(): void
    {
        parent::tearDown();
        unset($this->Acl);
    }

    /**
     * testAclCreate method
     *
     * @return void
     */
    public function testCreate()
    {
        $aro = new Aro(['alias' => 'Chotchkey']);
        $aro = $this->Acl->Aro->save($aro);
        $this->assertTrue((bool)$aro);

        $parent = $aro->id;

        $aro = new Aro(['parent_id' => $parent, 'alias' => 'Joanna']);
        $aro = $this->Acl->Aro->save($aro);
        $this->assertTrue((bool)$aro);

        $aro = new Aro(['parent_id' => $parent, 'alias' => 'Stapler']);
        $aro = $this->Acl->Aro->save($aro);
        $this->assertTrue((bool)$aro);

        $root = $this->Acl->Aco->node('ROOT')->first();
        $parent = $root->id;

        $aco = new Aco(['parent_id' => $parent, 'alias' => 'Drinks']);
        $aco = $this->Acl->Aco->save($aro);
        $this->assertTrue((bool)$aco);

        $aco = new Aco(['parent_id' => $parent, 'alias' => 'PiecesOfFlair']);
        $aco = $this->Acl->Aco->save($aro);
        $this->assertTrue((bool)$aco);
    }

    /**
     * testAclCreateWithParent method
     *
     * @return void
     */
    public function testCreateWithParent()
    {
        $parent = $this->Acl->Aro->findByAlias('Peter')->first();

        $this->Acl->Aro->save(new Aro([
            'alias' => 'Subordinate',
            'model' => 'User',
            'foreign_key' => 7,
            'parent_id' => $parent->id,
        ]));
        $result = $this->Acl->Aro->findByAlias('Subordinate')->first();
        $this->assertSame('AroTwoTest', $result->getSource());
        $this->assertSame(16, $result->lft);
        $this->assertSame(17, $result->rght);
    }

    /**
     * testDbAclAllow method
     *
     * @return void
     */
    public function testAllow()
    {
        $this->expectException(\PHPUnit\Framework\Error\Warning::class);
        $this->assertFalse($this->Acl->check('Micheal', 'tpsReports', 'read'));
        $this->assertTrue($this->Acl->allow('Micheal', 'tpsReports', ['read', 'delete', 'update']));
        $this->assertTrue($this->Acl->check('Micheal', 'tpsReports', 'update'));
        $this->assertTrue($this->Acl->check('Micheal', 'tpsReports', 'read'));
        $this->assertTrue($this->Acl->check('Micheal', 'tpsReports', 'delete'));

        $this->assertFalse($this->Acl->check('Micheal', 'tpsReports', 'create'));
        $this->assertTrue($this->Acl->allow('Micheal', 'ROOT/tpsReports', 'create'));
        $this->assertTrue($this->Acl->check('Micheal', 'tpsReports', 'create'));
        $this->assertTrue($this->Acl->check('Micheal', 'tpsReports', 'delete'));
        $this->assertTrue($this->Acl->allow('Micheal', 'printers', 'create'));
        // Michael no longer has his delete permission for tpsReports!
        $this->assertTrue($this->Acl->check('Micheal', 'tpsReports', 'delete'));
        $this->assertTrue($this->Acl->check('Micheal', 'printers', 'create'));

        $this->assertFalse($this->Acl->check('root/users/Samir', 'ROOT/tpsReports/view'));
        $this->assertTrue($this->Acl->allow('root/users/Samir', 'ROOT/tpsReports/view', '*'));
        $this->assertTrue($this->Acl->check('Samir', 'view', 'read'));
        $this->assertTrue($this->Acl->check('root/users/Samir', 'ROOT/tpsReports/view', 'update'));

        $this->assertFalse($this->Acl->check('root/users/Samir', 'ROOT/tpsReports/update', '*'));
        $this->assertTrue($this->Acl->allow('root/users/Samir', 'ROOT/tpsReports/update', '*'));
        $this->assertTrue($this->Acl->check('Samir', 'update', 'read'));
        $this->assertTrue($this->Acl->check('root/users/Samir', 'ROOT/tpsReports/update', 'update'));
        // Samir should still have his tpsReports/view permissions, but does not
        $this->assertTrue($this->Acl->check('root/users/Samir', 'ROOT/tpsReports/view', 'update'));

        $this->assertFalse($this->Acl->allow('Lumbergh', 'ROOT/tpsReports/DoesNotExist', 'create'));
    }

    /**
     * Test that allow() with an invalid permission name triggers an error.
     *
     * @return void
     */
    public function testAllowInvalidPermission()
    {
        $this->expectException(\Exception::class);
        $this->Acl->allow('Micheal', 'tpsReports', 'derp');
    }

    /**
     * testAllowInvalidNode method
     *
     * @return void
     */
    public function testAllowInvalidNode()
    {
        $this->expectException(\PHPUnit\Framework\Error\Warning::class);
        $this->Acl->allow('Homer', 'tpsReports', 'create');
    }

    /**
     * testDbAclCheck method
     *
     * @return void
     */
    public function testCheck()
    {
        $this->assertTrue($this->Acl->check('Samir', 'print', 'read'));
        $this->assertTrue($this->Acl->check('Lumbergh', 'current', 'read'));
        $this->assertFalse($this->Acl->check('Milton', 'smash', 'read'));
        $this->assertFalse($this->Acl->check('Milton', 'current', 'update'));

        $this->assertFalse($this->Acl->check(null, 'printers', 'create'));
        $this->assertFalse($this->Acl->check('managers', null, 'read'));

        $this->assertTrue($this->Acl->check('Bobs', 'ROOT/tpsReports/view/current', 'read'));
        $this->assertFalse($this->Acl->check('Samir', 'ROOT/tpsReports/update', 'read'));

        $this->assertFalse($this->Acl->check('root/users/Milton', 'smash', 'delete'));
    }

    /**
     * testCheckInvalidNode method
     *
     * @return void
     */
    public function testCheckInvalidNode()
    {
        $this->expectException(\PHPUnit\Framework\Error\Warning::class);
        $this->assertFalse($this->Acl->check('WRONG', 'tpsReports', 'read'));
    }

    /**
     * testCheckInvalidPermission method
     *
     * @return void
     */
    public function testCheckInvalidPermission()
    {
        $this->expectException(\PHPUnit\Framework\Error\Notice::class);
        $this->Acl->check('Lumbergh', 'smash', 'foobar');
    }

    /**
     * testCheckMissingPermission method
     *
     * @return void
     */
    public function testCheckMissingPermission()
    {
        $this->expectException(\PHPUnit\Framework\Error\Warning::class);
        $this->Acl->check('users', 'NonExistent', 'read');
    }

    /**
     * testDbAclCascadingDeny function
     *
     * Setup the acl permissions such that Bobs inherits from admin.
     * deny Admin delete access to a specific resource, check the permissions are inherited.
     *
     * @return void
     */
    public function testAclCascadingDeny()
    {
        $this->Acl->inherit('Bobs', 'ROOT', '*');
        $this->assertTrue($this->Acl->check('admin', 'tpsReports', 'delete'));
        $this->assertTrue($this->Acl->check('Bobs', 'tpsReports', 'delete'));
        $this->Acl->deny('admin', 'tpsReports', 'delete');
        $this->assertFalse($this->Acl->check('admin', 'tpsReports', 'delete'));
        $this->assertFalse($this->Acl->check('Bobs', 'tpsReports', 'delete'));
    }

    /**
     * testDbAclDeny method
     *
     * @return void
     */
    public function testDeny()
    {
        $this->expectException(\PHPUnit\Framework\Error\Warning::class);
        $this->assertTrue($this->Acl->check('Micheal', 'smash', 'delete'));
        $this->Acl->deny('Micheal', 'smash', 'delete');
        $this->assertFalse($this->Acl->check('Micheal', 'smash', 'delete'));
        $this->assertTrue($this->Acl->check('Micheal', 'smash', 'read'));
        $this->assertTrue($this->Acl->check('Micheal', 'smash', 'create'));
        $this->assertTrue($this->Acl->check('Micheal', 'smash', 'update'));
        $this->assertFalse($this->Acl->check('Micheal', 'smash', '*'));

        $this->assertTrue($this->Acl->check('Samir', 'refill', '*'));
        $this->Acl->deny('Samir', 'refill', '*');
        $this->assertFalse($this->Acl->check('Samir', 'refill', 'create'));
        $this->assertFalse($this->Acl->check('Samir', 'refill', 'update'));
        $this->assertFalse($this->Acl->check('Samir', 'refill', 'read'));
        $this->assertFalse($this->Acl->check('Samir', 'refill', 'delete'));

        $result = $this->Acl->Aro->Permission->find('all', [
            'conditions' => ['AroTwoTest.alias' => 'Samir'],
            'contain' => 'AroTwoTest',
        ])->toArray();
        $expected = '-1';
        $this->assertSame($expected, $result[0]->_delete);

        $this->assertFalse($this->Acl->deny('Lumbergh', 'ROOT/tpsReports/DoesNotExist', 'create'));
    }

    /**
     * testAclNodeLookup method
     *
     * @return void
     */
    public function testAclNodeLookup()
    {
        $result = $this->Acl->Aro->node('root/users/Samir')->enableHydration(false)->toArray();
        $expected = [
            ['id' => '7', 'parent_id' => '4', 'model' => 'User', 'foreign_key' => 3, 'alias' => 'Samir'],
            ['id' => '4', 'parent_id' => '1', 'model' => 'Group', 'foreign_key' => 3, 'alias' => 'users'],
            ['id' => '1', 'parent_id' => null, 'model' => null, 'foreign_key' => null, 'alias' => 'root'],
        ];
        $this->assertEquals($expected, $result);

        $result = $this->Acl->Aco->node('ROOT/tpsReports/view/current')->enableHydration(false)->toArray();
        $expected = [
            ['id' => '4', 'parent_id' => '3', 'model' => null, 'foreign_key' => null, 'alias' => 'current'],
            ['id' => '3', 'parent_id' => '2', 'model' => null, 'foreign_key' => null, 'alias' => 'view'],
            ['id' => '2', 'parent_id' => '1', 'model' => null, 'foreign_key' => null, 'alias' => 'tpsReports'],
            ['id' => '1', 'parent_id' => null, 'model' => null, 'foreign_key' => null, 'alias' => 'ROOT'],
        ];
        $this->assertEquals($expected, $result);
    }

    /**
     * testDbInherit method
     *
     * @return void
     */
    public function testInherit()
    {
        //parent doesn't have access inherit should still deny
        $this->assertFalse($this->Acl->check('Milton', 'smash', 'delete'));
        $this->Acl->inherit('Milton', 'smash', 'delete');
        $this->assertFalse($this->Acl->check('Milton', 'smash', 'delete'));

        //inherit parent
        $this->assertFalse($this->Acl->check('Milton', 'smash', 'read'));
        $this->Acl->inherit('Milton', 'smash', 'read');
        $this->assertTrue($this->Acl->check('Milton', 'smash', 'read'));
    }

    /**
     * testDbGrant method
     *
     * @return void
     */
    public function testGrant()
    {
        $this->expectException(\PHPUnit\Framework\Error\Warning::class);
        $this->assertFalse($this->Acl->check('Samir', 'tpsReports', 'create'));
        $this->Acl->allow('Samir', 'tpsReports', 'create');
        $this->assertTrue($this->Acl->check('Samir', 'tpsReports', 'create'));

        $this->assertFalse($this->Acl->check('Micheal', 'view', 'read'));
        $this->Acl->allow('Micheal', 'view', ['read', 'create', 'update']);
        $this->assertTrue($this->Acl->check('Micheal', 'view', 'read'));
        $this->assertTrue($this->Acl->check('Micheal', 'view', 'create'));
        $this->assertTrue($this->Acl->check('Micheal', 'view', 'update'));
        $this->assertFalse($this->Acl->check('Micheal', 'view', 'delete'));

        $this->assertFalse($this->Acl->allow('Peter', 'ROOT/tpsReports/DoesNotExist', 'create'));
    }

    /**
     * testDbRevoke method
     *
     * @return void
     */
    public function testRevoke()
    {
        $this->expectException(\PHPUnit\Framework\Error\Warning::class);
        $this->assertTrue($this->Acl->check('Bobs', 'tpsReports', 'read'));
        $this->Acl->deny('Bobs', 'tpsReports', 'read');
        $this->assertFalse($this->Acl->check('Bobs', 'tpsReports', 'read'));

        $this->assertTrue($this->Acl->check('users', 'printers', 'read'));
        $this->Acl->deny('users', 'printers', 'read');
        $this->assertFalse($this->Acl->check('users', 'printers', 'read'));
        $this->assertFalse($this->Acl->check('Samir', 'printers', 'read'));
        $this->assertFalse($this->Acl->check('Peter', 'printers', 'read'));

        $this->Acl->deny('Bobs', 'ROOT/printers/DoesNotExist', 'create');
    }

    /**
     * debug function - to help editing/creating test cases for the ACL component
     *
     * To check the overall ACL status at any time call $this->_debug();
     * Generates a list of the current aro and aco structures and a grid dump of the permissions that are defined
     * Only designed to work with the db based ACL
     *
     * @param bool $printTreesToo Set to True to output tree data to screen
     * @return void
     */
    protected function _debug($printTreesToo = false)
    {
        $this->Acl->Aro->displayField = 'alias';
        $this->Acl->Aco->displayField = 'alias';
        $aros = $this->Acl->Aro->find('list', ['order' => 'lft']);
        $acos = $this->Acl->Aco->find('list', ['order' => 'lft']);
        $rights = ['*', 'create', 'read', 'update', 'delete'];
        $permissions['Aros v Acos >'] = $acos;
        foreach ($aros as $aro) {
            $row = [];
            foreach ($acos as $aco) {
                $perms = '';
                foreach ($rights as $right) {
                    if ($this->Acl->check($aro, $aco, $right)) {
                        if ($right === '*') {
                            $perms .= '****';
                            break;
                        }
                        $perms .= $right[0];
                    } elseif ($right !== '*') {
                        $perms .= ' ';
                    }
                }
                $row[] = $perms;
            }
            $permissions[$aro] = $row;
        }
        foreach ($permissions as $key => $values) {
            array_unshift($values, $key);
            $values = array_map([&$this, '_pad'], $values);
            $permissions[$key] = implode(' ', $values);
        }
        $permissions = array_map([&$this, '_pad'], $permissions);
        array_unshift($permissions, 'Current Permissions :');
        if ($printTreesToo) {
            debug(['aros' => $this->Acl->Aro->generateTreeList(), 'acos' => $this->Acl->Aco->generateTreeList()]);
        }
        debug(implode("\r\n", $permissions));
    }

    /**
     * pad function
     * Used by debug to format strings used in the data dump
     *
     * @param string $string String for padding
     * @param int $len Padding length
     * @return void
     */
    protected function _pad($string = '', $len = 14)
    {
        return str_pad($string, $len);
    }
}
