<?php

declare(strict_types=1);

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

use Autogedal\FeedMonitor\Model\Fetch\CsvFormat;
use Autogedal\FeedMonitor\Model\Fetch\FeedFetcher;
use Autogedal\FeedMonitor\Model\Fetch\FeedReader;
use Autogedal\FeedMonitor\Model\Fetch\FetchResult;
use Autogedal\FeedMonitor\Model\Fetch\InvalidFeedException;
use Autogedal\FeedMonitor\Model\Fetch\XmlFormat;
use Autogedal\FeedMonitor\Model\Problem;
use PHPUnit\Framework\TestCase;

/**
 * Format validation of the common reader, with samples shaped like the real feeds.
 */
class FeedFormatTest extends TestCase
{
    private const ERROR_PAGE = "<!DOCTYPE html>\n<html><head><title>503 Service Unavailable</title></head>\n<body><h1>Service Unavailable</h1></body></html>\n";

    public function testCarsupremeCsvWithAHeaderIsKeyedByIdAndNamedColumns(): void
    {
        $csv = "\xEF\xBB\xBFid, title ,availability,price\n"
            . "PV-100,\"Paravant, set fata\",in stock,\"149,00 RON\"\n"
            . " PV-101 ,\"Paravant\nspate\",out of stock,99\n"
            . ",fara cod,in stock,1\n";

        $rows = (new CsvFormat(',', true, 'id', ['availability']))->parse($csv)->getRows();

        self::assertSame(['PV-100', 'PV-101'], array_keys($rows));
        self::assertSame('in stock', $rows['PV-100']['availability']);
        self::assertSame("Paravant\nspate", $rows['PV-101']['title']);
        self::assertSame('149,00 RON', $rows['PV-100']['price']);
    }

    public function testACsvWithoutTheRequiredColumnsIsInvalid(): void
    {
        $this->expectException(InvalidFeedException::class);
        $this->expectExceptionMessage('Coloanele necesare lipsesc din antetul CSV: availability');

        (new CsvFormat(',', true, 'id', ['availability']))->parse("id,title,stock\nPV-100,Paravant,1\n");
    }

    public function testPtcRowsNeedFourColumnsWithNumericQuantityAndListPrice(): void
    {
        $csv = "193261GRD,1,100.5,120,Carlig\r\nTM402805,0,80,95.90,Covorase\r\nBAD1,n/a,1,2,x\r\nSHORT,1\r\n";

        $parsed = (new CsvFormat(',', false, 0, [3], [1, 3], [], 1.0))->parse($csv);

        self::assertSame(['193261GRD', 'TM402805'], array_keys($parsed->getRows()));
        self::assertSame(['193261GRD', '1', '100.5', '120', 'Carlig'], $parsed->getRows()['193261GRD']);
        self::assertSame(2, $parsed->getSkipped());
    }

    public function testAnErrorPageInsteadOfTheCsvIsInvalidAndQuotedInTheMessage(): void
    {
        try {
            (new CsvFormat(',', false, 0, [3], [1, 3]))->parse(self::ERROR_PAGE);
            self::fail('expected an invalid feed');
        } catch (InvalidFeedException $e) {
            self::assertStringContainsString('Niciun rand nu are formatul asteptat', $e->getMessage());
            self::assertStringContainsString('503 Service Unavailable', $e->getMessage());
        }
    }

    public function testAlfaromtransXmlRowsHoldTheTrimmedFields(): void
    {
        $xml = '<?xml version="1.0"?><items>'
            . '<item><sku> AR-1 </sku><Stoc> InStock </Stoc><name>x</name></item>'
            . '<item><sku></sku><Stoc>instock</Stoc></item>'
            . '<item><sku>AR-2</sku><Stoc>outofstock</Stoc></item></items>';

        $parsed = (new XmlFormat('item', 'sku', ['Stoc'], [], [], 1.0))->parse($xml);

        self::assertSame([
            'AR-1' => ['sku' => 'AR-1', 'Stoc' => 'InStock'],
            'AR-2' => ['sku' => 'AR-2', 'Stoc' => 'outofstock'],
        ], $parsed->getRows());
        self::assertSame(1, $parsed->getSkipped());
    }

