<?php
/************************************************************************
 * This file is part of EspoCRM.
 *
 * EspoCRM – Open Source CRM application.
 * Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
 * Website: https://www.espocrm.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License version 3.
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
 * these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
 ************************************************************************/

namespace tests\integration\Espo\Core\Select;

use Espo\Core\Application;
use Espo\Core\Container;
use Espo\Core\Select\SearchParams;
use Espo\Core\Select\SelectBuilderFactory;

use Espo\Classes\Select\Email\AdditionalAppliers\Main as EmailAdditionalApplier;
use Espo\ORM\Query\Select;

class SelectBuilderTest extends \tests\integration\Core\BaseTestCase
{
    /**
     * @var SelectBuilderFactory
     */
    private $factory;

    protected function setUp(): void
    {
        parent::setUp();

        $injectableFactory = $this->getContainer()->get('injectableFactory');

        $this->factory = $injectableFactory->create(SelectBuilderFactory::class);
    }

    protected function initTest(array $aclData = [], bool $skipLogin = false, bool $isPortal = false) : Application
    {
        $this->createUser('tester', [
            'data' => $aclData,
        ]);

        if (!$skipLogin) {
            $this->auth('tester');
        }

        $app = $this->createApplication();

        $injectableFactory = $app->getContainer()->get('injectableFactory');

        $this->factory = $injectableFactory->create(SelectBuilderFactory::class);

        $this->user = $app->getContainer()->get('user');

        return $app;
    }

    protected function initTestPortal(array $aclData = [], bool $skipLogin = false) : Application
    {
        $app = $this->createApplication();

        $em = $app->getContainer()->get('entityManager');

        $this->contact = $em->createEntity('Contact', []);
        $this->account = $em->createEntity('Account', []);
        $this->portal = $em->createEntity('Portal', [
            'name' => 'Portal',
        ]);

        $this->createUser(
            [
                'userName' => 'tester',
                'portalsIds' => [$this->portal->id],
                'contactId' => $this->contact->id,
                'accountsIds' => [$this->account->id],
            ],
            [
                'data' => $aclData,
            ],
            true
        );

        if (!$skipLogin) {
            $this->auth('tester', null, $this->portal->id);
        }

        $app = $this->createApplication();

        $injectableFactory = $app->getContainer()->get('injectableFactory');

        $this->factory = $injectableFactory->create(SelectBuilderFactory::class);

        $container = $app->getContainer();

        $this->user = $container->get('user');

        return $app;
    }

    public function testBuild1()
    {
        $app = $this->initTest(
            [
                'Account' => [
                    'read' => 'team',
                ],
            ]
        );

        $container = $app->getContainer();

        $userId = $container->get('user')->id;

        $builder = $this->factory->create();

        $searchParams = SearchParams::fromRaw([
            'orderBy' => 'name',
            'order' => SearchParams::ORDER_DESC,
            'primaryFilter' => 'customers',
            'boolFilterList' => ['onlyMy'],
            'where' => [
                [
                    'type' => 'equals',
                    'attribute' => 'name',
                    'value' => 'test',
                ],
                [
                    'type' => 'before',
                    'attribute' => 'createdAt',
                    'value' => '2020-12-12 10:00',
                    'dateTime' => true,
                ],
            ],
        ]);

        $query = $builder
            ->from('Account')
            ->withStrictAccessControl()
            ->withSearchParams($searchParams)
            ->build();

        $raw = $query->getRaw();

        $expected = [
            'from' => 'Account',
            'orderBy' => [
                [
                    'name',
                    'DESC',
                ],
                [
                    'id',
                    'DESC',
                ],
            ],
            'whereClause' => [
                'id=s' => Select::fromRaw([
                    'select' => [
                        'id',
                    ],
                    'from' => 'Account',
                    'leftJoins' => [
                        [
                            'EntityTeam',
                            'entityTeam',
                            [
                                'entityTeam.entityId:' => 'id',
                                'entityTeam.entityType' => 'Account',
                                'entityTeam.deleted' => false,
                            ],
                        ],
                    ],
                    'whereClause' => [
                        'OR' =>
                            [
                                'entityTeam.teamId' => [],
                                'assignedUserId' => $userId,
                            ],
                    ],
                ]),
                'OR' => [
                    'assignedUserId=' => $userId,
                ],
                'type' => 'Customer',
                [
                    'name=' => 'test',
                ],
                [
                    'createdAt<' => '2020-12-12 10:00:00',
                ],
            ],
        ];

        $this->assertEquals($expected, $raw);
    }

