<commit_before><?php

namespace ZxcvbnPhp\Test\Matchers;

use ZxcvbnPhp\Matchers\BaseMatch;
use ZxcvbnPhp\Matchers\SpatialMatch;

/**
 * @covers \ZxcvbnPhp\Matchers\SpatialMatch
 */
class SpatialTest extends AbstractMatchTest
{
    public function shortPatternDataProvider()
    {
        return [
            [''],
            ['/'],
            ['qw'],
            ['*/'],
        ];
    }

    /**
     * @dataProvider shortPatternDataProvider
     * @param $password
     */
    public function testShortPatterns($password)
    {
        $this->assertEquals(
            [],
            SpatialMatch::match($password),
            "doesn't match 1- and 2-character spatial patterns"
        );
    }

    public function testNoPattern()
    {
        $this->assertEquals(
            [],
            SpatialMatch::match('qzpm'),
            "doesn't match non-pattern"
        );
    }

    public function testSurroundedPattern()
    {
        $pattern = "6tfGHJ";
        $password = "<PASSWORD>";

        // for testing, make a subgraph that contains a single keyboard
        $graphs = ['qwerty' => SpatialMatch::getAdjacencyGraphs()['qwerty']];

        $this->checkMatches(
            "matches against spatial patterns surrounded by non-spatial patterns",
            SpatialMatch::match($password, [], $graphs),
            'spatial',
            [$pattern],
            [[3, 8]],
            [
                'graph' => ['qwerty'],
                'turns' => [2],
                'shiftedCount' => [3],
            ]
        );
    }

    public function spatialDataProvider()
    {
        return [
            ['12345',        'qwerty',     1, 0],
            ['@WSX',         'qwerty',     1, 4],
            ['6tfGHJ',       'qwerty',     2, 3],
            ['hGFd',         'qwerty',     1, 2],
            ['/;p09876yhn',  'qwerty',     3, 0],
            ['Xdr%',         'qwerty',     1, 2],
            ['159-',         'keypad',     1, 0],
            ['*84',          'keypad',     1, 0],
            ['/8520',        'keypad',     1, 0],
            ['369',          'keypad',     1, 0],
            ['/963.',        'mac_keypad', 1, 0],
            ['*-632.0214',   'mac_keypad', 9, 0],
            ['aoEP%yIxkjq:', 'dvorak',     4, 5],
            [';qoaOQ:Aoq;a', 'dvorak',    11, 4],
        ];
    }

    /**
     * @dataProvider spatialDataProvider
     * @param $password
     * @param $keyboard
     * @param $turns
     * @param $shifts
     */
    public function testSpatialPatterns($password, $keyboard, $turns, $shifts)
    {
        $graphs = [$keyboard => SpatialMatch::getAdjacencyGraphs()[$keyboard]];

        $this->checkMatches(
            "matches '$password' as a $keyboard pattern",
            SpatialMatch::match($password, [], $graphs),
            'spatial',
            [$password],
            [[0, strlen($password) - 1]],
            [
                'graph' => [$keyboard],
                'turns' => [$turns],
                'shiftedCount' => [$shifts],
            ]
        );
    }

    public function testShiftedCountForMultipleMatches()
    {
        $password = <PASSWORD>";
        $this->checkMatches(
            "shifted count is correct for two matches in a row",
            SpatialMatch::match($password),
            'spatial',
            ['!QAZ', '1qaz'],
            [[0, 3], [4, 7]],
            [
                'graph' => ['qwerty', 'qwerty'],
                'turns' => [1, 1],
                'shiftedCount' => [4, 0],
            ]
        );
    }

    protected function getBaseGuessCount($token): float
    {
        // KEYBOARD_STARTING_POSITIONS * KEYBOARD_AVERAGE_DEGREE * (length - 1)
        // - 1 term because: not counting spatial patterns of length 1
        // eg for length==6, multiplier is 5 for needing to try len2,len3,..,len6
        return SpatialMatch::KEYBOARD_STARTING_POSITION
            * SpatialMatch::KEYBOARD_AVERAGE_DEGREES
            * (strlen($token) - 1);
    }