    public function testAFeedWithTooManyRejectedRowsIsNotUsedAtAll(): void
    {
        $rows = '';
        for ($i = 1; $i <= 18; $i++) {
            $rows .= "SKU{$i},{$i},10,12,Produs\n";
        }
        $rows .= "BAD1,n/a,10,12,x\nBAD2,,10,12,y\n";   // 2 of 20 = 10%: still used
        self::assertCount(18, (new CsvFormat(',', false, 0, [3], [1, 3]))->parse($rows)->getRows());

        $this->expectException(InvalidFeedException::class);
        $this->expectExceptionMessage('3 din 21 randuri (14%) nu au formatul asteptat, peste limita de 10%');
        (new CsvFormat(',', false, 0, [3], [1, 3]))->parse($rows . "BAD3,?,10,12,z\n");
    }

    public function testACarsupremePriceThatIsNotANumberIsRejectedNotReadAsZero(): void
    {
        $format = new CsvFormat(',', true, 'id', ['price'], [], ['price' => '/^\s*\d+(?:[.,]\d{1,2})?\s*(?:RON|LEI)?\s*$/i'], 1.0);
        $parsed = $format->parse("id,price\nA,149.00\nB,\"149,90 RON\"\nC,n/a\nD,\nE,\"1,234.56\"\nF,99\n");

        self::assertSame(['A', 'B', 'F'], array_keys($parsed->getRows()));
        self::assertSame(3, $parsed->getSkipped());
    }

    public function testAnAlfaromtransProductWithoutAKnownStockValueIsRejected(): void
    {
        $xml = '<?xml version="1.0"?><items>'
            . '<item><sku>AR-1</sku><Stoc>instock</Stoc></item>'
            . '<item><sku>AR-2</sku><Stoc>OutOfStock</Stoc></item>'
            . '<item><sku>AR-3</sku></item>'
            . '<item><sku>AR-4</sku><Stoc>preorder</Stoc></item></items>';

        $parsed = (new XmlFormat('item', 'sku', ['Stoc'], ['Stoc'], ['Stoc' => ['instock', 'outofstock']], 1.0))->parse($xml);

        self::assertSame(['AR-1', 'AR-2'], array_keys($parsed->getRows()));
        self::assertSame(2, $parsed->getSkipped());

        $this->expectException(InvalidFeedException::class);   // 2 of 4 rejected: the feed is not used
        (new XmlFormat('item', 'sku', ['Stoc'], ['Stoc'], ['Stoc' => ['instock', 'outofstock']]))->parse($xml);
    }

    public function testAnErrorPageInsteadOfTheXmlIsInvalid(): void
    {
        $this->expectException(InvalidFeedException::class);
        $this->expectExceptionMessage('Raspunsul nu este XML valid');

        (new XmlFormat('item', 'sku', ['Stoc']))->parse('Fatal error: database unavailable <br>');
    }

    public function testAnXmlWithoutProductElementsIsInvalid(): void
    {
        $this->expectException(InvalidFeedException::class);
        $this->expectExceptionMessage('nu contine elemente <item>');

        (new XmlFormat('item', 'sku'))->parse('<?xml version="1.0"?><feed><product><sku>A</sku></product></feed>');
    }

    public function testTheReaderTurnsTheOutcomesIntoResults(): void
    {
        $fetcher = $this->createMock(FeedFetcher::class);
        $fetcher->method('fetch')->willReturnOnConsecutiveCalls(
            FetchResult::failed('https://x.ro/f?token=abc', Problem::FETCH, 'Eroare conexiune: timed out', 0, 3),
            FetchResult::ok('https://x.ro/f', self::ERROR_PAGE, 200, 1),
            FetchResult::ok('https://x.ro/f', "id,availability\nA,in stock\n", 200, 2)
        );
        $reader = new FeedReader($fetcher);
        $format = new CsvFormat(',', true, 'id', ['availability']);

        $failed = $reader->read('https://x.ro/f?token=abc', $format);
        self::assertSame(Problem::FETCH, $failed->getProblem());
        self::assertStringContainsString('(x.ro, 3 incercari)', $failed->getMessage());
        self::assertStringNotContainsString('token', $failed->getMessage());
        self::assertSame(3, $failed->getAttempts());

        self::assertSame(Problem::INVALID, $reader->read('https://x.ro/f', $format)->getProblem());

        $ok = $reader->read('https://x.ro/f', $format);
        self::assertTrue($ok->isOk());
        self::assertSame(1, $ok->getCount());
        self::assertSame(Problem::EMPTY, $reader->parse(" \n", $format)->getProblem());
    }
}
