<?php

declare(strict_types=1);

namespace App\Tests\Unit\Enrichment;

use App\Infrastructure\Enrichment\Downloaders\DownloaderException;
use App\Infrastructure\Enrichment\Downloaders\MaxMindDownloader;
use PharData;
use PHPUnit\Framework\TestCase;

/**
 * SEC_REVIEW F48 — `MaxMindDownloader::assertSizeBudget` walks the
 * tarball before extraction and rejects per-entry / total-size limit
 * breaches so a small `.tar.gz` cannot decompress into multi-GB and
 * exhaust disk before MmdbVerifier sees the file.
 */
final class MaxMindDownloaderTest extends TestCase
{
    private string $tmpDir = '';

    protected function setUp(): void
    {
        $this->tmpDir = sys_get_temp_dir() . '/irdb-maxmind-' . bin2hex(random_bytes(6));
        mkdir($this->tmpDir);
    }

    protected function tearDown(): void
    {
        // Clean up the tar file and any generated companions.
        if ($this->tmpDir !== '' && is_dir($this->tmpDir)) {
            foreach (glob($this->tmpDir . '/*') ?: [] as $f) {
                @unlink($f);
            }
            @rmdir($this->tmpDir);
        }
    }

    public function testNormalArchivePasses(): void
    {
        $phar = $this->buildTar([
            'GeoLite2-Country.mmdb' => 'pretend-mmdb-bytes',
            'COPYRIGHT.txt' => 'CC BY-SA 4.0',
        ]);

        // Default caps (200 MiB / 400 MiB) — small fixtures pass cleanly.
        MaxMindDownloader::assertSizeBudget($phar, 'country');
        $this->expectNotToPerformAssertions();
    }

    public function testEntryOverPerEntryCapIsRejected(): void
    {
        $phar = $this->buildTar([
            'big.bin' => str_repeat('A', 4096),
        ]);

        $this->expectException(DownloaderException::class);
        $this->expectExceptionMessage('uncompressed size 4096 > cap 1024');
        // Force the per-entry cap below the entry size.
        MaxMindDownloader::assertSizeBudget($phar, 'country', maxEntryBytes: 1024);
    }

    public function testTotalOverArchiveCapIsRejected(): void
    {
        // Three 1 KiB entries individually pass per-entry; the total
        // breaches the archive cap at the third entry.
        $phar = $this->buildTar([
            'a.bin' => str_repeat('a', 1024),
            'b.bin' => str_repeat('b', 1024),
            'c.bin' => str_repeat('c', 1024),
        ]);

        $this->expectException(DownloaderException::class);
        $this->expectExceptionMessage('archive uncompressed size > cap 2048');
        MaxMindDownloader::assertSizeBudget(
            $phar,
            'country',
            maxEntryBytes: 4096,
            maxTotalBytes: 2048,
        );
    }

    public function testNestedEntriesAreCounted(): void
    {
        // The real MaxMind tarball nests one directory like
        // `GeoLite2-Country_20260427/GeoLite2-Country.mmdb`. Recursive
        // iteration must walk into the directory and count nested
        // entries against both caps.
        $phar = $this->buildTar([
            'GeoLite2-Country_20260427/GeoLite2-Country.mmdb' => str_repeat('m', 2048),
            'GeoLite2-Country_20260427/COPYRIGHT.txt' => 'CC',
        ]);

        $this->expectException(DownloaderException::class);
        $this->expectExceptionMessage('GeoLite2-Country.mmdb uncompressed size 2048 > cap 1024');
        MaxMindDownloader::assertSizeBudget(
            $phar,
            'country',
            maxEntryBytes: 1024,
        );
    }

    /**
     * @param array<string, string> $entries map of entry path → contents
     */
    private function buildTar(array $entries): PharData
    {
        $tarPath = $this->tmpDir . '/' . bin2hex(random_bytes(4)) . '.tar';
        $writer = new PharData($tarPath);
        foreach ($entries as $name => $contents) {
            $writer->addFromString($name, $contents);
        }
        // PharData iteration on the *write* handle returns nothing until
        // the file is closed and re-opened. Production builds the
        // PharData from an already-on-disk tarball, so this matches the
        // production path.
        unset($writer);

        return new PharData($tarPath);
    }
}
