<?php

namespace Ignite\Core\Tests\Unit\Auth;

use Illuminate\Auth\AuthenticationException;
use Illuminate\Contracts\Session\Session;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Ignite\Core\Auth\Authorization;
use Ignite\Core\Auth\Impersonation;
use Ignite\Core\Entities\Group;
use Ignite\Core\Entities\Permission;
use Ignite\Core\Entities\User;
use Ignite\Core\Tests\TestCase;

class ImpersonationTest extends TestCase
{
    use RefreshDatabase;

    /**
     * @test
     * @group Authorization
     * @group Impersonation
     */
    public function it_can_determine_when_not_impersonating()
    {
        $session = \Mockery::mock(Session::class)
            ->shouldReceive('has')
            ->with(Impersonation::SESSION_KEY)
            ->andReturn(false);

        $this->app->instance(Session::class, $session->getMock());

        $user = factory(User::class)->create();

        $this->actingAs($user);

        $impersonation = new Impersonation($this->app);

        $this->assertFalse($impersonation->isImpersonating());
    }

    /**
     * @test
     * @group Authorization
     * @group Impersonation
     */
    public function it_can_determine_when_impersonating()
    {
        $session = \Mockery::mock(Session::class)->shouldReceive('has')->with(Impersonation::SESSION_KEY)->andReturn(true);
        $this->app->instance('session.store', $session->getMock());

        $user = factory(User::class)->create();
        $this->actingAs($user);

        $impersonation = new Impersonation($this->app);

        $this->assertTrue($impersonation->isImpersonating());
    }

    /**
     * @test
     * @group Authorization
     * @group Impersonation
     */
    public function it_should_allow_impersonation_when_the_target_user_can_be_impersonated()
    {
        $session = \Mockery::mock(Session::class)->shouldReceive('has')->with(Impersonation::SESSION_KEY)->andReturn(false)->getMock();
        $this->app->instance('session.store', $session);

        $permission = factory(Permission::class)->create(['key' => Impersonation::PERMISSION_KEY]);
        $group1 = factory(Group::class)->create([
            'key' => 'program_manager',
            'name' => 'Program Manager',
            'level' => 2
        ]);
        $group1->givePermissionTo($permission)->refresh();
        $group2 = factory(Group::class)->create([
            'key' => 'participant',
            'name' => 'Participant',
            'level' => 8
        ])->refresh();
        $user1 = factory(User::class)->create()->assignToGroup($group1)->refresh();
        $user2 = factory(User::class)->create()->assignToGroup($group2)->refresh();

        $this->app->make(Authorization::class)->forgetPermissions()->registerPermissions();

        $this->actingAs($user1);
        $impersonation = new Impersonation($this->app);

        $this->assertTrue($impersonation->canImpersonate($user1, $user2));
    }

    /**
     * @test
     * @group Authorization
     * @group Impersonation
     */
    public function it_should_disallow_the_target_user_from_being_impersonated_when_the_target_is_assigned_a_group_with_better_access()
    {
        $session = \Mockery::mock(Session::class)->shouldReceive('has')->with(Impersonation::SESSION_KEY)->andReturn(false);
        $this->app->instance('session.store', $session->getMock());
        $permission = factory(Permission::class)->create(['key' => Impersonation::PERMISSION_KEY]);
        $group1 = factory(Group::class)->create(['key' => 'specialist', 'level' => 3]);
        $group1->givePermissionTo($permission)->setRelation('permissions', collect($permission))->refresh();
        $group2 = factory(Group::class)->create(['key' => 'admin', 'level' => 1]);
        $user1 = factory(User::class)->create()->assignToGroup($group1)->refresh();
        $user2 = factory(User::class)->create()->assignToGroup($group2)->refresh();

        $this->app->make(Authorization::class)->forgetPermissions()->registerPermissions();

        $this->actingAs($user1);
        $impersonation = new Impersonation($this->app);

        $this->assertFalse($impersonation->canImpersonate($user1, $user2), 'User with lower level group cannot impersonate.');
    }

    /**
     * @test
     * @group Authorization
     * @group Impersonation
     */
    public function it_should_provide_a_helper_method_to_invert_impersonate_check()
    {
        $session = \Mockery::mock(Session::class)
            ->shouldReceive('has')
            ->with(Impersonation::SESSION_KEY)
            ->andReturn(true);

        $this->app->instance('session.store', $session->getMock());

        $group1 = factory(Group::class)->create([
            'key' => 'manager',
            'name' => 'manager',
            'level' => 2
        ]);
        $group2 = factory(Group::class)->create([
            'key' => 'specialist',
            'name' => 'specialist',
            'level' => 3
        ]);

        $user1 = factory(User::class)->create()->assignToGroup($group1)->refresh();
        $user2 = factory(User::class)->create()->assignToGroup($group2)->refresh();

        $this->app->make(Authorization::class)->forgetCachedPermissions()->registerPermissions();

        $this->actingAs($user1);
        $impersonation = new Impersonation($this->app);

        $this->assertTrue($impersonation->cannotImpersonate($user1, $user2));
    }