    public function testBuild2()
    {
        $app = $this->initTest(
            [
                'Meeting' => [
                    'read' => 'team',
                ],
            ]
        );

        $container = $app->getContainer();

        $userId = $container->get('user')->id;

        $builder = $this->factory->create();

        $query = $builder
            ->from('Meeting')
            ->withStrictAccessControl()
            ->build();

        $raw = $query->getRaw();

        $expected = [
            'from' => 'Meeting',
            'whereClause' =>
                [
                    'id=s' => Select::fromRaw([
                        'select' =>
                            [
                                0 => 'id',
                            ],
                        'from' => 'Meeting',
                        'leftJoins' =>
                            [
                                0 =>
                                    [
                                        0 => 'EntityTeam',
                                        1 => 'entityTeam',
                                        2 =>
                                            [
                                                'entityTeam.entityId:' => 'id',
                                                'entityTeam.entityType' => 'Meeting',
                                                'entityTeam.deleted' => false,
                                            ],
                                    ],
                                1 =>
                                    [
                                        0 => 'MeetingUser',
                                        1 => 'usersMiddle',
                                        2 =>
                                            [
                                                'usersMiddle.meetingId:' => 'id',
                                                'usersMiddle.deleted' => false,
                                            ],
                                    ],
                            ],
                        'whereClause' =>
                            [
                                'OR' =>
                                    [
                                        0 =>
                                            [
                                                'entityTeam.teamId=' => [],
                                            ],
                                        1 =>
                                            [
                                                'usersMiddle.userId=' => $userId,
                                            ],
                                        2 =>
                                            [
                                                'assignedUserId=' => $userId,
                                            ],
                                    ],
                            ],
                    ]),
                ],
        ];

        $this->assertEquals($expected, $raw);
    }

    public function testEmailAccessFilterOnlyOwn()
    {
        $app = $this->initTest(
            [
                'Email' => [
                    'read' => 'own',
                ],
            ]
        );

        $container = $app->getContainer();

        $userId = $container->get('user')->id;

        $builder = $this->factory->create();

        $query = $builder
            ->from('Email')
            ->withStrictAccessControl()
            ->build();

        $raw = $query->getRaw();

        $expected = [
            'from' => 'Email',
            'leftJoins' =>
                [
                    0 =>
                        [
                            0 => 'EmailUser',
                            1 => 'emailUser',
                            2 =>
                                [
                                    'emailUser.emailId:' => 'id',
                                    'emailUser.deleted' => false,
                                    'emailUser.userId' => $userId,
                                ],
                        ],
                ],
            'whereClause' =>
                [
                    'emailUser.userId' => $userId,
                ]
        ];

        $this->assertEquals($expected, $raw);
    }

    public function testEmailAccessFilterOnlyTeam()
    {
        $app = $this->initTest(
            [
                'Email' => [
                    'read' => 'team',
                ],
            ]
        );

        $container = $app->getContainer();

        $userId = $container->get('user')->id;

        $builder = $this->factory->create();

        $query = $builder
            ->from('Email')
            ->withStrictAccessControl()
            ->build();

        $raw = $query->getRaw();

        $expected = [
            'from' => 'Email',
            'whereClause' =>
                [
                    'id=s' =>
                        Select::fromRaw([
                            'select' =>
                                [
                                    0 => 'id',
                                ],
                            'from' => 'Email',
                            'leftJoins' =>
                                [
                                    0 =>
                                        [
                                            0 => 'EntityTeam',
                                            1 => 'entityTeam',
                                            2 =>
                                                [
                                                    'entityTeam.entityId:' => 'id',
                                                    'entityTeam.entityType' => 'Email',
                                                    'entityTeam.deleted' => false,
                                                ],
                                        ],
                                    1 =>
                                        [
                                            0 => 'EmailUser',
                                            1 => 'emailUser',
                                            2 =>
                                                [
                                                    'emailUser.emailId:' => 'id',
                                                    'emailUser.deleted' => false,
                                                    'emailUser.userId' => $userId,
                                                ],
                                        ],
                                ],
                            'whereClause' =>
                                [
                                    'OR' =>
                                        [
                                            'entityTeam.teamId' => [],
                                            'emailUser.userId' => $userId,
                                        ],
                                ],
                        ]),
                ],
        ];

        $this->assertEquals($expected, $raw);
    }

    public function testEmailAccessFilterOnlyAccount()
    {
        $this->initTestPortal(
            [
                'Email' => [
                    'read' => 'account',
                ],
            ]
        );

        $builder = $this->factory->create();

        $query = $builder
            ->from('Email')
            ->withStrictAccessControl()
            ->build();

        $raw = $query->getRaw();

        $expected = [
            'from' => 'Email',
            'joins' => [],
            'leftJoins' => [
                [
                    'EmailUser',
                    'emailUser',
                    [
                        'emailUser.emailId:' => 'id',
                        'emailUser.deleted' => false,
                        'emailUser.userId' => $this->user->id,
                    ]
                ],
            ],
            'whereClause' => [
                'OR' => [
                    'emailUser.userId' => $this->user->id,
                    'accountId' => [$this->account->id],
                    [
                        'parentType' => 'Contact',
                        'parentId' => $this->contact->id,
                    ]
                ],
            ],
        ];

        $this->assertEquals($expected['whereClause'], $raw['whereClause']);
        $this->assertEquals($expected['leftJoins'], $raw['leftJoins']);
    }

