<?php

declare(strict_types=1);

namespace Tests\Feature;

use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use PDO;
use Tests\TestCase;

final class ReleasesNormalizeGuidsTest extends TestCase
{
    private string $databasePath = '';

    public function createApplication()
    {
        $this->databasePath = sys_get_temp_dir().'/nntmux-normalize-guids.sqlite';
        if (file_exists($this->databasePath)) {
            unlink($this->databasePath);
        }
        $pdo = new PDO('sqlite:'.$this->databasePath);
        $pdo->exec('CREATE TABLE settings (name VARCHAR PRIMARY KEY, value TEXT NULL)');
        $pdo->exec("INSERT INTO settings VALUES ('categorizeforeign', '0'), ('catwebdl', '0')");
        $this->setEnvironmentValue('APP_ENV', 'testing');
        $this->setEnvironmentValue('DB_CONNECTION', 'sqlite');
        $this->setEnvironmentValue('DB_DATABASE', $this->databasePath);

        $app = require __DIR__.'/../../bootstrap/app.php';
        $app->make(Kernel::class)->bootstrap();

        return $app;
    }

    protected function setUp(): void
    {
        parent::setUp();

        config([
            'database.default' => 'sqlite',
            'database.connections.sqlite.database' => $this->databasePath,
        ]);
        DB::purge();
        DB::reconnect();
        $this->createSchema();
    }

    protected function tearDown(): void
    {
        DB::disconnect();
        if (file_exists($this->databasePath)) {
            unlink($this->databasePath);
        }
        parent::tearDown();
    }

    public function test_leftguid_is_resynced_in_place(): void
    {
        $guid = '01234567-89ab-cdef-0123-456789abcdef';
        $this->insertRelease(1, $guid, 'f');

        $status = Artisan::call('releases:normalize-guids');

        $this->assertSame(0, $status);
        $this->assertSame('0', DB::table('releases')->where('id', 1)->value('leftguid'));
        $this->assertSame($guid, DB::table('releases')->where('id', 1)->value('guid'));
    }

    public function test_dry_run_reports_without_writing(): void
    {
        $this->insertRelease(1, '01234567-89ab-cdef-0123-456789abcdef', 'f');
        $this->insertRelease(2, str_repeat('a', 41), 'x');

        $status = Artisan::call('releases:normalize-guids', ['--dry-run' => true]);
        $output = Artisan::output();

        $this->assertSame(1, $status);
        $this->assertStringContainsString('Dry run: no rows are modified.', $output);
        $this->assertSame('f', DB::table('releases')->where('id', 1)->value('leftguid'));
        $this->assertSame(str_repeat('a', 41), DB::table('releases')->where('id', 2)->value('guid'));
    }

    public function test_legacy_sha1_guid_fits_the_narrowed_column_and_does_not_block(): void
    {
        $sha1 = '0c5c002220e26542a4c9dae845a58a38b3e7e63a';
        $this->insertRelease(1, $sha1, '0');

        $status = Artisan::call('releases:normalize-guids');
        $output = Artisan::output();

        $this->assertSame(0, $status);
        $this->assertStringContainsString('Release guids are consistent.', $output);
        $this->assertStringContainsString('1 releases still carry a legacy non-UUID guid', $output);
        $this->assertSame($sha1, DB::table('releases')->where('id', 1)->value('guid'));
    }

    public function test_oversized_guid_is_reported_and_never_rewritten(): void
    {
        $tooLong = str_repeat('b', 41);
        $this->insertRelease(1, $tooLong, 'b', ['name' => 'Oversized guid release']);

        $status = Artisan::call('releases:normalize-guids');
        $output = Artisan::output();

        $this->assertSame(1, $status);
        $this->assertStringContainsString('1 releases have a guid that blocks the normalization migration.', $output);
        $this->assertStringContainsString('Oversized guid release', $output);
        $this->assertSame($tooLong, DB::table('releases')->where('id', 1)->value('guid'));
    }

    public function test_non_ascii_guid_blocks_the_migration(): void
    {
        $this->insertRelease(1, 'd41d8cd98f00b204e9800998ecf842ü', 'd', ['name' => 'Non ASCII guid']);

        $status = Artisan::call('releases:normalize-guids');
        $output = Artisan::output();

        $this->assertSame(1, $status);
        $this->assertStringContainsString('1 releases have a guid that blocks', $output);
        $this->assertStringContainsString('Non ASCII guid', $output);
    }

    public function test_case_insensitive_duplicates_report_every_id_but_the_lowest(): void
    {
        $guid = 'abcdef01-2345-6789-abcd-ef0123456789';
        $this->insertRelease(1, $guid, 'a');
        $this->insertRelease(2, strtoupper($guid), 'A');

        $status = Artisan::call('releases:normalize-guids');
        $output = Artisan::output();

        $this->assertSame(1, $status);
        $this->assertStringContainsString('1 releases have a guid that blocks', $output);
        $this->assertSame($guid, DB::table('releases')->where('id', 1)->value('guid'));
        $this->assertSame(strtoupper($guid), DB::table('releases')->where('id', 2)->value('guid'));
    }

    public function test_consistent_guids_report_success(): void
    {
        $this->insertRelease(1, '01234567-89ab-cdef-0123-456789abcdef', '0');

        $status = Artisan::call('releases:normalize-guids');

        $this->assertSame(0, $status);
        $this->assertStringContainsString('Release guids are consistent.', Artisan::output());
    }

    public function test_portable_scans_never_use_offset_paging(): void
    {
        for ($id = 1; $id <= 250; $id++) {
            $this->insertRelease($id, sprintf('%08x-89ab-cdef-0123-456789abcdef', $id), 'x');
        }

        $queries = [];
        DB::listen(static function ($query) use (&$queries): void {
            $queries[] = $query->sql;
        });

        Artisan::call('releases:normalize-guids', ['--chunk' => 100]);

        $this->assertNotEmpty($queries);
        foreach ($queries as $sql) {
            $this->assertStringNotContainsStringIgnoringCase(
                'offset',
                $sql,
                'releases:normalize-guids must page by primary key, never with LIMIT/OFFSET.',
            );
        }
    }

    /** @param array<string, mixed> $overrides */
    private function insertRelease(int $id, string $guid, string $leftguid, array $overrides = []): void
    {
        DB::table('releases')->insert([
            'id' => $id,
            'guid' => $guid,
            'leftguid' => $leftguid,
            'name' => 'Release '.$id,
            ...$overrides,
        ]);
    }

    private function createSchema(): void
    {
        DB::statement('DROP TABLE IF EXISTS releases');
        DB::statement('CREATE TABLE releases (
            id INTEGER PRIMARY KEY,
            guid VARCHAR(40) NOT NULL,
            leftguid CHAR(1) NOT NULL,
            name VARCHAR(255) NOT NULL DEFAULT ""
        )');
    }

    private function setEnvironmentValue(string $key, ?string $value): void
    {
        if ($value === null) {
            putenv($key);
            unset($_ENV[$key], $_SERVER[$key]);

            return;
        }

        putenv("{$key}={$value}");
        $_ENV[$key] = $_SERVER[$key] = $value;
    }
}
