<?php

namespace AuthStack\TwoFactor\tests\units\Collection;


use AuthStack\TwoFactor\Model\MFAMode;
use mageekguy\atoum;
use Buckhill\RFC4226\OTP;
use AuthStack\TwoFactor\Model\MFAKey;
use AuthStack\TwoFactor\Collection\KeyCollection as Collection;


class KeyCollection extends atoum
{
    public function testLoadForUser()
    {
        $this
            ->given($first = Collection::loadForUser(1))
            ->and($second = Collection::loadForUser(2))
            ->and($third = Collection::loadForUser(3))
            ->then
                ->integer($first->count())->isEqualTo(3)
                ->integer($second->count())->isEqualTo(1)
                ->integer($third->count())->isEqualTo(0)

                ->boolean($first->hasU2F())->isTrue()
                ->boolean($second->hasU2F())->isTrue()
                ->boolean($third->hasU2F())->isFalse()

                ->boolean($first->hasOTP())->isTrue()
                ->boolean($second->hasOTP())->isFalse()
                ->boolean($third->hasOTP())->isFalse()

                ->array($first->getKeyList())->hasSize(3)
                ->array($second->getKeyList())->hasSize(1)
                ->array($third->getKeyList())->hasSize(0)
            ;
    }

    public function testVerifyRFC4226()
    {
        $this
            ->given($collection = Collection::loadForUser(1))
            ->and($secKey = $collection->getKeyCollection()->where('id', 4)->first())
            ->and($current_counter = $secKey->fromMetadata('counter'))
            ->then
                ->object($secKey)->isInstanceOf(MFAKey::class)
                ->string($otp_good = $this->getRFC4226OTP($secKey, 2))->hasLength(6) // Advances the counter position for 2 places (that's what 2 is)
                ->string($otp_at_limit = $this->getRFC4226OTP($secKey, 99))->hasLength(6) // Advances the counter position for 99 places, placing us at the very limit of window
                ->string($otp_too_far = $this->getRFC4226OTP($secKey, 100))->hasLength(6) // Advances the counter for 100 places, 1 place too far from default window so we should get errors for this one
                ->boolean($secKey->verify($otp_at_limit))->isTrue()
                ->boolean($secKey->verify($otp_too_far))->isFalse()
                ->array($secKey->getFailureReason())->hasSize(1)
                ->string($secKey->getFailureReason(true))->contains('No OTP match found. Started at counter value')

                ->boolean($secKey->verify($otp_good))->isTrue()
                ->boolean($secKey->synchronize())->isTrue()
                ->integer($secKey->fromMetadata('counter'))->isEqualTo($current_counter + 2)
            ;
    }


    public function testAddKey()
    {
        $this
            ->given($collection = Collection::loadForUser(1))
            ->then
                ->object($key = $collection->add([
                    'mode_id'       => MFAMode::MODE_RFC4226,
                    'title'         => 'Test Key',
                    'metadata'      => [
                        'secret'    => $secret = hash('sha256', str_random(32)),
                        'counter'   => 0
                    ]
                ]))->isInstanceOf(MFAKey::class)

                ->integer($collection->count())->isEqualTo(4)

                ->boolean($collection->remove($key->id))->isTrue()

                ->integer($collection->count())->isEqualTo(3)
        ;
    }


    /**
     * @param MFAKey $key
     * @param int $count_incr
     * @return string
     */
    protected function getRFC4226OTP(MFAKey $key, $count_incr = 1)
    {
        $otp = new OTP($key->fromMetadata('secret'), $key->fromMetadata('algo', 'sha1'));

        $counter_current = (int)$key->fromMetadata('counter', 0);

        $otp->setConversionMethod(function($secret) {
            return $secret;
        });

        $new_otp = $otp->at($counter_current + $count_incr);

        return $new_otp;
    }
}