    public function testEmailAccessFilterOnlyContact()
    {
        $this->initTestPortal(
            [
                'Email' => [
                    'read' => 'contact',
                ],
            ]
        );

        $builder = $this->factory->create();

        $query = $builder
            ->from('Email')
            ->withStrictAccessControl()
            ->build();

        $raw = $query->getRaw();

        $expected = [
            'from' => 'Email',
            'joins' => [],
            'leftJoins' => [
                [
                    'EmailUser',
                    'emailUser',
                    [
                        'emailUser.emailId:' => 'id',
                        'emailUser.deleted' => false,
                        'emailUser.userId' => $this->user->id,
                    ]
                ],
            ],
            'whereClause' => [
                'OR' => [
                    'emailUser.userId' => $this->user->id,
                    [
                        'parentType' => 'Contact',
                        'parentId' => $this->contact->id,
                    ]
                ],
            ],
        ];

        $this->assertEquals($expected['whereClause'], $raw['whereClause']);
        $this->assertEquals($expected['leftJoins'], $raw['leftJoins']);
    }

    public function testBuildDefaultOrder()
    {
        $this->initTest(
            []
        );

        $searchParams = SearchParams::fromRaw([]);

        $builder = $this->factory->create();

        $query = $builder
            ->from('Meeting')
            ->withSearchParams($searchParams)
            ->build();

        $raw = $query->getRaw();

        $expectedOrderBy = [
            ['dateStart', 'DESC'],
            ['id', 'DESC'],
        ];

        $this->assertEquals($expectedOrderBy, $raw['orderBy']);
    }

    public function testBuildMeetingDateTime()
    {
        $this->initTest(
            []
        );

        $searchParams = SearchParams::fromRaw([
            'where' => [
                [
                    'type' => 'on',
                    'attribute' => 'dateStart',
                    'value' => '2020-12-12',
                    'dateTime' => true,
                ],
            ],
        ]);

        $builder = $this->factory->create();

        $query = $builder
            ->from('Meeting')
            ->withSearchParams($searchParams)
            ->build();

        $raw = $query->getRaw();

        $expectedWhereClause = [
            'OR' => [
                [
                    'dateStartDate=' => '2020-12-12',
                ],
                [
                    'AND' => [
                        [
                            'AND' => [
                                'dateStart>=' => '2020-12-12 00:00:00',
                                'dateStart<=' => '2020-12-12 23:59:59',
                            ],
                        ],
                        [
                            'dateStartDate=' => null,
                        ],
                    ],
                ],
            ],
        ];

        $this->assertEquals($expectedWhereClause, $raw['whereClause']);
    }

    public function testEmailInbox()
    {
        $app = $this->initTest(
            []
        );

        $container = $app->getContainer();

        $userId = $container->get('user')->id;

        $emailAddressId = $this->createUserEmailAddress($container);

        $searchParams = SearchParams::fromRaw([
            'where' => [
                [
                    'type' => 'inFolder',
                    'attribute' => 'folderId',
                    'value' => 'inbox',
                ],
            ],
        ]);

        $builder = $this->factory->create();

        $query = $builder
            ->from('Email')
            ->withSearchParams($searchParams)
            ->withAdditionalApplierClassNameList([
                EmailAdditionalApplier::class,
            ])
            ->build();

        $raw = $query->getRaw();

        $expectedWhereClause = [
            'emailUser.inTrash' => false,
            'emailUser.folderId' => null,
            'emailUser.userId' => $userId,
            [
                'status' => ['Archived', 'Sent'],
                'groupFolderId' => null,
            ],
            'fromEmailAddressId!=' => [$emailAddressId],
            [
                'OR' => [
                    'status' => 'Archived',
                    'createdById!=' => $userId,
                ],
            ],
        ];

        $expectedLeftJoins = [
            [
                'EmailUser',
                'emailUser',
                [
                    'emailUser.emailId:' => 'id',
                    'emailUser.deleted' => false,
                    'emailUser.userId' => $this->user->id,
                ],
            ],
        ];

        $expectedSelect = [
            '*',
            ['emailUser.isRead', 'isRead'],
            ['emailUser.isImportant', 'isImportant'],
            ['emailUser.inTrash', 'inTrash'],
            ['emailUser.folderId', 'folderId'],
        ];

        $expectedUseIndex = ['dateSent'];

        $this->assertEquals($expectedWhereClause, $raw['whereClause']);
        $this->assertEquals($expectedLeftJoins, $raw['leftJoins']);
        $this->assertEquals($expectedSelect, $raw['select']);
        $this->assertEquals($expectedUseIndex, $raw['useIndex']);
    }

