<?php

declare(strict_types=1);

/*
 * This file is part of the Ferienpass package.
 *
 * (c) Richard Henkenjohann <richard@ferienpass.online>
 *
 * For more information visit the project website <https://ferienpass.online>
 * or the documentation under <https://docs.ferienpass.online>.
 */

namespace Ferienpass\AdminBundle\Tests\Components;

use Ferienpass\AdminBundle\Components\SearchableQueryableList;
use Ferienpass\AdminBundle\LiveComponent\MultiSelect;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

final class SearchableQueryableListTest extends TestCase
{
    /**
     * The header checkbox cycles preferred → all → none, always scoped to the
     * rows in view: `preferred` is built from the unfiltered set, so ids
     * outside the current filter/search/page must not leak into the selection.
     *
     * @param list<int> $preferred
     * @param list<int> $ids
     * @param list<int> $selected
     * @param list<int> $expected
     */
    #[DataProvider('selectionProvider')]
    public function testCycleSelection(array $preferred, array $ids, array $selected, array $expected, bool $expectedAll): void
    {
        $component = new \ReflectionClass(SearchableQueryableList::class)->newInstanceWithoutConstructor();
        $component->ms = new MultiSelect([], 'handler', $preferred);
        $component->selected = $selected;

        new \ReflectionMethod(SearchableQueryableList::class, 'cycleSelection')->invoke($component, $ids);

        self::assertSame($expected, $component->selected);
        self::assertSame($expectedAll, $component->selectedAll);
    }

    public static function selectionProvider(): iterable
    {
        yield 'no preferred: selects everything at once' => [[], [1, 2, 3], [], [1, 2, 3], true];
        yield 'preferred first' => [[2], [1, 2, 3], [], [2], false];
        yield 'preferred then all' => [[2], [1, 2, 3], [2], [1, 2, 3], true];
        yield 'all then none' => [[2], [1, 2, 3], [1, 2, 3], [], false];

        // The regression: none of the preferred ids are on screen.
        yield 'preferred out of view: selects everything at once' => [[7, 8], [1, 2, 3], [], [1, 2, 3], true];
        yield 'preferred partly out of view' => [[2, 7], [1, 2, 3], [], [2], false];

        // A leftover selection from a wider view must not be counted as "all
        // selected", which would collapse the cycle to a no-op.
        yield 'stale selection out of view' => [[], [1, 2], [7, 8, 9], [1, 2], true];

        yield 'preferred covers the whole view' => [[1, 2], [1, 2], [], [1, 2], true];
        yield 'checkbox values arrive as strings' => [[2], [1, 2, 3], ['2'], [1, 2, 3], true];
    }

    /**
     * Malformed UTF-8 anywhere in the query string must be rejected before it
     * reaches the LiveProp dehydration (json_encode with JSON_THROW_ON_ERROR),
     * which would otherwise raise an uncaught 500 at render time.
     *
     * @param array<array-key, mixed> $query
     */
    #[DataProvider('queryProvider')]
    public function testIsValidUtf8(bool $expected, array $query): void
    {
        self::assertSame($expected, $this->invokeIsValidUtf8($query));
    }

    public static function queryProvider(): iterable
    {
        $malformed = "\xc3\x28"; // invalid 2-byte sequence

        yield 'empty' => [true, []];
        yield 'plain values' => [true, ['q' => 'Sömmer', 'page' => '2']];
        yield 'nested filter ok' => [true, ['filter' => ['name' => 'Ärger', 'age' => ['12', '13']]]];
        yield 'numeric values' => [true, ['page' => 1, 'flags' => [true, null]]];

        yield 'malformed top-level value' => [false, ['q' => $malformed]];
        yield 'malformed nested value' => [false, ['filter' => ['name' => $malformed]]];
        yield 'malformed deep value' => [false, ['filter' => ['ids' => ['ok', $malformed]]]];
        yield 'malformed key' => [false, [$malformed => 'value']];
        yield 'malformed nested key' => [false, ['filter' => [$malformed => 'value']]];
    }

    private function invokeIsValidUtf8(array $query): bool
    {
        $method = new \ReflectionMethod(SearchableQueryableList::class, 'isValidUtf8');

        return $method->invoke(null, $query);
    }
}
