<?php

declare(strict_types=1);

namespace Autogedal\FeedMonitor\Test\Unit\Model;

use Autogedal\FeedMonitor\Model\AlertManager;
use Autogedal\FeedMonitor\Model\AlertNotifier;
use Autogedal\FeedMonitor\Model\Config;
use Autogedal\FeedMonitor\Model\Problem;
use Magento\Framework\Lock\LockManagerInterface;
use PHPUnit\Framework\TestCase;

/**
 * One email when a problem starts, reminders at most every N hours, one email when it is gone.
 */
class AlertManagerTest extends TestCase
{
    private const KEY = 'alfaromtrans_paravanturi_stock';

    private FixedClock $clock;
    private InMemoryStateRepository $states;
    /** @var array<int, array{0: string, 1: string, 2: string}> type, key, problem */
    private array $sent = [];
    private bool $smtpWorks = true;
    /** @var string[] keys whose lock is held by another process */
    private array $lockedElsewhere = [];
    private AlertManager $alerts;

    protected function setUp(): void
    {
        $this->clock = new FixedClock();
        $this->states = new InMemoryStateRepository($this->clock);
        $notifier = $this->createMock(AlertNotifier::class);
        $notifier->method('send')->willReturnCallback(function (string $type, string $key, string $problem): bool {
            if ($this->smtpWorks) {
                $this->sent[] = [$type, $key, $problem];
            }
            return $this->smtpWorks;
        });
        $config = $this->createMock(Config::class);
        $config->method('getReminderHours')->willReturn(6.0);

        $locks = $this->createMock(LockManagerInterface::class);
        $locks->method('lock')->willReturnCallback(
            fn (string $name): bool => !in_array($name, array_map(static fn (string $key): string => 'autogedal_feed_monitor_' . md5($key), $this->lockedElsewhere), true)
        );
        $locks->method('unlock')->willReturn(true);

        $this->alerts = new AlertManager($this->states, $notifier, $config, $this->clock, $locks);
    }

    public function testTheFirstFailureSendsOneAlertAndRepeatedFailuresDoNot(): void
    {
        $this->alerts->reportProblem(self::KEY, Problem::FETCH, 'Connection timed out');
        $this->clock->advanceMinutes(60);
        $this->alerts->reportProblem(self::KEY, Problem::FETCH, 'Connection timed out');

        self::assertSame([[AlertManager::TYPE_ALERT, self::KEY, Problem::FETCH]], $this->sent);
        self::assertTrue($this->alerts->isOpen(self::KEY, Problem::FETCH));
    }

    public function testAReminderGoesAfterTheReminderIntervalWhileTheProblemLasts(): void
    {
        $this->alerts->reportProblem(self::KEY, Problem::FETCH, 'timeout');
        $this->clock->advanceMinutes(5 * 60);
        $this->alerts->reportProblem(self::KEY, Problem::FETCH, 'timeout');
        $this->clock->advanceMinutes(61);
        $this->alerts->reportProblem(self::KEY, Problem::FETCH, 'timeout');

        self::assertSame([AlertManager::TYPE_ALERT, AlertManager::TYPE_REMINDER], array_column($this->sent, 0));
    }

    public function testRemindersAlsoGoForAJobThatDoesNotReportAgain(): void
    {
        $this->alerts->reportProblem(self::KEY, Problem::FETCH, 'timeout');
        $this->clock->advanceMinutes(3 * 60);
        $this->alerts->remindOpenAlerts();
        $this->clock->advanceMinutes(3 * 60);
        $this->alerts->remindOpenAlerts();
        $this->alerts->remindOpenAlerts();

        self::assertSame([AlertManager::TYPE_ALERT, AlertManager::TYPE_REMINDER], array_column($this->sent, 0));
    }

    public function testADifferentProblemIsANewAlert(): void
    {
        $this->alerts->reportProblem(self::KEY, Problem::FETCH, 'timeout');
        $this->alerts->reportProblem(self::KEY, Problem::INVALID, 'HTML instead of XML');

        self::assertSame([Problem::FETCH, Problem::INVALID], array_column($this->sent, 2));
        self::assertSame([AlertManager::TYPE_ALERT, AlertManager::TYPE_ALERT], array_column($this->sent, 0));
    }

