<?php declare(strict_types=1);
/*
 * This file is part of PHPUnit.
 *
 * (c) Sebastian Bergmann <sebastian@phpunit.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
namespace PHPUnit\Framework;

use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Small;
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\TestFixture\ObjectComparison\ChildOfValueObjectA;
use PHPUnit\TestFixture\ObjectComparison\SameStructAsValueObjectA;
use PHPUnit\TestFixture\ObjectComparison\ValueObjectA;
use stdClass;

#[CoversMethod(Assert::class, 'assertArraysAreEqualIgnoringOrder')]
#[TestDox('assertArraysAreEqualIgnoringOrder()')]
#[Small]
#[Group('framework')]
#[Group('framework/assertions')]
final class assertArraysAreEqualIgnoringOrderTest extends TestCase
{
    /**
     * @return non-empty-array<non-empty-string, array{0: array<mixed>, 1: array<mixed>}>
     */
    public static function successProvider(): array
    {
        $object = new stdClass;

        return [
            'empty arrays' => [
                [],
                [],
            ],

            'implicit integer keys' => [
                ['a', 'b', 'c'],
                ['a', 'b', 'c'],
            ],

            'explicit integer keys' => [
                [1 => 'a', 2 => 'b', 4 => 'c'],
                [1 => 'a', 2 => 'b', 4 => 'c'],
            ],

            'explicit negative integer keys' => [
                [-1 => 'a', -2 => 'b'],
                [-1 => 'a', -2 => 'b'],
            ],

            'explicit numeric keys (literal strings, auto-cast to int)' => [
                ['1' => 'a', '2' => 'b', '4' => 'c'],
                ['1' => 'a', '2' => 'b', '4' => 'c'],
            ],

            'different explicit numeric keys (literal strings, auto-cast to int)' => [
                [1 => 'a', 2 => 'b', 4 => 'c'],
                ['1' => 'a', '2' => 'b', '4' => 'c'],
            ],

            'string keys' => [
                ['a' => 1, 'b' => 2, 'c' => 3],
                ['a' => 1, 'b' => 2, 'c' => 3],
            ],

            'nested arrays' => [
                ['a' => ['b' => ['c' => 1]]],
                ['a' => ['b' => ['c' => 1]]],
            ],

            'more deeply nested arrays' => [
                [['a' => [['b' => 'c']]]],
                [['a' => [['b' => 'c']]]],
            ],

            'mixed value types' => [
                [1, 'string', 3.14, true, null],
                [1, 'string', 3.14, true, null],
            ],

            'object references' => [
                ['obj' => $object],
                ['obj' => $object],
            ],

            'null values' => [
                ['a' => null, 'b' => null],
                ['a' => null, 'b' => null],
            ],

            'boolean values' => [
                [true, false, true],
                [true, false, true],
            ],

            'float values' => [
                [1.1, 2.2, 3.3],
                [1.1, 2.2, 3.3],
            ],

            'integer vs float (loose comparison)' => [
                [1],
                [1.0],
            ],

            'true vs 1 (loose comparison)' => [
                [true],
                [1],
            ],

            'false vs 0 (loose comparison)' => [
                [false],
                [0],
            ],

            'different object instances (loose comparison)' => [
                ['obj' => new stdClass],
                ['obj' => new stdClass],
            ],

            'different object instances with strictly equal properties' => [
                ['obj' => new ValueObjectA('value')],
                ['obj' => new ValueObjectA('value')],
            ],

            'different object instances with loosely equal properties' => [
                ['obj' => new ValueObjectA(false)],
                ['obj' => new ValueObjectA(0)],
            ],

            'identical keys, identical values, different order' => [
                [0 => 1, 'a' => 'b', 2 => 'c'],
                ['a' => 'b', 0 => 1, 2 => 'c'],
            ],

            'null vs empty string (loose comparison)' => [
                ['a' => null],
                ['a' => ''],
            ],

            'false vs null (loose comparison)' => [
                [false],
                [null],
            ],

            'mixed scalar and boolean values, different order, same key-value pairs' => [
                ['a' => 'string-a', 'b' => true, 'c' => 'string-c'],
                ['a' => 'string-a', 'c' => 'string-c', 'b' => true],
            ],

            'nested associative arrays whose keys appear in a different order' => [
                ['outer' => ['inner_1' => 'v1', 'inner_2' => 'v2']],
                ['outer' => ['inner_2' => 'v2', 'inner_1' => 'v1']],
            ],

            'deeply nested associative arrays whose keys appear in a different order' => [
                ['l1' => ['l2' => ['l3_1' => 'v1', 'l3_2' => 'v2']]],
                ['l1' => ['l2' => ['l3_2' => 'v2', 'l3_1' => 'v1']]],
            ],
        ];
    }

    /**
     * @return non-empty-array<non-empty-string, array{0: non-empty-string, 1: array<mixed>, 2: array<mixed>}>
     */
    public static function failureProvider(): array
    {
        return [
            'empty expected' => [
                <<<'EOT'

--- Expected
+++ Actual
@@ @@
-Array &0 []
+Array &0 [
+    0 => 1,
+]

EOT,
                [],
                [1],
            ],

            'empty actual' => [
                <<<'EOT'

--- Expected
+++ Actual
@@ @@
-Array &0 [
-    0 => 1,
-]
+Array &0 []

EOT,
                [1],
                [],
            ],

            'extra element in expected' => [
                <<<'EOT'

--- Expected
+++ Actual
@@ @@
 Array &0 [
     0 => 1,
     1 => 2,
-    2 => 3,
 ]

EOT,
                [1, 2, 3],
                [1, 2],
            ],

            'extra element in actual' => [
                <<<'EOT'

--- Expected
+++ Actual
@@ @@
 Array &0 [
     0 => 1,
     1 => 2,
+    2 => 3,
 ]

EOT,
                [1, 2],
                [1, 2, 3],
            ],

            'different keys, identical values' => [
                <<<'EOT'

--- Expected
+++ Actual
@@ @@
 Array &0 [
     'a' => 1,
-    'b' => 2,
+    'c' => 2,
 ]

EOT,
                ['a' => 1, 'b' => 2],
                ['a' => 1, 'c' => 2],
            ],

            'identical keys, different values' => [
                <<<'EOT'

--- Expected
+++ Actual
@@ @@
 Array &0 [
-    0 => 1,
+    0 => 2,
     'a' => 'b',
     2 => 'c',
 ]

EOT,
                [0 => 1, 'a' => 'b', 2 => 'c'],
                [0 => 2, 'a' => 'b', 2 => 'c'],
            ],

            'different nested values' => [
                <<<'EOT'

--- Expected
+++ Actual
@@ @@
 Array &0 [
     'a' => Array &1 [
-        'b' => 1,
+        'b' => 2,
     ],
 ]

EOT,
                ['a' => ['b' => 1]],
                ['a' => ['b' => 2]],
            ],

            'missing nested key' => [
                <<<'EOT'

--- Expected
+++ Actual
@@ @@
 Array &0 [
     'a' => Array &1 [
         'b' => 1,
-        'c' => 2,
     ],
 ]

EOT,
                ['a' => ['b' => 1, 'c' => 2]],
                ['a' => ['b' => 1]],
            ],

            'different nested array order' => [
                <<<'EOT'

--- Expected
+++ Actual
@@ @@
 Array &0 [
     'a' => Array &1 [
-        0 => 1,
+        0 => 3,
         1 => 2,
-        2 => 3,
+        2 => 1,
     ],
 ]

EOT,
                ['a' => [1, 2, 3]],
                ['a' => [3, 2, 1]],
            ],

            'object instances with different values' => [
                <<<'EOT'

--- Expected
+++ Actual
@@ @@
 Array &0 [
-    0 => stdClass Object #%d (
-        'a' => 'a',
+    0 => stdClass Object #%d (
+        'a' => 'A',
     ),
 ]

EOT,
                [(object) ['a' => 'a']],
                [(object) ['a' => 'A']],
            ],

            'object instances with the same structure but of different classes' => [
                <<<'EOT'

--- Expected
+++ Actual
@@ @@
 Array &0 [
-    0 => PHPUnit\TestFixture\ObjectComparison\ValueObjectA Object #%d (
+    0 => PHPUnit\TestFixture\ObjectComparison\SameStructAsValueObjectA Object #%d (
         'value' => 'a',
     ),
 ]

EOT,
                [new ValueObjectA('a')],
                [new SameStructAsValueObjectA('a')],
            ],

            'object instances with the same structure but of different classes but with a common root' => [
                <<<'EOT'

--- Expected
+++ Actual
@@ @@
 Array &0 [
-    0 => PHPUnit\TestFixture\ObjectComparison\ValueObjectA Object #%d (
+    0 => PHPUnit\TestFixture\ObjectComparison\ChildOfValueObjectA Object #%d (
         'value' => 'a',
     ),
 ]

