<?php

namespace Ignite\Core\Tests\Unit\Entities\Filters;

use Ignite\Core\Entities\Block;
use Ignite\Core\Entities\Participant;
use Ignite\Core\Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;

class BaseQueryPermissionFilterTest extends TestCase
{
    use RefreshDatabase;

    /**
     * @dataProvider can_check_if_user_can_perform_action_with_specific_model_provider
     * @test
     */
    public function can_check_if_user_can_perform_action_with_specific_model($authedParticipantData, $permission, $participantFetcher, $expectedResult)
    {
        // Arrange
        $this->actingAs(factory(Participant::class)->create($authedParticipantData)->user);
        $this->seedParticipants();
        $participantPermissions = app(DummyParticipantPermissionFilter::class)
            ->forPermission($permission);

        // Execute
        $result = $participantPermissions->canAccess($participantFetcher());

        // Check
        $this->assertEquals($expectedResult, $result);
    }

    public function can_check_if_user_can_perform_action_with_specific_model_provider()
    {
        yield [
            ['state' => 'TX', 'type' => 'Account Executive'],
            'core.user.participant.browse',
            function () {
                return Participant::query()->where('type', 'Type1')->first();
            },
            true,
        ];

        yield [
            ['state' => 'TX', 'type' => 'Account Executive'],
            'core.user.participant.browse',
            function () {
                return Participant::query()->where('type', 'Type3')->first();
            },
            false,
        ];

        yield [
            ['state' => 'TX', 'type' => 'Account Executive'],
            'core.user.participant.update',
            function () {
                return Participant::query()->where('type', 'Type1')->first();
            },
            true,
        ];

        yield [
            ['state' => 'TX', 'type' => 'Account Executive'],
            'core.user.participant.update',
            function () {
                return Participant::query()->where('type', 'Type2')->first()->getKey();
            },
            false,
        ];
    }

    /** @test */
    public function can_specify_which_user_to_check_when_asking_can_with()
    {
        // Arrange
        $this->actingAs(factory(Participant::class)->create(['state' => 'TX', 'type' => 'Account Executive'])->user);
        $user = factory(Participant::class)->create(['state' => 'NM', 'type' => 'Account Executive'])->user;
        $this->seedParticipants();
        $participantPermissions = app(DummyParticipantPermissionFilter::class)
            ->forPermission('core.user.participant.update');
        $fetchParticipant = function ($type) {
            return Participant::query()->firstWhere('type', $type);
        };

        // Execute & Check
        $this->assertEquals(false, $participantPermissions->canAccess($fetchParticipant('Type1'), $user));
        $this->assertEquals(false, $participantPermissions->canAccess($fetchParticipant('Type2'), $user));
        $this->assertEquals(true, $participantPermissions->canAccess($fetchParticipant('Type3'), $user));
    }

    /** @test */
    public function verifies_the_correct_model_type_is_passed_in()
    {
        // Arrange
        $participantPermissions = app(DummyParticipantPermissionFilter::class)
            ->forPermission('some.permission'); // Should we verify this is a permission this class is responsible for? Seems like a good idea.

        // Set expectation
        $this->expectException(\LogicException::class);

        // Execute
        $participantPermissions->canAccess(factory(Block::class)->create());
    }

    /** @test */
    public function scopes_a_query()
    {
        // Arrange
        $query = Participant::query();
        $this->actingAs(factory(Participant::class)->create(['state' => 'AZ'])->user);
        $this->seedParticipants();
        $participantPermissions = app(DummyParticipantPermissionFilter::class)
            ->forPermission('core.user.participant.browse');

        // Pre-check
        $this->assertEquals(16, $query->count());

        // Execute
        $query = $participantPermissions->apply($query);

        // Check
        $this->assertEquals(9, $query->count(), 'Should get Type4 participants & authed participant');
    }

    /** @test */
    public function scopes_a_query_when_the_table_is_aliased()
    {
        // Arrange
        $query = DB::table('core_participant AS participant');
        $authedUser = factory(Participant::class)->create(['state' => 'AZ'])->user;
        $this->actingAs($authedUser);
        $this->seedParticipants();
        $participantPermissions = app(DummyParticipantPermissionFilter::class)
            ->forPermission('core.user.participant.browse');

        // Pre-check
        $this->assertEquals(16, $query->count());

        // Execute
        $query = $participantPermissions->apply($query, 'participant', $authedUser);

        // Check
        $this->assertEquals(9, $query->count(), 'Should get Type4 participants & authed participant');
    }

    /**
     * @test
     * @dataProvider can_scope_based_on_authed_user_provider
     */
    public function can_scope_based_on_authed_user(?array $participantData, int $expectedRows)
    {
        // Arrange
        $user = null;
        if ($participantData) {
            // Kinda weird, but no participant data passed in also implies we don't want an authed user either.
            // Not authing the participantData participant to test the case where we're passing in a user that isn't
            // the authed user (let's say we're sending an email via a background job).
            $this->actingAs(factory(Participant::class)->create()->user);
            $user = factory(Participant::class)->create($participantData)->user;
        }

        $query = DB::table('core_participant AS participant');
        $this->seedParticipants();
        $participantPermissions = app(DummyParticipantPermissionFilter::class)
            ->forPermission('core.user.participant.browse');

        // Pre-check
        $this->assertEquals($participantData ? 17 : 15, $query->count());

        // Execute
        $query = $participantPermissions->apply($query, 'participant', $user);

        // Check
        $this->assertEquals($expectedRows, $query->count());
    }

    public function can_scope_based_on_authed_user_provider()
    {
        yield [
            ['state' => 'TX', 'type' => 'Account Executive'],
            7,
        ];

        yield [
            ['state' => 'NM', 'type' => 'Account Executive'],
            2,
        ];

        yield [
            ['state' => 'NE', 'type' => 'Account Executive'],
            9,
        ];

        yield [
            null, // No authed user
            0,
        ];
    }

    /**
     * @dataProvider can_filter_based_on_permission_given_provider
     * @test
     */
    public function can_filter_based_on_permission_given($authedParticipantData, $permission, $expectedRows)
    {
        // Arrange
        $this->actingAs(factory(Participant::class)->create($authedParticipantData)->user);
        $query = Participant::query();
        $this->seedParticipants();
        $participantPermissions = app(DummyParticipantPermissionFilter::class)
            ->forPermission($permission);

        // Pre-check
        $this->assertEquals(16, $query->count());

        // Execute
        $query = $participantPermissions->apply($query);

        // Check
        $this->assertEquals($expectedRows, $query->count());
    }

    public function can_filter_based_on_permission_given_provider()
    {
        yield [
            ['state' => 'TX', 'type' => 'Account Executive'],
            'core.user.participant.browse',
            7,
        ];

        yield [
            ['state' => 'TX', 'type' => 'Account Executive'],
            'core.user.participant.update',
            5,
        ];
    }

    /**
     * @return void
     */
    private function seedParticipants(): void
    {
        factory(Participant::class)->times(4)->create(['type' => 'Type1']);
        factory(Participant::class)->times(2)->create(['type' => 'Type2']);
        factory(Participant::class)->times(1)->create(['type' => 'Type3']);
        factory(Participant::class)->times(8)->create(['type' => 'Type4']);
    }
}
