<?php

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;

uses(RefreshDatabase::class);

/** @var array<int, string> */
$tempArchives = [];

afterEach(function () use (&$tempArchives): void {
    foreach ($tempArchives as $path) {
        if (is_file($path)) {
            @unlink($path);
        }
    }

    $tempArchives = [];
});

/**
 * @param array<string, string> $entries
 * @param (callable(\ZipArchive,string):void)|null $entryCallback
 */
function makeArchiveUpload(array $entries, array &$tempArchives, ?callable $entryCallback = null): UploadedFile
{
    if (! class_exists(\ZipArchive::class)) {
        test()->markTestSkipped('ZipArchive extension is required.');
    }

    $path = tempnam(sys_get_temp_dir(), 'sb_upload_zip_');
    if ($path === false) {
        throw new \RuntimeException('Failed to allocate temp archive path.');
    }

    $tempArchives[] = $path;

    $zip = new \ZipArchive();
    if ($zip->open($path, \ZipArchive::OVERWRITE | \ZipArchive::CREATE) !== true) {
        throw new \RuntimeException('Failed to create test zip archive.');
    }

    foreach ($entries as $name => $content) {
        $zip->addFromString($name, $content);
        if ($entryCallback !== null) {
            $entryCallback($zip, $name);
        }
    }

    $zip->close();

    return new UploadedFile($path, 'archive.zip', 'application/zip', null, true);
}

it('rejects archive with zip slip path during preload', function () use (&$tempArchives) {
    Storage::fake('local');
    $user = User::factory()->create();

    $archive = makeArchiveUpload([
        '../evil.txt' => 'x',
    ], $tempArchives);

    $screenshot = UploadedFile::fake()->image('screen.jpg', 800, 600);

    $response = $this->actingAs($user)->postJson('/api/uploads/preload', [
        'main' => $archive,
        'screenshots' => [$screenshot],
    ]);

    $response->assertStatus(422)
        ->assertJsonPath('message', 'Archive inspection failed.');

    expect(strtolower((string) $response->json('reason')))->toContain('path');
});

it('rejects archive with symlink during preload', function () use (&$tempArchives) {
    Storage::fake('local');
    $user = User::factory()->create();

    $archive = makeArchiveUpload([
        'safe/readme.txt' => 'ok',
        'safe/link' => 'target',
    ], $tempArchives, function (\ZipArchive $zip, string $entryName): void {
        if ($entryName === 'safe/link') {
            $zip->setExternalAttributesName($entryName, \ZipArchive::OPSYS_UNIX, 0120777 << 16);
        }
    });

    $screenshot = UploadedFile::fake()->image('screen.jpg', 800, 600);

    $response = $this->actingAs($user)->postJson('/api/uploads/preload', [
        'main' => $archive,
        'screenshots' => [$screenshot],
    ]);

    $response->assertStatus(422)
        ->assertJsonPath('message', 'Archive inspection failed.');

    expect(strtolower((string) $response->json('reason')))->toContain('symlink');
});

it('rejects archive with deep nesting during preload', function () use (&$tempArchives) {
    Storage::fake('local');
    $user = User::factory()->create();

    $archive = makeArchiveUpload([
        'a/b/c/d/e/f/file.txt' => 'deep',
    ], $tempArchives);

    $screenshot = UploadedFile::fake()->image('screen.jpg', 800, 600);

    $response = $this->actingAs($user)->postJson('/api/uploads/preload', [
        'main' => $archive,
        'screenshots' => [$screenshot],
    ]);

    $response->assertStatus(422)
        ->assertJsonPath('message', 'Archive inspection failed.');

    expect(strtolower((string) $response->json('reason')))->toContain('depth');
});

it('rejects archive with too many files during preload', function () use (&$tempArchives) {
    Storage::fake('local');
    $user = User::factory()->create();

    $entries = [];
    for ($index = 0; $index < 5001; $index++) {
        $entries['f' . $index . '.txt'] = 'x';
    }

    $archive = makeArchiveUpload($entries, $tempArchives);
    $screenshot = UploadedFile::fake()->image('screen.jpg', 800, 600);

    $response = $this->actingAs($user)->postJson('/api/uploads/preload', [
        'main' => $archive,
        'screenshots' => [$screenshot],
    ]);

    $response->assertStatus(422)
        ->assertJsonPath('message', 'Archive inspection failed.');

    expect((string) $response->json('reason'))->toContain('5000');
});

it('rejects archive with executable inside during preload', function () use (&$tempArchives) {
    Storage::fake('local');
    $user = User::factory()->create();

    $archive = makeArchiveUpload([
        'safe/readme.txt' => 'ok',
        'safe/run.exe' => 'MZ',
    ], $tempArchives);

    $screenshot = UploadedFile::fake()->image('screen.jpg', 800, 600);

    $response = $this->actingAs($user)->postJson('/api/uploads/preload', [
        'main' => $archive,
        'screenshots' => [$screenshot],
    ]);

    $response->assertStatus(422)
        ->assertJsonPath('message', 'Archive inspection failed.');

    expect(strtolower((string) $response->json('reason')))->toContain('blocked');
});

it('rejects archive with zip bomb ratio during preload', function () use (&$tempArchives) {
    Storage::fake('local');
    $user = User::factory()->create();

    $archive = makeArchiveUpload([
        'payload.txt' => str_repeat('A', 6 * 1024 * 1024),
    ], $tempArchives);

    $screenshot = UploadedFile::fake()->image('screen.jpg', 800, 600);

    $response = $this->actingAs($user)->postJson('/api/uploads/preload', [
        'main' => $archive,
        'screenshots' => [$screenshot],
    ]);

    $response->assertStatus(422)
        ->assertJsonPath('message', 'Archive inspection failed.');

    expect(strtolower((string) $response->json('reason')))->toContain('ratio');
});

it('accepts valid archive during preload', function () use (&$tempArchives) {
    Storage::fake('local');
    $user = User::factory()->create();

    $archive = makeArchiveUpload([
        'skins/theme/readme.txt' => 'hello',
        'skins/theme/layout.ini' => 'v=1',
    ], $tempArchives);

    $screenshot = UploadedFile::fake()->image('screen.jpg', 800, 600);

    $response = $this->actingAs($user)->postJson('/api/uploads/preload', [
        'main' => $archive,
        'screenshots' => [$screenshot],
    ]);

    $response->assertOk()->assertJsonStructure([
        'upload_id',
        'status',
        'expires_at',
    ]);
});
