<?php

declare(strict_types=1);

namespace KupShop\BonusProgramBundle\Tests;

use KupShop\BonusProgramBundle\Email\BonusPointsExpiryEmail;
use KupShop\BonusProgramBundle\EventListener\CronListener;
use KupShop\BonusProgramBundle\Utils\BonusComputer;
use KupShop\BonusProgramBundle\Utils\BonusProvider;
use PHPUnit\DbUnit\DataSet\ArrayDataSet;

class BonusProgramEmailExpiryTest extends \DatabaseTestCase
{
    private const REMOVE_TIME = 60;
    private const EXPIRY_EMAIL_TIME = 14;

    private BonusComputerMock $bonusComputer;
    private BonusProvider $bonusProvider;

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

        $this->bonusComputer = $this->autowire(BonusComputerMock::class);
        $this->bonusProvider = $this->get(BonusProvider::class);
    }

    public function testGetUsersWithoutOrdersInLast(): void
    {
        $result = $this->bonusComputer->getUsersWithoutOrdersInLastPub(4);

        // user 1 has an order today
        // user 2 does not have any recent orders and has active points
        // user 3 does not have active points
        $this->assertEquals(0, $this->bonusProvider->getActivePointsAmount(3));
        $this->assertTrue($this->bonusProvider->getActivePointsAmount(2) > 0);

        $this->assertEquals([2, 4], $result, 'Špatně načtení uživatelé s bez nedávných objednávek!');

        $result1 = $this->bonusComputer->getUsersWithoutOrdersInLastPub(40);
        $this->assertEquals([4], $result1, 'Špatně načtení uživatelé s bez nedávných objednávek!');

        $result2 = $this->bonusComputer->getUsersWithoutOrdersInLastPub(60);
        $this->assertEmpty($result2, 'Špatně načtení uživatelé s bez nedávných objednávek!');
    }

    public function testGetPointsThatWillExpire(): void
    {
        // user 1 has an order created 3 days ago
        $dateCreated = sqlQueryBuilder()->select('DATE(date_created)')
            ->from('orders')
            ->where('id_user = 1')
            ->execute()->fetchFirstColumn();

        $this->assertEquals(
            (new \DateTimeImmutable())->sub(new \DateInterval('P3D'))->format('Y-m-d'),
            $dateCreated[0] ?? false,
        );

        // user 1 has points that are 2 days old
        // remove time will be set to 60
        //      -> 60 - 2 = 58
        //          -> if the user would be notified today, the expiry_email_time would have to be set to 58 days
        //          -> test lookup of today + 58 days (simulate remove_time = 60 & expiry_email_time = 58)

        $pointsCreatedDate = sqlQueryBuilder()->select('DATE(date_created)')
            ->from('bonus_points')
            ->where('id_user = 1')
            ->execute()->fetchFirstColumn();

        $this->assertCount(1, $pointsCreatedDate);
        $this->assertEquals(
            (new \DateTimeImmutable())->sub(new \DateInterval('P2D'))->format('Y-m-d'),
            $pointsCreatedDate[0] ?? false,
        );

        $result = $this->bonusComputer->getPointsThatExpireIn(
            58,
            self::REMOVE_TIME,
        );

        $this->assertEquals([
            ['id_user' => 1, 'points' => 256],
        ], $result, 'Neočekávaní uživatelé, kterým by měly expirovat body z BonusProgramu!');
    }

    public function testEmailGetsSent(): void
    {
        $this->set(
            BonusPointsExpiryEmail::class,
            $this->autowire(PointsExpiryEmailMock::class),
        );

        $dbcfg = \Settings::getDefault();
        $dbcfg->saveValue('bonus_program', [
            'expiry_email_time' => self::EXPIRY_EMAIL_TIME,
            'remove_time' => self::REMOVE_TIME,
        ]);

        \Settings::clearCache(true);
        $listener = $this->get(CronListener::class);

        $listener->sendPointsExpiryEmails();

        $this->assertCount(
            1,
            PointsExpiryEmailMock::$payloads,
            'E-mail o expiraci bodů nebyl odeslán!',
        );

        // make points 1 day older
        sqlQueryBuilder()->update('bonus_points')
            ->where('id = 163')
            ->set('date_created', 'date_created - INTERVAL 1 DAY')
            ->execute();

        PointsExpiryEmailMock::$payloads = [];
        $listener->sendPointsExpiryEmails();
        $this->assertCount(
            0,
            PointsExpiryEmailMock::$payloads,
            'E-mail o expiraci bodů byl odeslán vícekrát!',
        );

        // make points 1 day newer (1 + 1 from previous change)
        sqlQueryBuilder()->update('bonus_points')
            ->where('id = 163')
            ->set('date_created', 'date_created + INTERVAL 2 DAY')
            ->execute();

        PointsExpiryEmailMock::$payloads = [];
        $listener->sendPointsExpiryEmails();
        $this->assertCount(
            0,
            PointsExpiryEmailMock::$payloads,
            'E-mail o expiraci bodů byl odeslán o den dříve než měl být!',
        );
    }

    public function testEmailDoesNotGetSent()
    {
        $this->set(
            BonusPointsExpiryEmail::class,
            $this->autowire(PointsExpiryEmailMock::class),
        );

        $dbcfg = \Settings::getDefault();
        $dbcfg->saveValue('bonus_program', [
            'expiry_email_time' => self::EXPIRY_EMAIL_TIME,
            'remove_time' => self::REMOVE_TIME,
        ]);

        \Settings::clearCache(true);
        $listener = $this->get(CronListener::class);

        $listener->sendPointsExpiryEmails();

        $this->assertCount(
            1,
            PointsExpiryEmailMock::$payloads,
            'E-mail o expiraci bodů nebyl odeslán!',
        );
    }

    protected function getJsonDataSet($string): ArrayDataSet
    {
        $json = json_decode_strict($string, true);

        $twoDaysAgo = (new \DateTimeImmutable())
            ->sub(new \DateInterval('P2D'))
            ->format('Y-m-d H:i:s');

        $threeDaysAgo = (new \DateTimeImmutable())
            ->sub(new \DateInterval('P3D'))
            ->format('Y-m-d H:i:s');

        $removeTime = self::REMOVE_TIME;
        $notifyTime = self::EXPIRY_EMAIL_TIME;
        $exactlyNeededTime = (new \DateTimeImmutable())
            ->add(new \DateInterval("P{$notifyTime}D"))
            ->sub(new \DateInterval("P{$removeTime}D"))
            ->format('Y-m-d H:i:s');
        // meaning the user should be notified today
        // there is 60 day ($removeTime) difference between date_created and 14 days from now ($notifyTime)

        $sixteenDaysAgo = (new \DateTimeImmutable())
            ->sub(new \DateInterval('P16D'))
            ->format('Y-m-d H:i:s');

        $orders = &$json['orders'];
        $orders[0]['date_created'] = $threeDaysAgo;
        $orders[1]['date_created'] = $sixteenDaysAgo;

        $bonusPoints = &$json['bonus_points'];
        $bonusPoints[0]['date_created'] = $twoDaysAgo;
        $bonusPoints[3]['date_created'] = $exactlyNeededTime;

        return new ArrayDataSet($json);
    }

    protected function getDataSet()
    {
        return $this->getJsonDataSetFromFile();
    }
}

class BonusComputerMock extends BonusComputer
{
    public function getUsersWithoutOrdersInLastPub(int $days): array
    {
        return $this->getUsersWithoutOrdersInLast($days);
    }
}

class PointsExpiryEmailMock extends BonusPointsExpiryEmail
{
    public static array $payloads = [];

    public function sendEmail($message)
    {
        self::$payloads[] = $message;

        return parent::sendEmail($message);
    }
}