    public function testGuessesBasic()
    {
        $token = 'zxcvbn';
        $match = new SpatialMatch($token, 0, strlen($token) - 1, $token, [
            'graph' => 'qwerty',
            'turns' => 1,
            'shifted_count' => 0,
        ]);

        $this->assertEquals(
            $this->getBaseGuessCount($token),
            $match->getGuesses(),
            "with no turns or shifts, guesses is starts * degree * (len-1)"
        );
    }

    public function testGuessesShifted()
    {
        $token = 'ZxCvbn';
        $match = new SpatialMatch($token, 0, strlen($token) - 1, $token, [
            'graph' => 'qwerty',
            'turns' => 1,
            'shifted_count' => 2,
        ]);

        $this->assertEquals(
            $this->getBaseGuessCount($token) * (BaseMatch::binom(6, 2) + BaseMatch::binom(6, 1)),
            $match->getGuesses(),
            "guesses is added for shifted keys, similar to capitals in dictionary matching"
        );
    }

    public function testGuessesEverythingShifted()
    {
        $token = 'ZXCVBN';
        $match = new SpatialMatch($token, 0, strlen($token) - 1, $token, [
            'graph' => 'qwerty',
            'turns' => 1,
            'shifted_count' => 6,
        ]);

        $this->assertEquals(
            $this->getBaseGuessCount($token) * 2,
            $match->getGuesses(),
            "when everything is shifted, guesses are double"
        );
    }

    public function complexGuessProvider()
    {
        return [
            ['6yhgf',        2, 19596],
            ['asde3w',       3, 203315],
            ['zxcft6yh',     3, 558460],
            ['xcvgy7uj',     3, 558460],
            ['ertghjm,.',    5, 30160744],
            ['qwerfdsazxcv', 5, 175281377],
        ];
    }

    /**
     * @dataProvider complexGuessProvider<commit_msg>Test known values as ints not floats to avoid exact floatpoint comparisons which may vary depending on php compiling settings and platform<commit_after><?php

namespace ZxcvbnPhp\Test\Matchers;

use ZxcvbnPhp\Matchers\BaseMatch;
use ZxcvbnPhp\Matchers\SpatialMatch;

/**
 * @covers \ZxcvbnPhp\Matchers\SpatialMatch
 */
class SpatialTest extends AbstractMatchTest
{
    public function shortPatternDataProvider()
    {
        return [
            [''],
            ['/'],
            ['qw'],
            ['*/'],
        ];
    }

    /**
     * @dataProvider shortPatternDataProvider
     * @param $password
     */
    public function testShortPatterns($password)
    {
        $this->assertEquals(
            [],
            SpatialMatch::match($password),
            "doesn't match 1- and 2-character spatial patterns"
        );
    }

    public function testNoPattern()
    {
        $this->assertEquals(
            [],
            SpatialMatch::match('qzpm'),
            "doesn't match non-pattern"
        );
    }

    public function testSurroundedPattern()
    {
        $pattern = "6tfGHJ";
        $password = "<PASSWORD>";

        // for testing, make a subgraph that contains a single keyboard
        $graphs = ['qwerty' => SpatialMatch::getAdjacencyGraphs()['qwerty']];

        $this->checkMatches(
            "matches against spatial patterns surrounded by non-spatial patterns",
            SpatialMatch::match($password, [], $graphs),
            'spatial',
            [$pattern],
            [[3, 8]],
            [
                'graph' => ['qwerty'],
                'turns' => [2],
                'shiftedCount' => [3],
            ]
        );
    }

    public function spatialDataProvider()
    {
        return [
            ['12345',        'qwerty',     1, 0],
            ['@WSX',         'qwerty',     1, 4],
            ['6tfGHJ',       'qwerty',     2, 3],
            ['hGFd',         'qwerty',     1, 2],
            ['/;p09876yhn',  'qwerty',     3, 0],
            ['Xdr%',         'qwerty',     1, 2],
            ['159-',         'keypad',     1, 0],
            ['*84',          'keypad',     1, 0],
            ['/8520',        'keypad',     1, 0],
            ['369',          'keypad',     1, 0],
            ['/963.',        'mac_keypad', 1, 0],
            ['*-632.0214',   'mac_keypad', 9, 0],
            ['aoEP%yIxkjq:', 'dvorak',     4, 5],
            [';qoaOQ:Aoq;a', 'dvorak',    11, 4],
        ];
    }

