<?php

declare(strict_types=1);

namespace Autogedal\FeedMonitor\Test\Unit\Model\Adapter;

use Autogedal\FeedMonitor\Model\Adapter\CustomCronMonitor;
use Autogedal\FeedMonitor\Model\Adapter\CustomSubfeedMonitor;
use Autogedal\FeedMonitor\Model\Adapter\MagentoHeartbeatMonitor;
use Autogedal\FeedMonitor\Model\Config;
use Autogedal\FeedMonitor\Model\FeedMonitor;
use Autogedal\FeedMonitor\Model\FeedRun;
use Autogedal\FeedMonitor\Model\Fetch\CsvFormat;
use Autogedal\FeedMonitor\Model\Fetch\FeedReader;
use Autogedal\FeedMonitor\Model\Fetch\FeedResult;
use Autogedal\FeedMonitor\Model\Heartbeat;
use Autogedal\FeedMonitor\Model\JobRegistry;
use Autogedal\FeedMonitor\Model\MonitorKey;
use Autogedal\FeedMonitor\Model\Problem;
use Autogedal\FeedMonitor\Test\Unit\Model\FixedClock;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

/**
 * The source adapters: custom crons, sub-feeds, Magento cron.
 */
class AdapterTest extends TestCase
{
    private FeedMonitor&MockObject $monitor;
    private FeedReader&MockObject $reader;
    private Config&MockObject $config;
    private JobRegistry $jobs;

    protected function setUp(): void
    {
        $this->monitor = $this->createMock(FeedMonitor::class);
        $this->reader = $this->createMock(FeedReader::class);
        $this->config = $this->createMock(Config::class);
        $this->config->method('getMaxAgeMinutes')->willReturnCallback(
            static fn (string $job, int $default): int => $job === 'alfaromtrans_paravanturi_stock' ? 2000 : $default
        );
        $this->jobs = new JobRegistry([
            'alfaromtrans_paravanturi_stock' => ['max_age_minutes' => 1560],
            'smartbill_stock_update' => ['max_age_minutes' => 60],
            'autogedal_feed_oos_products' => ['max_age_minutes' => 180, 'feeds' => ['schiuri' => 'Schiuri', 'cutii' => 'Cutii']],
        ]);
    }

    public function testAnUnreadableFeedEndsTheRunWithTheReadersProblem(): void
    {
        $run = new FeedRun(3, 'alfaromtrans_paravanturi_stock', '');
        $this->reader->method('read')->willReturn(FeedResult::failed(Problem::FETCH, 'Eroare conexiune: timed out', 3));
        $this->monitor->expects(self::once())->method('fail')->with($run, Problem::FETCH, 'Eroare conexiune: timed out');
        $this->monitor->expects(self::never())->method('acceptItemCount');

        self::assertNull($this->cron()->readFeed($run, 'https://feed', new CsvFormat()));
    }

    public function testAReadableFeedStillNeedsAnAcceptableNumberOfProducts(): void
    {
        $run = new FeedRun(3, 'ptc_update_stock', '');
        $result = FeedResult::ok(['A' => ['A', '1', '2', '3']], 0, 1);
        $this->reader->method('read')->willReturn($result);
        $this->monitor->method('acceptItemCount')->willReturnOnConsecutiveCalls(true, false);

        self::assertSame($result, $this->cron()->readFeed($run, 'https://feed', new CsvFormat()));
        self::assertNull($this->cron()->readFeed($run, 'https://feed', new CsvFormat()));
    }

    public function testAnUpdateWithoutFailuresIsASuccess(): void
    {
        $run = new FeedRun(3, 'ptc_update_stock', '');
        $this->monitor->expects(self::once())->method('success')->with($run, 4200, 3100);
        $this->monitor->expects(self::never())->method('fail');

        $this->cron()->complete($run, 4200, 3100);
    }

    public function testProductsThatCouldNotBeSavedFailTheRunWithTheSavedCount(): void
    {
        $run = new FeedRun(3, 'ptc_update_stock', '');
        $this->monitor->expects(self::once())->method('fail')->with(
            $run,
            Problem::UPDATE,
            '2 produse nu au putut fi salvate, 3098 salvate. Produse: 13119 - Deadlock found; TM402805 - Lock wait timeout.',
            4200,
            3098
        );

        // numeric codes become integer keys
        $this->cron()->complete($run, 4200, 3098, [13119 => 'Deadlock found', 'TM402805' => 'Lock wait timeout']);
    }

    public function testRejectedRowsAndIgnoredPricesArePartOfTheResult(): void
    {
        $run = new FeedRun(3, 'carsupreme_paravanturi_prices', '');
        $run->setRejectedRows(4);
        $this->monitor->expects(self::once())->method('success')->with(
            $run,
            900,
            880,
            '900 produse citite, 880 actualizate. 4 randuri respinse (format invalid, neactualizate); 2 preturi sub 1 leu ignorate.'
        );

        $this->cron()->complete($run, 900, 880, [], ['2 preturi sub 1 leu ignorate']);
    }