    public function testEmailSent()
    {
        $app = $this->initTest(
            []
        );

        $container = $app->getContainer();

        $userId = $container->get('user')->id;

        $emailAddressId = $this->createUserEmailAddress($container);

        $searchParams = SearchParams::fromRaw([
            'where' => [
                [
                    'type' => 'inFolder',
                    'attribute' => 'folderId',
                    'value' => 'sent',
                ],
            ],
        ]);

        $builder = $this->factory->create();

        $query = $builder
            ->from('Email')
            ->withSearchParams($searchParams)
            ->build();

        $raw = $query->getRaw();

        $expectedWhereClause = [
            'OR' => [
                'fromEmailAddressId' => [$emailAddressId],
                [
                    'status' => 'Sent',
                    'createdById' => $userId,
                ]
            ],
            [
                'status!=' => 'Draft',
            ],
            'emailUser.inTrash' => false,
        ];

        $expectedLeftJoins = [
            [
                'EmailUser',
                'emailUser',
                [
                    'emailUser.emailId:' => 'id',
                    'emailUser.deleted' => false,
                    'emailUser.userId' => $this->user->id,
                ],
            ],
        ];

        $this->assertEquals($expectedWhereClause, $raw['whereClause']);
        $this->assertEquals($expectedLeftJoins, $raw['leftJoins']);
    }

    public function testEmailEmailAddressEquals()
    {
        $app = $this->initTest(
            []
        );

        $em = $app->getContainer()->get('entityManager');

        $emailAddress = $em->createEntity('EmailAddress', [
           'name' => 'test@test.com',
        ]);

        $searchParams = SearchParams::fromRaw([
            'where' => [
                [
                    'type' => 'equals',
                    'attribute' => 'emailAddress',
                    'value' => 'test@test.com',
                ],
            ],
        ]);

        $builder = $this->factory->create();

        $query = $builder
            ->from('Email')
            ->withSearchParams($searchParams)
            ->build();

        $raw = $query->getRaw();

        $this->assertEquals($emailAddress->id, $raw['whereClause']['OR']['fromEmailAddressId']);
    }

    public function testEmailFromEquals()
    {
        $app = $this->initTest();

        $em = $app->getContainer()->get('entityManager');

        $emailAddress = $em->createEntity('EmailAddress', [
           'name' => 'test@test.com',
        ]);

        $searchParams = SearchParams::fromRaw([
            'where' => [
                [
                    'type' => 'equals',
                    'attribute' => 'from',
                    'value' => 'test@test.com',
                ],
            ],
        ]);

        $builder = $this->factory->create();

        $query = $builder
            ->from('Email')
            ->withSearchParams($searchParams)
            ->build();

        $raw = $query->getRaw();

        $expectedWhereClause = [
            'fromEmailAddressId' => $emailAddress->id,
        ];

        $this->assertEquals($expectedWhereClause, $raw['whereClause']);
    }

    public function testEmailBoolOnlyMy()
    {
        $this->initTest();

        $builder = $this->factory->create();

        $query = $builder
            ->from('Email')
            ->withBoolFilter('onlyMy')
            ->build();

        $raw = $query->getRaw();

        $expectedWhereClause = [
            'OR' => [
                [
                    'emailUser.userId' => $this->user->id,
                ],
            ],
        ];

        $this->assertEquals($expectedWhereClause, $raw['whereClause']);
    }

    public function testEmailTextSearch1()
    {
        $app = $this->initTest();

        $emailAddressId = $this->createUserEmailAddress($app->getContainer());

        $builder = $this->factory->create();

        $query = $builder
            ->from('Email')
            ->withTextFilter('test@test.com')
            ->build();

        $raw = $query->getRaw();

        $expectedWhereClause = [
            'OR' => [
                ['NOT_EQUAL:(MATCH_NATURAL_LANGUAGE:(name, bodyPlain, body, \'test@test.com\'), 0):' => null],
                ['fromEmailAddressId=' => $emailAddressId],
                ['emailEmailAddress.emailAddressId=' => $emailAddressId],
            ],
        ];

        $this->assertEquals($expectedWhereClause, $raw['whereClause']);
    }

    protected function createUserEmailAddress(Container $container) : string
    {
        $userId = $container->get('user')->id;

        $em = $container->get('entityManager');

        $user = $em->getEntity('User', $userId);

        $emailAddress = $em->createEntity('EmailAddress', [
            'name' => 'test@test.com',
        ]);

        $em
            ->getRepository('User')
            ->getRelation($user, 'emailAddresses')
            ->relate($emailAddress);

        return $emailAddress->id;
    }
}
