<?php

declare(strict_types=1);

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

use Autogedal\FeedMonitor\Model\Config;
use Autogedal\FeedMonitor\Model\Fetch\FeedFetcher;
use Autogedal\FeedMonitor\Model\Fetch\FetchResult;
use Autogedal\FeedMonitor\Model\Fetch\HttpClient;
use Autogedal\FeedMonitor\Model\Fetch\SimulatedHttpClient;
use Autogedal\FeedMonitor\Model\Problem;
use Autogedal\FeedMonitor\Test\Unit\Model\FixedClock;
use PHPUnit\Framework\TestCase;

class FeedFetcherTest extends TestCase
{
    private const URL = 'https://feed.example/products.csv?access_token=SECRET&x=1';

    public function testATimeoutIsRetriedAndTheContentOfALaterAttemptIsReturned(): void
    {
        $fetcher = $this->fetcher([
            $this->response(false, 0, 'Failed to connect after 20001 ms: Connection timed out', 28),
            $this->response("sku,qty\nA,1\n", 200),
        ], $sleeps);

        $result = $fetcher->fetch(self::URL);

        self::assertTrue($result->isOk());
        self::assertSame("sku,qty\nA,1\n", $result->getBody());
        self::assertSame(2, $result->getAttempts());
        self::assertSame([10], $sleeps);
    }

    public function testAfterTheLastAttemptTheConnectionErrorIsReportedWithTheHostOnly(): void
    {
        $timeout = $this->response(false, 0, 'Connection timed out', 28);
        $result = $this->fetcher([$timeout, $timeout, $timeout], $sleeps)->fetch(self::URL);

        self::assertFalse($result->isOk());
        self::assertSame(Problem::FETCH, $result->getProblem());
        self::assertSame(3, $result->getAttempts());
        self::assertSame([10, 20], $sleeps);
        self::assertStringContainsString('Connection timed out', $result->getErrorMessage());
        self::assertStringContainsString('(feed.example, 3 incercari)', $result->getErrorMessage());
        self::assertStringNotContainsString('SECRET', $result->getErrorMessage());
        self::assertStringNotContainsString('products.csv', $result->getErrorMessage());
    }

    public function testAMissingFeedIsNotRetried(): void
    {
        $result = $this->fetcher([$this->response('<html>Not found</html>', 404)], $sleeps)->fetch(self::URL);

        self::assertSame(Problem::FETCH, $result->getProblem());
        self::assertSame(1, $result->getAttempts());
        self::assertSame(404, $result->getHttpCode());
        self::assertSame([], $sleeps);
    }

    public function testAServerErrorIsRetried(): void
    {
        $result = $this->fetcher([
            $this->response('Service Unavailable', 503),
            $this->response('a,b', 200),
        ], $sleeps)->fetch(self::URL);

        self::assertTrue($result->isOk());
        self::assertSame(2, $result->getAttempts());
    }

    public function testAnEmptyAnswerIsReportedAsEmptyWithoutRetry(): void
    {
        $result = $this->fetcher([$this->response("  \n", 200)], $sleeps)->fetch(self::URL);

        self::assertSame(Problem::EMPTY, $result->getProblem());
        self::assertSame(1, $result->getAttempts());
    }

    public function testTheWholeReadStopsAtTheTotalLimitAndEachAttemptGetsOnlyTheTimeLeft(): void
    {
        $clock = new FixedClock();
        $timeouts = [];
        $client = $this->createMock(HttpClient::class);
        // a host that accepts the connection and never answers: every attempt uses all the time it is given
        $client->method('get')->willReturnCallback(function (string $url, int $connect, int $timeout) use ($clock, &$timeouts): array {
            $timeouts[] = $timeout;
            $clock->time += $timeout;
            return $this->response(false, 0, 'Operation timed out', 28);
        });
        $client->method('sleep')->willReturnCallback(function (int $seconds) use ($clock): void {
            $clock->time += $seconds;
        });
        $config = $this->createMock(Config::class);
        $config->method('getAttempts')->willReturn(3);
        $config->method('getRetryDelay')->willReturn(10);
        $config->method('getConnectTimeout')->willReturn(20);
        $config->method('getTimeout')->willReturn(120);
        $config->method('getMaxTotalSeconds')->willReturn(300);

        $start = $clock->now();
        $result = (new FeedFetcher($client, $config, $clock))->fetch(self::URL);

        self::assertSame([120, 120, 30], $timeouts);   // 120 + 10 pause + 120 + 20 pause leaves 30
        self::assertLessThanOrEqual(300, $clock->now() - $start);
        self::assertSame(Problem::FETCH, $result->getProblem());
    }

    public function testNoAttemptStartsOnceTheTotalLimitIsReached(): void
    {
        $clock = new FixedClock();
        $client = $this->createMock(HttpClient::class);
        $client->expects(self::once())->method('get')->willReturnCallback(function () use ($clock): array {
            $clock->time += 300;
            return $this->response(false, 0, 'Operation timed out', 28);
        });
        $config = $this->createMock(Config::class);
        $config->method('getAttempts')->willReturn(3);
        $config->method('getRetryDelay')->willReturn(10);
        $config->method('getConnectTimeout')->willReturn(20);
        $config->method('getTimeout')->willReturn(300);
        $config->method('getMaxTotalSeconds')->willReturn(300);

        $result = (new FeedFetcher($client, $config, $clock))->fetch(self::URL);

        self::assertStringContainsString('oprit dupa limita totala de 300 s', $result->getErrorMessage());
        self::assertSame(1, $result->getAttempts());
    }

    public function testOnlyTheHostOfASecretFeedPathIsShown(): void
    {
        self::assertSame('www.suportbicicleta.ro', FetchResult::hostOf('https://www.suportbicicleta.ro/feed/products/061ca76d790a'));
    }

    public function testTheSimulatedClientOfTheRunCommandAnswersWithoutNetwork(): void
    {
        $client = new SimulatedHttpClient();
        $client->setResponse('<html>503</html>', 503);
        self::assertSame(['body' => '<html>503</html>', 'http_code' => 503, 'error' => '', 'errno' => 0], $client->get(self::URL, 1, 1));

        $client->setResponse(null);
        $timeout = $client->get(self::URL, 1, 1);
        self::assertFalse($timeout['body']);
        self::assertSame(28, $timeout['errno']);
        self::assertSame(2, $client->getSimulatedRequests());
    }

    /**
     * @param array<int, array<string, mixed>> $responses
     */
    private function fetcher(array $responses, ?array &$sleeps): FeedFetcher
    {
        $sleeps = [];
        $client = $this->createMock(HttpClient::class);
        $client->method('get')->willReturnOnConsecutiveCalls(...$responses);
        $client->method('sleep')->willReturnCallback(function (int $seconds) use (&$sleeps): void {
            $sleeps[] = $seconds;
        });
        $config = $this->createMock(Config::class);
        $config->method('getAttempts')->willReturn(3);
        $config->method('getRetryDelay')->willReturn(10);
        $config->method('getConnectTimeout')->willReturn(20);
        $config->method('getTimeout')->willReturn(300);
        $config->method('getMaxTotalSeconds')->willReturn(3600);

        return new FeedFetcher($client, $config, new FixedClock());
    }

    private function response(string|false $body, int $code, string $error = '', int $errno = 0): array
    {
        return ['body' => $body, 'http_code' => $code, 'error' => $error, 'errno' => $errno];
    }
}
