<?php

namespace Psalm\Tests\TypeReconciliation;

use Psalm\Tests\TestCase;
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;

class ArrayKeyExistsTest extends TestCase
{
    use ValidCodeAnalysisTestTrait;
    use InvalidCodeAnalysisTestTrait;

    /**
     * @return iterable<string,array{code:string,assertions?:array<string,string>,ignored_issues?:list<string>}>
     */
    public function providerValidCodeParse(): iterable
    {
        return [
            'arrayKeyExistsOnStringArrayShouldInformArrayness' => [
                'code' => '<?php
                    /**
                     * @param string[] $a
                     * @return array{b: string}
                     */
                    function foo(array $a) {
                        if (array_key_exists("b", $a)) {
                            return $a;
                        }

                        throw new \Exception("bad");
                    }'
            ],
             'arrayKeyExistsThrice' => [
                'code' => '<?php
                    function three(array $a): void {
                        if (!array_key_exists("a", $a)
                            || !array_key_exists("b", $a)
                            || !array_key_exists("c", $a)
                            || (!is_string($a["a"]) && !is_int($a["a"]))
                            || (!is_string($a["b"]) && !is_int($a["b"]))
                            || (!is_string($a["c"]) && !is_int($a["c"]))
                        ) {
                            throw new \Exception();
                        }

                        echo $a["a"];
                        echo $a["b"];
                    }'
            ],
            'arrayKeyExistsTwice' => [
                'code' => '<?php
                    function two(array $a): void {
                        if (!array_key_exists("a", $a) || !(is_string($a["a"]) || is_int($a["a"])) ||
                            !array_key_exists("b", $a) || !(is_string($a["b"]) || is_int($a["b"]))
                        ) {
                            throw new \Exception();
                        }

                        echo $a["a"];
                        echo $a["b"];
                    }'
            ],
            'assertConstantOffsetsInMethod' => [
                'code' => '<?php
                    class C {
                        public const ARR = [
                            "a" => ["foo" => true],
                            "b" => []
                        ];

                        public function bar(string $key): bool {
                            if (!array_key_exists($key, self::ARR) || !array_key_exists("foo", self::ARR[$key])) {
                                return false;
                            }

                            return self::ARR[$key]["foo"];
                        }
                    }',
                'assertions' => [],
                'ignored_issues' => ['MixedReturnStatement', 'MixedInferredReturnType'],
            ],
            'assertSelfClassConstantOffsetsInFunction' => [
                'code' => '<?php
                    namespace Ns;

                    class C {
                        public const ARR = [
                            "a" => ["foo" => true],
                            "b" => []
                        ];

                        public function bar(?string $key): bool {
                            if ($key === null || !array_key_exists($key, self::ARR) || !array_key_exists("foo", self::ARR[$key])) {
                                return false;
                            }

                            return self::ARR[$key]["foo"];
                        }
                    }',
                'assertions' => [],
                'ignored_issues' => ['MixedReturnStatement', 'MixedInferredReturnType'],
            ],
            'assertNamedClassConstantOffsetsInFunction' => [
                'code' => '<?php
                    namespace Ns;

                    class C {
                        public const ARR = [
                            "a" => ["foo" => true],
                            "b" => [],
                        ];
                    }

                    function bar(?string $key): bool {
                        if ($key === null || !array_key_exists($key, C::ARR) || !array_key_exists("foo", C::ARR[$key])) {
                            return false;
                        }

                        return C::ARR[$key]["foo"];
                    }',
                'assertions' => [],
                'ignored_issues' => ['MixedReturnStatement', 'MixedInferredReturnType'],
            ],
            'possiblyUndefinedArrayAccessWithArrayKeyExists' => [
                'code' => '<?php
                    if (rand(0,1)) {
                      $a = ["a" => 1];
                    } else {
                      $a = [2, 3];
                    }

                    if (array_key_exists(0, $a)) {
                        echo $a[0];
                    }',
            ],
            'arrayKeyExistsShoudldNotModifyIntType' => [
                'code' => '<?php
                    class HttpError {
                        const ERRS = [
                            403 => "a",
                            404 => "b",
                            500 => "c"
                        ];
                    }

                    function init(string $code) : string {
                        if (array_key_exists($code, HttpError::ERRS)) {
                            return $code;
                        }

                        return "";
                    }'
            ],
            'arrayKeyExistsWithClassConst' => [
                'code' => '<?php
                    class C {}
                    class D {}

                    class A {
                        const FLAGS = [
                            0 => [C::class => "foo"],
                            1 => [D::class => "bar"],
                        ];

                        private function foo(int $i) : void {
                            if (array_key_exists(C::class, self::FLAGS[$i])) {
                                echo self::FLAGS[$i][C::class];
                            }
                        }
                    }'
            ],
            'constantArrayKeyExistsWithClassConstant' => [
                'code' => '<?php
                    class Foo {
                        public const F = "key";
                    }

                    /** @param array{key?: string} $a */
                    function one(array $a): void {
                        if (array_key_exists(Foo::F, $a)) {
                            echo $a[Foo::F];
                        }
                    }'
            ],
            'assertTypeNarrowedByNestedIsset' => [
                'code' => '<?php
                    /**
                     * @psalm-suppress MixedMethodCall
                     * @psalm-suppress MixedArgument
                     */
                    function foo(array $array = []): void {
                        if (array_key_exists("a", $array)) {
                            echo $array["a"];
                        }

                        if (array_key_exists("b", $array)) {
                            echo $array["b"]->format("Y-m-d");
                        }
                    }',
            ],
            'assertArrayKeyExistsRefinesType' => [
                'code' => '<?php
                    class Foo {
                        public const DAYS = [
                            1 => "mon",
                            2 => "tue",
                            3 => "wed",
                            4 => "thu",
                            5 => "fri",
                            6 => "sat",
                            7 => "sun",
                        ];

                        /** @param key-of<self::DAYS> $dayNum*/
                        private static function doGetDayName(int $dayNum): string {
                            return self::DAYS[$dayNum];
                        }

                        /** @throws LogicException */
                        public static function getDayName(int $dayNum): string {
                            if (! array_key_exists($dayNum, self::DAYS)) {
                                throw new \LogicException();
                            }
                            return self::doGetDayName($dayNum);
                        }
                    }'
            ],
            'arrayKeyExistsInferString' => [
                'code' => '<?php
                    function foo(mixed $file) : string {
                        /** @psalm-suppress MixedArgument */
                        if (array_key_exists($file, ["a" => 1, "b" => 2])) {
                            return $file;
                        }

                        return "";
                    }',
                'assertions' => [],
                'ignored_issues' => [],
                'php_version' => '8.0'
            ],
            'arrayKeyExistsComplex' => [
                'code' => '<?php
                    class A {
                        private const MAP = [
                            "a" => 1,
                            "b" => 2,
                            "c" => 3,
                            "d" => 4,
                            "e" => 5,
                            "f" => 6,
                            "g" => 7,
                            "h" => 8,
                            "i" => 9,
                            "j" => 10,
                            "k" => 11,
                        ];

                        public function doWork(string $a): void {
                            if (!array_key_exists($a, self::MAP)) {}
                        }
                    }'
            ],
            'arrayKeyExistsAccess' => [
                'code' => '<?php
                    /** @param array<int, string> $arr */
                    function foo(array $arr) : void {
                        if (array_key_exists(1, $arr)) {
                            $a = ($arr[1] === "b") ? true : false;
                        }
                    }',
            ],
            'arrayKeyExistsVariable' => [
                'code' => '<?php
                    class pony
                    {
                    }
                    /**
                     * @param array{0?: string, test?: string, pony?: string} $params
                     * @return string|null
                     */
                    function a(array $params = [])
                    {
                        foreach ([0, "test", pony::class] as $key) {
                            if (\array_key_exists($key, $params)) {
                                return $params[$key];
                            }
                        }
                    }'
            ],
            'noCrashOnArrayKeyExistsBracket' => [
                'code' => '<?php
                    class MyCollection {
                        /**
                         * @param int $commenter
                         * @param int $numToGet
                         * @return int[]
                         */
                        public function getPosters($commenter, $numToGet=10) {
                            $posters = array();
                            $count = 0;
                            $a = new ArrayObject([[1234]]);
                            $iter = $a->getIterator();
                            while ($iter->valid() && $count < $numToGet) {
                                $value = $iter->current();
                                if ($value[0] != $commenter) {
                                    if (!array_key_exists($value[0], $posters)) {
                                        $posters[$value[0]] = 1;
                                        $count++;
                                    }
                                }
                                $iter->next();
                            }
                            return array_keys($posters);
                        }
                    }',
                'assertions' => [],
                'ignored_issues' => [
                    'MixedArrayAccess', 'MixedAssignment', 'MixedArrayOffset',
                    'MixedArgument',
                ],
            ],
            'arrayKeyExistsTwoVars' => [
                'code' => '<?php
                    /**
                     * @param array{a: string, b: string, c?: string} $info
                     */
                    function getReason(array $info, string $key, string $value): bool {
                        if (array_key_exists($key, $info) && $info[$key] === $value) {
                            return true;
                        }

                        return false;
                    }'
            ],
            'allowIntKeysToo' => [
                'code' => '<?php
                    /**
                     * @param array<1|2|3, string> $arr
                     * @return 1|2|3
                     */
                    function checkArrayKeyExistsInt(array $arr, int $int): int
                    {
                        if (array_key_exists($int, $arr)) {
                            return $int;
                        }

                        return 1;
                    }'
            ],
            'comparesStringAndAllIntKeysCorrectly' => [
                'code' => '<?php
                    /**
                     * @param array<1|2|3, string> $arr
                     * @return bool
                     */
                    function checkArrayKeyExistsComparison(array $arr, string $key): bool
                    {
                        if (array_key_exists($key, $arr)) {
                            return true;
                        }
                        return false;
                    }'
            ],
            'comparesStringAndAllIntKeysCorrectlyNegated' => [
                'code' => '<?php
                    /**
                     * @param array<1|2|3, string> $arr
                     * @return bool
                     */
                    function checkArrayKeyExistsComparisonNegated(array $arr, string $key): bool
                    {
                        if (!array_key_exists($key, $arr)) {
                            return false;
                        }
                        return true;
                    }'
            ],
            'arrayKeyExistsAssertBothWays' => [
                'code' => '<?php
                    class a {
                        const STATE_A = 0;
                        const STATE_B = 1;
                        const STATE_C = 2;
                        /**
                         * @return array<self::STATE_*, non-empty-string>
                         * @psalm-pure
                         */
                        public static function getStateLabels(): array {
                            return [
                                self::STATE_A => "A",
                                self::STATE_B => "B",
                                self::STATE_C => "C",
                            ];
                        }
                        /**
                         * @param self::STATE_* $state
                         */
                        public static function getStateLabelIf(int $state): string {
                            $states = self::getStateLabels();
                            if (array_key_exists($state, $states)) {
                                return $states[$state];
                            }
                            return "";
                        }
                    }'
            ],
        ];
    }

