<?php
declare(strict_types=1);

/**
 * Copyright 2010 - 2026, Cake Development Corporation (https://www.cakedc.com)
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright Copyright 2010 - 2026, Cake Development Corporation (https://www.cakedc.com)
 * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
 */

namespace CakeDC\Users\Test\TestCase\Controller\Traits;

use Cake\Core\Configure;
use Cake\Event\Event;
use Cake\Http\ServerRequest;
use Cake\ORM\TableRegistry;
use CakeDC\Auth\Authentication\AuthenticationService;
use CakeDC\Auth\Authenticator\TwoFactorAuthenticator;
use CakeDC\Auth\Controller\Component\OneTimePasswordAuthenticatorComponent;
use CakeDC\Users\UsersPlugin;

class OneTimePasswordVerifyTraitTest extends BaseTrait
{
    protected array $loginPage = [
        'plugin' => 'CakeDC/Users',
        'prefix' => false,
        'controller' => 'users',
        'action' => 'login',
    ];

    /**
     * setup
     *
     * @return void
     */
    public function setUp(): void
    {
        $this->traitClassName = 'CakeDC\Users\Controller\UsersController';
        $this->traitMockMethods = ['dispatchEvent', 'redirect', 'getUsersTable', 'set'];
        $this->traitMockAddMethods = ['isStopped'];

        parent::setUp();
        $request = new ServerRequest();
        $this->Trait = $this->getMockBuilder($this->traitClassName)
            ->onlyMethods(['dispatchEvent', 'redirect', 'set', 'getUsersTable'])
            ->setConstructorArgs([new ServerRequest()])
            ->getMock();

        $this->Trait->setRequest($request);
        Configure::write('Auth.AuthenticationComponent.loginAction', $this->loginPage);
    }

    /**
     * tearDown
     *
     * @return void
     */
    public function tearDown(): void
    {
        parent::tearDown();
    }

    /**
     * testVerifyHappy
     */
    public function testVerifyHappy()
    {
        Configure::write('OneTimePasswordAuthenticator.login', true);
        $request = $this->getMockBuilder('Cake\Http\ServerRequest')
            ->onlyMethods(['is', 'getData', 'getSession'])
            ->addMethods(['allow'])
            ->getMock();
        $this->Trait->setRequest($request);
        $this->Trait->getRequest()->expects($this->once())
            ->method('is')
            ->with('post')
            ->will($this->returnValue(false));
        $this->Trait->expects($this->never())
            ->method('redirect');

        $this->_mockSession([
            'temporarySession' => [
                'id' => 1,
                'secret_verified' => 1,
            ],
        ]);

        $this->Trait->verify();
    }

    /**
     * testVerifyHappy
     */
    public function testVerifyNotEnabled()
    {
        $this->_mockFlash();
        Configure::write('OneTimePasswordAuthenticator.login', false);
        $this->Trait->setRequest($this->Trait->getRequest()->withQueryParams(['redirect' => 'dashboard/list']));
        $this->Trait->Flash->expects($this->once())
            ->method('error')
            ->with('Please enable Google Authenticator first.');
        $this->Trait->expects($this->once())
            ->method('redirect')
            ->with($this->loginPage + ['?' => ['redirect' => 'dashboard/list']]);

        $this->Trait->verify();
    }

    /**
     * testVerifyHappy
     */
    public function testVerifyGetShowQR()
    {
        Configure::write('OneTimePasswordAuthenticator.login', true);
        $oneTimePasswordAuthenticator = $this->getMockBuilder(OneTimePasswordAuthenticatorComponent::class)
            ->disableOriginalConstructor()
            ->onlyMethods(['createSecret', 'getQRCodeImageAsDataUri'])
            ->getMock();
        $this->Trait->components()->set('OneTimePasswordAuthenticator', $oneTimePasswordAuthenticator);

        $request = $this->getMockBuilder(ServerRequest::class)
            ->onlyMethods(['is', 'getData', 'getSession'])
            ->addMethods(['allow'])
            ->getMock();
        $this->Trait->setRequest($request);

        $this->_mockSession([
            'temporarySession' => [
                'id' => '00000000-0000-0000-0000-000000000001',
                'email' => 'email@example.com',
                'secret_verified' => 0,
            ],
        ]);
        $this->Trait->expects($this->any())
            ->method('getUsersTable')
            ->will($this->returnValue(TableRegistry::getTableLocator()->get('CakeDC/Users.Users')));

        $this->Trait->getRequest()->expects($this->once())
            ->method('is')
            ->with('post')
            ->will($this->returnValue(false));
        $this->Trait->OneTimePasswordAuthenticator->expects($this->once())
            ->method('createSecret')
            ->will($this->returnValue('newSecret'));
        $this->Trait->OneTimePasswordAuthenticator->expects($this->once())
            ->method('getQRCodeImageAsDataUri')
            ->with('email@example.com', 'newSecret')
            ->will($this->returnValue('newDataUriGenerated'));
        $this->Trait->expects($this->once())
            ->method('set')
            ->with(['secretDataUri' => 'newDataUriGenerated', 'secret' => 'newSecret']);

        $this->Trait->verify();
        $user = $this->Trait->getUsersTable()->findById('00000000-0000-0000-0000-000000000001')->firstOrFail();
        $this->assertEquals('newSecret', $user->secret);
    }