    /**
     * @dataProvider spatialDataProvider
     * @param $password
     * @param $keyboard
     * @param $turns
     * @param $shifts
     */
    public function testSpatialPatterns($password, $keyboard, $turns, $shifts)
    {
        $graphs = [$keyboard => SpatialMatch::getAdjacencyGraphs()[$keyboard]];

        $this->checkMatches(
            "matches '$password' as a $keyboard pattern",
            SpatialMatch::match($password, [], $graphs),
            'spatial',
            [$password],
            [[0, strlen($password) - 1]],
            [
                'graph' => [$keyboard],
                'turns' => [$turns],
                'shiftedCount' => [$shifts],
            ]
        );
    }

    public function testShiftedCountForMultipleMatches()
    {
        $password = <PASSWORD>";
        $this->checkMatches(
            "shifted count is correct for two matches in a row",
            SpatialMatch::match($password),
            'spatial',
            ['!QAZ', '1qaz'],
            [[0, 3], [4, 7]],
            [
                'graph' => ['qwerty', 'qwerty'],
                'turns' => [1, 1],
                'shiftedCount' => [4, 0],
            ]
        );
    }

    protected function getBaseGuessCount($token): float
    {
        // KEYBOARD_STARTING_POSITIONS * KEYBOARD_AVERAGE_DEGREE * (length - 1)
        // - 1 term because: not counting spatial patterns of length 1
        // eg for length==6, multiplier is 5 for needing to try len2,len3,..,len6
        return SpatialMatch::KEYBOARD_STARTING_POSITION
            * SpatialMatch::KEYBOARD_AVERAGE_DEGREES
            * (strlen($token) - 1);
    }

    public function testGuessesBasic()
    {
        $token = 'zxcvbn';
        $match = new SpatialMatch($token, 0, strlen($token) - 1, $token, [
            'graph' => 'qwerty',
            'turns' => 1,
            'shifted_count' => 0,
        ]);

        $this->assertEquals(
            (int)$this->getBaseGuessCount($token),
            (int)$match->getGuesses(),
            "with no turns or shifts, guesses is starts * degree * (len-1)"
        );
    }

    public function testGuessesShifted()
    {
        $token = 'ZxCvbn';
        $match = new SpatialMatch($token, 0, strlen($token) - 1, $token, [
            'graph' => 'qwerty',
            'turns' => 1,
            'shifted_count' => 2,
        ]);

        $this->assertEquals(
            (int)$this->getBaseGuessCount($token) * (BaseMatch::binom(6, 2) + BaseMatch::binom(6, 1)),
            (int)$match->getGuesses(),
            "guesses is added for shifted keys, similar to capitals in dictionary matching"
        );
    }

    public function testGuessesEverythingShifted()
    {
        $token = 'ZXCVBN';
        $match = new SpatialMatch($token, 0, strlen($token) - 1, $token, [
            'graph' => 'qwerty',
            'turns' => 1,
            'shifted_count' => 6,
        ]);

        $this->assertEquals(
            (int)$this->getBaseGuessCount($token) * 2,
            (int)$match->getGuesses(),
            "when everything is shifted, guesses are double"
        );
    }

    public function complexGuessProvider()
    {
        return [
            ['6yhgf',        2, 19596],
            ['asde3w',       3, 203315],
            ['zxcft6yh',     3, 558460],
            ['xcvgy7uj',     3, 558460],
            ['ertghjm,.',    5, 30160744],
            ['qwerfdsazxcv', 5, 175281377],
        ];
    }

    /**
     * @dataProvider complexGuessProvider
{'fraction_non_alphanumeric': 0.14907148308318494, 'fraction_numerical': 0.026625964555244637, 'mean_word_length': 1.5966534566270365, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 75, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7195352', 'n_tokens_mistral': 3922, 'n_tokens_neox': 3553, 'n_words': 760}