<?php

declare(strict_types=1);

namespace Apexl\Utils\Tests\Arrays;

use Apexl\Utils\Arrays\Merge;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class MergeArrayByWeightTest extends TestCase
{
    /**
     * @param array<int, array{0: int, 1: string}> $items
     * @return array<int, string>
     */
    private function build(array $items): array
    {
        $out = [];
        foreach ($items as [$weight, $value]) {
            Merge::arrayByWeight($weight, $value, $out);
        }

        return $out;
    }

    #[Test]
    public function distinctWeightsAreKeyedByWeightAndSortedAscending(): void
    {
        $result = $this->build([
            [5, 'five'],
            [-3, 'minusThree'],
            [100, 'hundred'],
            [0, 'zero'],
            [42, 'fortyTwo'],
        ]);

        $this->assertSame(
            [-3 => 'minusThree', 0 => 'zero', 5 => 'five', 42 => 'fortyTwo', 100 => 'hundred'],
            $result
        );
        $this->assertSame([-3, 0, 5, 42, 100], array_keys($result));
    }

    #[Test]
    public function tiedWeightsStackInRegistrationOrder(): void
    {
        $result = $this->build([
            [10, 'first'],
            [10, 'second'],
            [10, 'third'],
        ]);

        $this->assertSame([10 => 'first', 11 => 'second', 12 => 'third'], $result);
        $this->assertSame(['first', 'second', 'third'], array_values($result));
    }

    /**
     * The shape that hid four Town Centre admin surfaces: a group of ties whose
     * neighbouring slots are already taken, plus a unique weight just above the
     * packed range. Every entry must survive.
     */
    #[Test]
    public function packedGroupWithSevenTiesKeepsEveryEntry(): void
    {
        $result = $this->build([
            [9, 'crm'],
            [10, 'projects'],
            [11, 'finance'],
            [9, 'hr'],
            [9, 'quoting'],
            [9, 'procurement'],
            [9, 'contentLibrary'],
            [22, 'branding'],
        ]);

        $this->assertCount(8, $result);
        $this->assertSame(
            ['crm', 'projects', 'finance', 'hr', 'quoting', 'procurement', 'contentLibrary', 'branding'],
            array_values($result)
        );
        // The adjacent unique weight keeps its own slot rather than being swallowed.
        $this->assertSame('branding', $result[22]);
    }

    #[Test]
    public function sevenTiesOnOneWeightAllSurviveAlongsideAnAdjacentUniqueWeight(): void
    {
        $result = $this->build([
            [9, 'a'], [9, 'b'], [9, 'c'], [9, 'd'], [9, 'e'], [9, 'f'], [9, 'g'],
            [22, 'adjacent'],
        ]);

        $this->assertCount(8, $result);
        $this->assertSame(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'adjacent'], array_values($result));
        $this->assertSame('adjacent', $result[22]);
    }

    #[Test]
    public function existingEntriesAreNeverOverwritten(): void
    {
        $items = [];
        foreach (range(1, 30) as $i) {
            // Half the entries collide on weight 5, the rest fill the range around it.
            $items[] = [$i % 2 === 0 ? 5 : $i, "item$i"];
        }

        $result = $this->build($items);

        $this->assertCount(30, $result);
        $this->assertSame(30, count(array_unique(array_values($result))));
    }

    /**
     * The old implementation kept its conflict ledger in a static property that was
     * never reset, so building the same menu twice in one process gave two different
     * answers. Repeated invocations must now be independent.
     */
    #[Test]
    public function repeatedInvocationsAreIndependent(): void
    {
        $items = [
            [9, 'crm'],
            [10, 'projects'],
            [9, 'hr'],
            [9, 'quoting'],
            [22, 'branding'],
        ];

        $first  = $this->build($items);
        $second = $this->build($items);
        $third  = $this->build($items);

        $this->assertSame($first, $second);
        $this->assertSame($first, $third);
    }

    #[Test]
    public function unrelatedBuildsDoNotLeakIntoEachOther(): void
    {
        // A noisy build full of collisions must not shift a later, unrelated build.
        $expected = $this->build([[1, 'a'], [2, 'b'], [3, 'c']]);

        $this->build(array_map(static fn(int $i): array => [7, "noise$i"], range(1, 50)));

        $this->assertSame($expected, $this->build([[1, 'a'], [2, 'b'], [3, 'c']]));
    }

    #[Test]
    public function negativeWeightsStackTheSameWayAsPositiveOnes(): void
    {
        $result = $this->build([
            [-5, 'first'],
            [-5, 'second'],
            [-4, 'wasFree'],
            [-5, 'third'],
        ]);

        $this->assertCount(4, $result);
        $this->assertSame([-5 => 'first', -4 => 'second', -3 => 'wasFree', -2 => 'third'], $result);
    }

    #[Test]
    public function resetIsRetainedAndHarmless(): void
    {
        $expected = $this->build([[1, 'a'], [1, 'b']]);

        Merge::reset();

        $this->assertSame($expected, $this->build([[1, 'a'], [1, 'b']]));
    }
}