    /**
     * Tests that a GET request causes a a new secret to be generated in case it's
     * not already present in the session.
     */
    public function testVerifyGetGeneratesNewSecret()
    {
        Configure::write('OneTimePasswordAuthenticator.login', true);

        $oneTimePasswordAuthenticator = $this
            ->getMockBuilder(OneTimePasswordAuthenticatorComponent::class)
            ->disableOriginalConstructor()
            ->onlyMethods(['createSecret', 'getQRCodeImageAsDataUri'])
            ->getMock();
        $this->Trait->components()->set('OneTimePasswordAuthenticator', $oneTimePasswordAuthenticator);

        $request = $this->getMockBuilder(ServerRequest::class)
            ->onlyMethods(['is', 'getData', 'getSession'])
            ->addMethods(['allow'])
            ->getMock();
        $this->Trait->setRequest($request);
        $this->Trait
            ->getRequest()
            ->expects($this->once())
            ->method('is')
            ->with('post')
            ->will($this->returnValue(false));

        $this->Trait->OneTimePasswordAuthenticator
            ->expects($this->once())
            ->method('createSecret')
            ->will($this->returnValue('newSecret'));
        $this->Trait->OneTimePasswordAuthenticator
            ->expects($this->once())
            ->method('getQRCodeImageAsDataUri')
            ->with('email@example.com', 'newSecret')
            ->will($this->returnValue('newDataUriGenerated'));

        $session = $this->_mockSession([
            'temporarySession' => [
                'id' => '00000000-0000-0000-0000-000000000001',
                'email' => 'email@example.com',
                'secret_verified' => false,
            ],
        ]);
        $this->Trait->expects($this->any())
            ->method('getUsersTable')
            ->will($this->returnValue(TableRegistry::getTableLocator()->get('CakeDC/Users.Users')));
        $this->Trait->verify();

        $this->assertEquals(
            [
                'temporarySession' => [
                    'id' => '00000000-0000-0000-0000-000000000001',
                    'email' => 'email@example.com',
                    'secret_verified' => false,
                    'secret' => 'newSecret',
                ],
            ],
            $session->read(),
        );
    }

    /**
     * Tests that a GET request does not cause a new secret to be generated in case
     * it's already present in the session.
     */
    public function testVerifyGetDoesNotGenerateNewSecret()
    {
        Configure::write('OneTimePasswordAuthenticator.login', true);

        $oneTimePasswordAuthenticator = $this
            ->getMockBuilder(OneTimePasswordAuthenticatorComponent::class)
            ->disableOriginalConstructor()
            ->onlyMethods(['createSecret', 'getQRCodeImageAsDataUri'])
            ->getMock();
        $this->Trait->components()->set('OneTimePasswordAuthenticator', $oneTimePasswordAuthenticator);

        $request = $this->getMockBuilder(ServerRequest::class)
            ->onlyMethods(['is', 'getData', 'getSession'])
            ->addMethods(['allow'])
            ->getMock();
        $this->Trait->setRequest($request);
        $this->Trait
            ->getRequest()
            ->expects($this->once())
            ->method('is')
            ->with('post')
            ->will($this->returnValue(false));

        $this->Trait->OneTimePasswordAuthenticator
            ->expects($this->never())
            ->method('createSecret');
        $this->Trait->OneTimePasswordAuthenticator
            ->expects($this->once())
            ->method('getQRCodeImageAsDataUri')
            ->with('email@example.com', 'alreadyPresentSecret')
            ->will($this->returnValue('newDataUriGenerated'));

        $session = $this->_mockSession([
            'temporarySession' => [
                'id' => '00000000-0000-0000-0000-000000000001',
                'email' => 'email@example.com',
                'secret_verified' => false,
                'secret' => 'alreadyPresentSecret',
            ],
        ]);
        $this->Trait->expects($this->any())
            ->method('getUsersTable')
            ->will($this->returnValue(TableRegistry::getTableLocator()->get('CakeDC/Users.Users')));
        $this->Trait->verify();

        $this->assertEquals(
            [
                'temporarySession' => [
                    'id' => '00000000-0000-0000-0000-000000000001',
                    'email' => 'email@example.com',
                    'secret_verified' => false,
                    'secret' => 'alreadyPresentSecret',
                ],
            ],
            $session->read(),
        );
    }

    /**
     * testVerifySkipEventCheck
     */
    public function testVerifySkipEventCheck()
    {
        Configure::write('OneTimePasswordAuthenticator.login', true);
        $request = $this->getMockBuilder('Cake\Http\ServerRequest')
            ->onlyMethods(['is', 'getData', 'getSession'])
            ->addMethods(['allow'])
            ->getMock();
        $this->Trait->setRequest($request);

        $userData = [
            'id' => 1,
            'secret_verified' => 1,
            'email' => 'test@example.com',
        ];
        $session = $this->_mockSession([
            'temporarySession' => $userData,
        ]);

        $eventMock = $this->getMockBuilder(Event::class)
            ->disableOriginalConstructor()
            ->getMock();
        $eventMock->method('getResult')->willReturn(true);

        $this->Trait->expects($this->once())
            ->method('dispatchEvent')
            ->with(UsersPlugin::EVENT_2FA_SKIP_VERIFY, ['user' => $userData])
            ->willReturn($eventMock);

        $this->Trait->expects($this->once())
            ->method('redirect');
        $this->Trait->verify();

        $this->assertNull(
            $session->read(AuthenticationService::TWO_FACTOR_VERIFY_SESSION_KEY),
        );

        $this->assertEquals(
            $userData,
            $session->read(TwoFactorAuthenticator::USER_SESSION_KEY),
        );
    }
}
