<?php

namespace Velis\Test;

use Mockery;
use PHPUnit\Framework\TestCase;
use Velis\Acl\Role;
use Velis\App;
use Velis\Arrays;
use Velis\Db\Postgres;
use Velis\Exception\BusinessLogicException;
use Velis\Lang;
use Velis\Mvc\Controller\AccessException;
use Velis\User;

/**
 * @author Jan Małysiak <jan.malysiak@velis.pl>
 */
class UserTest extends TestCase
{
    /**
     * @param $firstName
     * @param $lastName
     * @param $reverse
     * @param $locale
     * @param $expectedResult
     *
     * @dataProvider provideTestDataForGetFullNameMethod
     */
    public function testGetFullName($firstName, $lastName, $reverse, $locale, $expectedResult)
    {
        $user = new User([
            'name' => $firstName,
            'last_name' => $lastName,
        ]);

        App::$user['locale'] = $locale;
        App::$user['country_id'] = substr($locale, '3');
        Lang::switchLanguage(substr($locale, 0, 2), false);

        $actualResult = $user->getFullName($reverse);

        $this->assertEquals($expectedResult, $actualResult);
    }

    public function provideTestDataForGetFullNameMethod()
    {
        return [
            ['Jan', 'Kowalski', false, 'pl_PL', 'Jan Kowalski'],
            ['Jan', 'Kowalski', true, 'pl_PL', 'Kowalski Jan'],
            ['John', 'Smith', true, 'en_GB', 'Smith John'],
            ['John', 'Smith', true, 'en_US', 'Smith, John'],
        ];
    }

    /**
     * @param array $roleIds
     *
     * @dataProvider provideTestDataForSaveRolesMethodWhenMixingInternalAndExternalRoles
     */
    public function testSaveRolesThrowExceptionWhenMixingInternalAndExternalRoles(array $roleIds)
    {
        $this->expectException(BusinessLogicException::class);

        $user = new User();
        $user->saveRoles($roleIds, true);
    }

    public function provideTestDataForSaveRolesMethodWhenMixingInternalAndExternalRoles()
    {
        /** @var Role $internalRole */
        $internalRole = Arrays::getFirst(Role::listInternal());

        /** @var Role $externalRole */
        $externalRole = Arrays::getFirst(Role::listExternal());

        $roleIds = [$internalRole->id(), $externalRole->id()];

        return [
            [$roleIds],
        ];
    }

    /**
     * @param User $user
     * @param array $roleIds
     *
     * @dataProvider provideTestDataForSaveRolesMethod
     */
    public function testSaveRoles(User $user, array $roleIds)
    {
        $user->saveRoles($roleIds, true);
        unset($user['acl_roles']);

        foreach ($roleIds as $roleId) {
            $this->assertTrue($user->hasRole($roleId));
        }
    }

    public function provideTestDataForSaveRolesMethod()
    {
        $internalUser = Arrays::getFirst(User::listAll([
            'is_internal' => 1,
            'active' => 0,
        ]));

        $internalRoleIds = Arrays::getColumn(Role::listInternal(), 'role_id');

        $externalUser = Arrays::getFirst(User::listAll([
            'role_id' => 'Tenant',
            'active' => 0,
        ]));

        $externalRoleIds = Arrays::getColumn(Role::listExternal(), 'role_id');

        return [
            [$internalUser, $internalRoleIds],
            [$externalUser, $externalRoleIds],
        ];
    }

    public function testHasAnyPrivConditionConstruction(): void
    {
        $userId = 1;
        $privs = [
            ['Inspection', 'Access'],
            ['Inspection', 'ReadOnly'],
            ['Inspection', 'List'],
        ];
        $userMock = Mockery::mock(User::class)->makePartial();
        $postgresMock = Mockery::mock(Postgres::class)->makePartial();

        $userMock::changeDb($postgresMock);

        $postgresMock
            ->expects('cacheGetOne')
            ->withArgs(function (string $sql, array $bindings) use ($userId) {
                $this->assertEquals([
                    'user_id' => $userId,
                    'app_module_id_0' => 'Inspection',
                    'priv_acro_0' => 'Access',
                    'app_module_id_1' => 'Inspection',
                    'priv_acro_1' => 'ReadOnly',
                    'app_module_id_2' => 'Inspection',
                    'priv_acro_2' => 'List',
                ], $bindings);

                $this->assertTrue(
                    str_contains(
                        $sql,
                        'AND user_id = :user_id AND ('
                        . '(app_module_id = :app_module_id_0 AND priv_acro = :priv_acro_0)'
                        . ' OR (app_module_id = :app_module_id_1 AND priv_acro = :priv_acro_1)'
                        . ' OR (app_module_id = :app_module_id_2 AND priv_acro = :priv_acro_2))'
                    ),
                );
            });

        $userMock
            ->expects('id')
            ->andReturns($userId)
        ;

        $userMock->hasAnyPriv($privs);
    }

    public function testHasAnyPrivNoAccess(): void
    {
        $userId = 1;
        $privs = [
            ['Inspection', 'Access'],
        ];
        $userMock = Mockery::mock(User::class)->makePartial();
        $postgresMock = Mockery::mock(Postgres::class)->makePartial();

        $userMock::changeDb($postgresMock);

        $postgresMock
            ->expects('cacheGetOne')
            ->andReturns(0)
        ;

        $userMock
            ->expects('id')
            ->andReturns($userId)
        ;

        $result = $userMock->hasAnyPriv($privs);

        $this->assertFalse($result);
    }

    public function testHasAnyPrivAccessGranted(): void
    {
        $userId = 1;
        $privs = [
            ['Inspection', 'Access'],
        ];
        $userMock = Mockery::mock(User::class)->makePartial();
        $postgresMock = Mockery::mock(Postgres::class)->makePartial();

        $userMock::changeDb($postgresMock);

        $postgresMock
            ->expects('cacheGetOne')
            ->andReturns(1)
        ;

        $userMock
            ->expects('id')
            ->andReturns($userId)
        ;

        $result = $userMock->hasAnyPriv($privs);

        $this->assertTrue($result);
    }

    public function testHasAnyPrivEmptyList(): void
    {
        $privs = [];

        $user = new User();
        $result = $user->hasAnyPriv($privs);

        $this->assertFalse($result);
    }

    public function testCheckAnyPrivForbidden(): void
    {
        $this->expectException(AccessException::class);

        $privs = [];
        $userMock = Mockery::mock(User::class)->makePartial();
        $userMock->expects('hasAnyPriv')->andReturns(false);

        $userMock->checkAnyPriv($privs);
    }

    public function testCheckAnyPrivAccessGranted(): void
    {
        $privs = [['module', 'priv']];
        $userMock = Mockery::mock(User::class)->makePartial();
        $userMock->expects('hasAnyPriv')->andReturns(true)->once();

        $userMock->checkAnyPriv($privs);

        $this->expectNotToPerformAssertions();
    }
}