EOT,
                [new ValueObjectA('a')],
                [new ChildOfValueObjectA('a')],
            ],

            'identical keys in different order, one value differs' => [
                <<<'EOT'

--- Expected
+++ Actual
@@ @@
 Array &0 [
-    'z' => 3,
-    'y' => 2,
     'x' => 1,
+    'y' => 99,
+    'z' => 3,
 ]

EOT,
                ['z' => 3, 'y' => 2, 'x' => 1],
                ['x' => 1, 'y' => 99, 'z' => 3],
            ],

            'identical integer keys in different order, one value differs' => [
                <<<'EOT'

--- Expected
+++ Actual
@@ @@
 Array &0 [
-    2 => 'c',
-    1 => 'b',
     0 => 'a',
+    1 => 'B',
+    2 => 'c',
 ]

EOT,
                [2 => 'c', 1 => 'b', 0 => 'a'],
                [0 => 'a', 1 => 'B', 2 => 'c'],
            ],

            'mixed keys in different order, one value differs' => [
                <<<'EOT'

--- Expected
+++ Actual
@@ @@
 Array &0 [
+    'baz' => 'QUX',
     'foo' => 'bar',
     0 => 1,
-    'baz' => 'qux',
 ]

EOT,
                ['foo' => 'bar', 0 => 1, 'baz' => 'qux'],
                ['baz' => 'QUX', 'foo' => 'bar', 0 => 1],
            ],
        ];
    }

    #[DataProvider('successProvider')]
    public function testSucceedsWhenConstraintEvaluatesToTrue(array $expected, array $actual): void
    {
        $this->assertArraysAreEqualIgnoringOrder($expected, $actual);
    }

    #[DataProvider('failureProvider')]
    public function testFailsWhenConstraintEvaluatesToFalse(string $comparisonFailure, array $expected, array $actual): void
    {
        try {
            $this->assertArraysAreEqualIgnoringOrder($expected, $actual);
        } catch (ExpectationFailedException $e) {
            $this->assertSame('Failed asserting that two arrays are equal while ignoring order.', $e->getMessage());
            $this->assertStringMatchesFormat($comparisonFailure, $e->getComparisonFailure()->toString());

            return;
        }

        $this->fail();
    }
}
