<?php

declare(strict_types=1);

namespace ShlinkioTest\Shlink\CLI\Command\Visit;

use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\Visit\DownloadGeoLiteDbCommand;
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface;
use Shlinkio\Shlink\CLI\GeoLite\GeolocationResult;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
use Symfony\Component\Console\Tester\CommandTester;

use function sprintf;

class DownloadGeoLiteDbCommandTest extends TestCase
{
    use CliTestUtilsTrait;

    private CommandTester $commandTester;
    private MockObject $dbUpdater;

    protected function setUp(): void
    {
        $this->dbUpdater = $this->createMock(GeolocationDbUpdaterInterface::class);
        $this->commandTester = $this->testerForCommand(new DownloadGeoLiteDbCommand($this->dbUpdater));
    }

    /**
     * @test
     * @dataProvider provideFailureParams
     */
    public function showsProperMessageWhenGeoLiteUpdateFails(
        bool $olderDbExists,
        string $expectedMessage,
        int $expectedExitCode,
    ): void {
        $this->dbUpdater->expects($this->once())->method('checkDbUpdate')->withAnyParameters()->willReturnCallback(
            function (callable $beforeDownload, callable $handleProgress) use ($olderDbExists): void {
                $beforeDownload($olderDbExists);
                $handleProgress(100, 50);

                throw $olderDbExists
                    ? GeolocationDbUpdateFailedException::withOlderDb()
                    : GeolocationDbUpdateFailedException::withoutOlderDb();
            },
        );

        $this->commandTester->execute([]);
        $output = $this->commandTester->getDisplay();
        $exitCode = $this->commandTester->getStatusCode();

        self::assertStringContainsString(
            sprintf('%s GeoLite2 db file...', $olderDbExists ? 'Updating' : 'Downloading'),
            $output,
        );
        self::assertStringContainsString($expectedMessage, $output);
        self::assertSame($expectedExitCode, $exitCode);
    }

    public function provideFailureParams(): iterable
    {
        yield 'existing db' => [
            true,
            '[WARNING] GeoLite2 db file update failed. Visits will continue to be located',
            ExitCodes::EXIT_WARNING,
        ];
        yield 'not existing db' => [
            false,
            '[ERROR] GeoLite2 db file download failed. It will not be possible to locate',
            ExitCodes::EXIT_FAILURE,
        ];
    }

    /**
     * @test
     * @dataProvider provideSuccessParams
     */
    public function printsExpectedMessageWhenNoErrorOccurs(callable $checkUpdateBehavior, string $expectedMessage): void
    {
        $this->dbUpdater->expects($this->once())->method('checkDbUpdate')->withAnyParameters()->willReturnCallback(
            $checkUpdateBehavior,
        );

        $this->commandTester->execute([]);
        $output = $this->commandTester->getDisplay();
        $exitCode = $this->commandTester->getStatusCode();

        self::assertStringContainsString($expectedMessage, $output);
        self::assertSame(ExitCodes::EXIT_SUCCESS, $exitCode);
    }

    public function provideSuccessParams(): iterable
    {
        yield 'up to date db' => [fn () => GeolocationResult::CHECK_SKIPPED, '[INFO] GeoLite2 db file is up to date.'];
        yield 'outdated db' => [function (callable $beforeDownload): GeolocationResult {
            $beforeDownload(true);
            return GeolocationResult::DB_CREATED;
        }, '[OK] GeoLite2 db file properly downloaded.'];
    }
}