<?php

declare(strict_types=1);

namespace App\Tests\Unit\Shared\Infrastructure\Console;

use App\Shared\Domain\Clock;
use App\Shared\Infrastructure\Console\ArchiveAuditLogsCommand;
use DateTimeImmutable;
use Doctrine\DBAL\Connection;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Tester\CommandTester;

/**
 * @see Story 1.7 - T8: Archivage
 */
final class ArchiveAuditLogsCommandTest extends TestCase
{
    private Connection&MockObject $connection;
    private Clock&MockObject $clock;
    private ArchiveAuditLogsCommand $command;
    private CommandTester $commandTester;

    protected function setUp(): void
    {
        $this->connection = $this->createMock(Connection::class);
        $this->clock = $this->createMock(Clock::class);

        $this->command = new ArchiveAuditLogsCommand(
            $this->connection,
            $this->clock,
        );

        $this->commandTester = new CommandTester($this->command);
    }

    public function testCommandNameIsCorrect(): void
    {
        $this->assertSame('app:audit:archive', $this->command->getName());
    }

    public function testCommandDescription(): void
    {
        $this->assertSame(
            'Archive audit log entries older than 5 years',
            $this->command->getDescription(),
        );
    }

    public function testNoEntriesToArchiveReturnsSuccess(): void
    {
        $now = new DateTimeImmutable('2026-02-03 10:00:00');
        $this->clock->method('now')->willReturn($now);

        $this->connection->method('fetchOne')
            ->willReturnOnConsecutiveCalls(0); // COUNT returns 0

        $this->commandTester->execute([]);

        $output = $this->commandTester->getDisplay();
        $this->assertStringContainsString('No entries to archive', $output);
        $this->assertSame(Command::SUCCESS, $this->commandTester->getStatusCode());
    }

    public function testDryRunDoesNotCallArchiveFunction(): void
    {
        $now = new DateTimeImmutable('2026-02-03 10:00:00');
        $this->clock->method('now')->willReturn($now);

        $this->connection->expects($this->once())
            ->method('fetchOne')
            ->willReturn(100); // 100 entries to archive

        // archive_audit_entries should NOT be called in dry-run mode
        $this->connection->expects($this->never())
            ->method('executeStatement');

        $this->commandTester->execute(['--dry-run' => true]);

        $output = $this->commandTester->getDisplay();
        $this->assertStringContainsString('DRY RUN', $output);
        $this->assertStringContainsString('Would archive 100 entries', $output);
        $this->assertSame(Command::SUCCESS, $this->commandTester->getStatusCode());
    }

    public function testArchivesBatchesUntilComplete(): void
    {
        $now = new DateTimeImmutable('2026-02-03 10:00:00');
        $this->clock->method('now')->willReturn($now);

        // First call: COUNT returns 150
        // Subsequent calls: archive_audit_entries returns batch counts
        $this->connection->method('fetchOne')
            ->willReturnOnConsecutiveCalls(
                150,    // COUNT query
                100,    // First batch (full)
                50,     // Second batch (partial, stops)
            );

        $this->commandTester->execute(['--batch-size' => '100']);

        $output = $this->commandTester->getDisplay();
        $this->assertStringContainsString('Successfully archived 150', $output);
        $this->assertSame(Command::SUCCESS, $this->commandTester->getStatusCode());
    }

    public function testCustomRetentionYears(): void
    {
        $now = new DateTimeImmutable('2026-02-03 10:00:00');
        $this->clock->method('now')->willReturn($now);

        $this->connection->method('fetchOne')->willReturn(0);

        $this->commandTester->execute(['--retention-years' => '3']);

        $output = $this->commandTester->getDisplay();
        $this->assertStringContainsString('(3 years retention)', $output);
    }

    public function testCustomBatchSize(): void
    {
        $now = new DateTimeImmutable('2026-02-03 10:00:00');
        $this->clock->method('now')->willReturn($now);

        // Return 500 entries to archive, then archive in 500-entry batches
        $this->connection->method('fetchOne')
            ->willReturnOnConsecutiveCalls(
                500,    // COUNT
                500,    // First batch (equal to batch size)
                0,      // Second batch (none left)
            );

        $this->commandTester->execute(['--batch-size' => '500']);

        $output = $this->commandTester->getDisplay();
        $this->assertStringContainsString('Successfully archived 500', $output);
    }

    public function testShowsProgressBar(): void
    {
        $now = new DateTimeImmutable('2026-02-03 10:00:00');
        $this->clock->method('now')->willReturn($now);

        $this->connection->method('fetchOne')
            ->willReturnOnConsecutiveCalls(
                50,     // COUNT
                50,     // First batch
            );

        $this->commandTester->execute([]);

        // Progress bar output includes percentage
        $output = $this->commandTester->getDisplay();
        $this->assertStringContainsString('Successfully archived 50', $output);
    }

    public function testCalculatesCutoffDateCorrectly(): void
    {
        $now = new DateTimeImmutable('2026-02-03 10:00:00');
        $this->clock->method('now')->willReturn($now);

        $capturedCutoff = null;
        $this->connection->method('fetchOne')
            ->willReturnCallback(static function (string $sql, array $params) use (&$capturedCutoff) {
                if (str_contains($sql, 'COUNT')) {
                    $capturedCutoff = $params['cutoff'];

                    return 0;
                }

                return 0;
            });

        $this->commandTester->execute(['--retention-years' => '5']);

        // Cutoff should be 5 years before now (2021-02-03)
        $this->assertNotNull($capturedCutoff);
        $this->assertStringContainsString('2021-02-03', $capturedCutoff);
    }

    public function testZeroBatchSizeReturnsFailure(): void
    {
        $now = new DateTimeImmutable('2026-02-03 10:00:00');
        $this->clock->method('now')->willReturn($now);

        $this->commandTester->execute(['--batch-size' => '0']);

        $output = $this->commandTester->getDisplay();
        $this->assertStringContainsString('Batch size must be a positive integer', $output);
        $this->assertSame(Command::FAILURE, $this->commandTester->getStatusCode());
    }

    public function testNegativeBatchSizeReturnsFailure(): void
    {
        $now = new DateTimeImmutable('2026-02-03 10:00:00');
        $this->clock->method('now')->willReturn($now);

        $this->commandTester->execute(['--batch-size' => '-5']);

        $output = $this->commandTester->getDisplay();
        $this->assertStringContainsString('Batch size must be a positive integer', $output);
        $this->assertSame(Command::FAILURE, $this->commandTester->getStatusCode());
    }

    public function testZeroRetentionYearsReturnsFailure(): void
    {
        $this->commandTester->execute(['--retention-years' => '0']);

        $output = $this->commandTester->getDisplay();
        $this->assertStringContainsString('Retention years must be a positive integer', $output);
        $this->assertSame(Command::FAILURE, $this->commandTester->getStatusCode());
    }

    public function testNegativeRetentionYearsReturnsFailure(): void
    {
        $this->commandTester->execute(['--retention-years' => '-5']);

        $output = $this->commandTester->getDisplay();
        $this->assertStringContainsString('Retention years must be a positive integer', $output);
        $this->assertSame(Command::FAILURE, $this->commandTester->getStatusCode());
    }
}