    public function testRecoverySendsOneEmailAndClosesTheAlert(): void
    {
        $this->alerts->reportProblem(self::KEY, Problem::FETCH, 'timeout');
        $this->alerts->resolve(self::KEY, '120 produse citite');
        $this->alerts->resolve(self::KEY, '120 produse citite');

        self::assertSame([AlertManager::TYPE_ALERT, AlertManager::TYPE_RECOVERY], array_column($this->sent, 0));
        self::assertFalse($this->alerts->isOpen(self::KEY));
    }

    public function testASuccessWithoutAnOpenAlertSendsNothing(): void
    {
        $this->alerts->resolve(self::KEY, 'ok');

        self::assertSame([], $this->sent);
    }

    public function testASubFeedAlertsOnTheFirstFailureIndependentlyOfTheOthers(): void
    {
        $this->alerts->reportProblem('autogedal_feed_oos_products:schiuri', Problem::EMPTY, 'gol');
        $this->alerts->resolve('autogedal_feed_oos_products:cutii_portbagaj', 'ok');

        self::assertSame([[AlertManager::TYPE_ALERT, 'autogedal_feed_oos_products:schiuri', Problem::EMPTY]], $this->sent);
        self::assertFalse($this->alerts->isOpen('autogedal_feed_oos_products:cutii_portbagaj'));
    }

    public function testNothingIsSentWhileAnotherProcessHoldsTheKey(): void
    {
        $this->lockedElsewhere = [self::KEY];

        try {
            $this->alerts->reportProblem(self::KEY, Problem::FETCH, 'timeout');
            self::fail('expected the lock error');
        } catch (\RuntimeException $e) {
            self::assertStringContainsString('locked by another process', $e->getMessage());
        }
        self::assertSame([], $this->sent);
    }

    public function testALockedKeyDoesNotStopTheRemindersOfTheOthers(): void
    {
        $this->alerts->reportProblem(self::KEY, Problem::FETCH, 'timeout');
        $this->alerts->reportProblem('ptc_update_stock', Problem::INVALID, 'html');
        $this->clock->advanceMinutes(7 * 60);
        $this->lockedElsewhere = [self::KEY];

        try {
            $this->alerts->remindOpenAlerts();
            self::fail('expected the lock error');
        } catch (\RuntimeException) {
            // reported at the end, after the other keys
        }
        self::assertSame([AlertManager::TYPE_REMINDER, 'ptc_update_stock', Problem::INVALID], end($this->sent));
    }

    public function testAFailedRecoveryEmailKeepsTheAlertUntilItIsSent(): void
    {
        $this->alerts->reportProblem(self::KEY, Problem::FETCH, 'timeout');
        $this->smtpWorks = false;
        $this->alerts->resolve(self::KEY, '120 produse citite');

        self::assertTrue($this->alerts->isOpen(self::KEY));
        self::assertSame(1, $this->states->rows[self::KEY]['recovery_pending']);

        $this->smtpWorks = true;
        $this->clock->advanceMinutes(15);
        $this->alerts->remindOpenAlerts();

        self::assertSame([AlertManager::TYPE_ALERT, AlertManager::TYPE_RECOVERY], array_column($this->sent, 0));
        self::assertFalse($this->alerts->isOpen(self::KEY));
    }

    public function testAProblemBackBeforeTheRecoveryEmailLeftGoesOnWithoutANewAlert(): void
    {
        $this->alerts->reportProblem(self::KEY, Problem::FETCH, 'timeout');
        $this->smtpWorks = false;
        $this->alerts->resolve(self::KEY, 'ok');
        $this->smtpWorks = true;
        $this->alerts->reportProblem(self::KEY, Problem::FETCH, 'timeout');

        self::assertSame([AlertManager::TYPE_ALERT], array_column($this->sent, 0));
        self::assertSame(0, $this->states->rows[self::KEY]['recovery_pending']);
    }

    public function testWhenTheFirstEmailFailsTheNextAttemptIsStillAnAlert(): void
    {
        $this->smtpWorks = false;
        $this->alerts->reportProblem(self::KEY, Problem::FETCH, 'timeout');
        $this->smtpWorks = true;
        $this->clock->advanceMinutes(15);
        $this->alerts->remindOpenAlerts();

        self::assertSame([[AlertManager::TYPE_ALERT, self::KEY, Problem::FETCH]], $this->sent);
    }
}