    public function testPricesRefusedByMagentoBecomeFailuresPerProduct(): void
    {
        $refused = $this->createMock(\Magento\Catalog\Api\Data\PriceUpdateResultInterface::class);
        $refused->method('getMessage')->willReturn('Invalid attribute %fieldName = %fieldValue.');
        $refused->method('getParameters')->willReturn(['SKU' => 'PV-100', 'fieldName' => 'Price', 'fieldValue' => '-5']);

        self::assertSame(
            ['PV-100 (pret)' => 'Invalid attribute Price = -5.'],
            CustomCronMonitor::priceUpdateFailures([$refused], 'pret')
        );
    }

    public function testCustomCronsAreCheckedWithTheAdminIntervalAndWithoutTheSubFeedJobs(): void
    {
        $checked = [];
        $this->monitor->method('checkNotRunning')->willReturnCallback(
            function (string $job, string $feed, int $maxAge) use (&$checked): ?string {
                $checked[] = [$job, $feed, $maxAge];
                return null;
            }
        );

        $this->cron()->check(false);

        self::assertSame([['alfaromtrans_paravanturi_stock', '', 2000], ['smartbill_stock_update', '', 60]], $checked);
    }

    public function testEverySubFeedIsCheckedOnItsOwn(): void
    {
        $checked = [];
        $this->monitor->method('checkNotRunning')->willReturnCallback(
            function (string $job, string $feed) use (&$checked): ?string {
                $checked[] = MonitorKey::build($job, $feed);
                return $feed === 'cutii' ? 'cutii: stale' : null;
            }
        );

        self::assertSame(['cutii: stale'], $this->subfeed()->check(false));
        self::assertSame(['autogedal_feed_oos_products:schiuri', 'autogedal_feed_oos_products:cutii'], $checked);
    }

    public function testTooManyMissingProductsStopTheSubFeedUnlessAllowed(): void
    {
        $run = new FeedRun(9, 'autogedal_feed_oos_products', 'schiuri');
        $this->monitor->method('isIncompleteAllowed')->willReturnOnConsecutiveCalls(false, true);
        $this->monitor->expects(self::once())->method('fail')
            ->with($run, Problem::INCOMPLETE, self::stringContains('Lipsesc 1600 produse'));

        self::assertTrue($this->subfeed()->acceptMissingCount($run, 1500, 4000));
        self::assertFalse($this->subfeed()->acceptMissingCount($run, 1600, 4000));
        self::assertTrue($this->subfeed()->acceptMissingCount($run, 1600, 4000));
    }

    public function testAnEmptySavedCopyIsAnEmptyReferenceAndAnInvalidOneStopsTheSubFeed(): void
    {
        $run = new FeedRun(9, 'autogedal_feed_oos_products', 'schiuri');
        $this->reader->method('parse')->willReturnOnConsecutiveCalls(
            FeedResult::failed(Problem::EMPTY, 'Continut gol', 0),
            FeedResult::failed(Problem::INVALID, 'Niciun rand nu are formatul asteptat', 0)
        );
        $this->monitor->expects(self::once())->method('fail')->with($run, Problem::INVALID);

        self::assertSame([], $this->subfeed()->readReference($run, '', new CsvFormat())->getRows());
        self::assertNull($this->subfeed()->readReference($run, '<html>', new CsvFormat()));
    }

    public function testTheMagentoCronIsReportedOnlyFromOutsideCronRun(): void
    {
        $clock = new FixedClock();
        $heartbeat = $this->createMock(Heartbeat::class);
        $heartbeat->method('getLastActivity')->willReturnOnConsecutiveCalls($clock->now() - 45 * 60, $clock->now() - 60);
        $this->config->method('getHeartbeatMaxIdleMinutes')->willReturn(30);
        $this->monitor->method('duration')->willReturn('45 min');
        $this->monitor->expects(self::once())->method('raise')->with(MonitorKey::MAGENTO_CRON, Problem::CRON_DOWN);
        $this->monitor->expects(self::once())->method('clear')->with(MonitorKey::MAGENTO_CRON);
        $adapter = new MagentoHeartbeatMonitor($this->monitor, $heartbeat, $this->config, $clock);

        self::assertSame([], $adapter->check(false));
        self::assertCount(1, $adapter->check(true));
        self::assertSame([], $adapter->check(true));
    }

    private function cron(): CustomCronMonitor
    {
        return new CustomCronMonitor($this->monitor, $this->reader, $this->jobs, $this->config);
    }

    private function subfeed(): CustomSubfeedMonitor
    {
        return new CustomSubfeedMonitor($this->monitor, $this->reader, $this->jobs, $this->config);
    }
}