    /**
     * @return iterable<string,array{code:string,error_message:string,ignored_issues?:list<string>,php_version?:string}>
     */
    public function providerInvalidCodeParse(): iterable
    {
        return [
            'possiblyUndefinedArrayAccessWithArrayKeyExistsOnWrongKey' => [
                'code' => '<?php
                    if (rand(0,1)) {
                      $a = ["a" => 1];
                    } else {
                      $a = [2, 3];
                    }

                    if (array_key_exists("a", $a)) {
                        echo $a[0];
                    }',
                'error_message' => 'PossiblyUndefinedArrayOffset',
            ],
            'possiblyUndefinedArrayAccessWithArrayKeyExistsOnMissingKey' => [
                'code' => '<?php
                    if (rand(0,1)) {
                      $a = ["a" => 1];
                    } else {
                      $a = [2, 3];
                    }

                    if (array_key_exists("b", $a)) {
                        echo $a[0];
                    }',
                'error_message' => 'PossiblyUndefinedArrayOffset',
            ],
            'dontCreateWeirdString' => [
                'code' => '<?php
                    /**
                     * @psalm-param array{inner:string} $options
                     */
                    function go(array $options): void {
                        if (!array_key_exists(\'size\', $options)) {
                            throw new Exception(\'bad\');
                        }

                        /** @psalm-suppress MixedArgument */
                        echo $options[\'\\\'size\\\'\'];
                    }',
                'error_message' => 'InvalidArrayOffset',
            ],
        ];
    }
}