    /**
     * @test
     * @group Authorization
     * @group Impersonation
     */
    public function it_should_disallow_the_target_user_from_being_impersonated_when_the_logged_in_user_doesnt_have_the_required_permission()
    {
        $session = \Mockery::mock(Session::class)->shouldReceive('has')->with(Impersonation::SESSION_KEY)->andReturn(true);
        $this->app->instance('session.store', $session->getMock());
        $group1 = factory(Group::class)->create(['key' => 'manager', 'level' => 2]);
        $group2 = factory(Group::class)->create(['key' => 'specialist', 'level' => 4]);
        $user1 = factory(User::class)->create()->assignToGroup($group1)->refresh();
        $user2 = factory(User::class)->create()->assignToGroup($group2)->refresh();

        $this->app->make(Authorization::class)->forgetCachedPermissions()->registerPermissions();

        $this->actingAs($user1);
        $impersonation = new Impersonation($this->app);

        $this->assertFalse($impersonation->canImpersonate($user1, $user2), 'User without permission to impersonate cannot impersonate.');
    }

    /**
     * @test
     * @group Authorization
     * @group Impersonation
     */
    public function it_can_start_impersonating_a_user()
    {
        $permission = factory(Permission::class)->create(['key' => Impersonation::PERMISSION_KEY]);
        $group1 = factory(Group::class)->create(['key' => 'manager', 'level' => 2]);
        $group1->givePermissionTo($permission)->refresh();
        $group2 = factory(Group::class)->create(['key' => 'participant', 'level' => 8])->refresh();
        $user1 = factory(User::class)->create()->assignToGroup($group1)->refresh();
        $user2 = factory(User::class)->create()->assignToGroup($group2)->refresh();

        $this->app->make(Authorization::class)->forgetPermissions()->registerPermissions();

        $this->actingAs($user1);
        $impersonation = new Impersonation($this->app);
        $impersonating = $impersonation->startImpersonating($user1, $user2);

        $this->assertAuthenticatedAs($user2);
        $this->assertInstanceOf(User::class, $impersonating);
        $this->assertSame($impersonating, $user2);
        $this->assertEquals($impersonation->getImpersonatorId(), $user1->getKey());
        $this->assertEquals($impersonation->getImpersonatedId(), $user2->getKey());
    }

    /**
     * @test
     * @group Authorization
     * @group Impersonation
     */
    public function it_throws_an_exception_when_it_cannot_start_impersonating_a_user()
    {
        $this->expectException(AuthenticationException::class);

        $session = \Mockery::mock(Session::class)
            ->shouldReceive('get')
            ->with(Impersonation::SESSION_KEY)
            ->andReturn('foo', new \Exception());
        $this->app->instance('session.store', $session->getMock());

        $permission = factory(Permission::class)->create(['key' => Impersonation::PERMISSION_KEY]);
        $group1 = factory(Group::class)->create(['key' => 'manager', 'level' => 2]);
        $group1->givePermissionTo($permission)->refresh();
        $group2 = factory(Group::class)->create(['key' => 'participant', 'level' => 8])->refresh();
        $user1 = factory(User::class)->create()->assignToGroup($group1)->refresh();
        $user2 = factory(User::class)->create()->assignToGroup($group2)->refresh();

        $this->app->make(Authorization::class)->forgetPermissions()->registerPermissions();

        $this->actingAs($user1);
        $impersonation = new Impersonation($this->app);
        $impersonation->startImpersonating($user1, $user2);
    }

    /**
     * @test
     * @group Authorization
     * @group Impersonation
     */
    public function it_can_stop_impersonating_a_user()
    {
        $permission = factory(Permission::class)->create(['key' => Impersonation::PERMISSION_KEY]);
        $group1 = factory(Group::class)->create(['key' => 'manager', 'level' => 2]);
        $group1->givePermissionTo($permission)->refresh();
        $group2 = factory(Group::class)->create(['key' => 'participant', 'level' => 8])->refresh();
        $user1 = factory(User::class)->create()->assignToGroup($group1)->refresh();
        $user2 = factory(User::class)->create()->assignToGroup($group2)->refresh();

        $this->app->make(Authorization::class)->forgetPermissions()->registerPermissions();

        $this->actingAs($user1);
        $impersonation = new Impersonation($this->app);

        $impersonation->startImpersonating($user1, $user2);
        $this->assertAuthenticatedAs($user2);

        $impersonator = $impersonation->stopImpersonating();
        $this->assertAuthenticatedAs($user1);

        $this->assertInstanceOf(User::class, $impersonator);
    }

    /**
     * @test
     * @group Authorization
     * @group Impersonation
     */
    public function it_throws_an_exception_when_it_cannot_stop_impersonating_a_user()
    {
        $this->expectException(AuthenticationException::class);
        $session = \Mockery::mock(Session::class)
            ->shouldReceive('get')
            ->with(Impersonation::SESSION_KEY)
            ->andReturn('foo', new \Exception());
        $this->app->instance('session.store', $session->getMock());

        $group = factory(Group::class)->create(['key' => 'participant', 'level' => 8])->refresh();
        $user = factory(User::class)->create()->assignToGroup($group)->refresh();
        $this->app->make(Authorization::class)->forgetPermissions()->registerPermissions();

        $this->actingAs($user);
        $impersonation = new Impersonation($this->app);
        $impersonation->stopImpersonating();
    }
}
