<?php

namespace Ignite\Core\Tests\Unit\Repositories;

use Carbon\Carbon;
use Carbon\Exceptions\InvalidFormatException;
use Ignite\Core\Models\Import\Hashers\TransactionHasher;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Ignite\Core\Entities\User;
use Ignite\Core\Entities\Participant;
use Ignite\Core\Tests\TestCase;
use Ignite\Core\Entities\Transaction;
use Ignite\Core\Repositories\TransactionRepository;

class TransactionRepositoryTest extends TestCase
{
    use RefreshDatabase;

    /** @var TransactionRepository */
    private $transactionRepository;

    protected function setUp(): void
    {
        parent::setUp();

        $this->transactionRepository = app(TransactionRepository::class);
    }

    /**
     * @test
     * @group Transaction
     * @group Repository
     */
    public function it_returns_the_expected_system_transaction_types()
    {
        $types = $this->transactionRepository->getAllowedTypes();

        $this->assertIsArray($types);

        $this->assertContainsEquals('CANCELLED', $types);
        $this->assertContainsEquals('EARNED', $types);
        $this->assertContainsEquals('REDEEMED', $types);
        $this->assertContainsEquals('MANUAL-RECEIVE', $types);
        $this->assertContainsEquals('MANUAL-REDEEM', $types);
    }

    /**
     * @test
     * @group Transaction
     * @group Repository
     */
    public function it_merges_configured_transaction_types()
    {
        $this->app['config']->set('core.transaction.types', ['FOO' => 'FOO']);
        $types = $this->transactionRepository->getAllowedTypes();

        $this->assertIsArray($types);

        $this->assertContainsEquals('FOO', $types);
    }

    /**
     * @test
     * @group Transaction
     * @group Repository
     */
    public function it_provides_matching_keys_and_values_for_types()
    {
        $this->app['config']->set('core.transaction.types', ['BAR' => 'FOO']);
        $types = $this->transactionRepository->getAllowedTypes();

        $this->assertIsArray($types);
        $this->assertArrayHasKey('FOO', $types);
        $this->assertContainsEquals('FOO', $types);
    }

    /**
     * @test
     * @group Transaction
     * @group Repository
     */
    public function it_can_get_the_balance_for_the_current_user()
    {
        $user = $this->loginAsAdmin();
        $participant = factory(Participant::class)->create([
            'user_id' => $user->getKey()
        ]);

        factory(Transaction::class)->create([
            'user_id' => $participant->getKey(),
            'value' => 1000
        ]);

        $balance = $this->transactionRepository->getBalance();
        $this->assertEquals(1000.0, $balance);
    }

    /**
     * @test
     * @group Transaction
     * @group Repository
     */
    public function it_can_get_the_balance_for_the_provided_user()
    {
        $this->loginAsAdmin();

        $user = factory(User::class)->create();
        $participant = factory(Participant::class)->create([
            'user_id' => $user->getKey()
        ]);

        factory(Transaction::class)->create([
            'user_id' => $participant->getKey(),
            'value' => 500
        ]);

        $balance = $this->transactionRepository->getBalance($participant);
        $this->assertEquals(500.0, $balance);
    }

    /**
     * @test
     */
    public function it_can_create_a_transaction_with_only_the_basic_required_data()
    {
        $user = factory(User::class)->create();
        factory(Participant::class)->create([
            'user_id' => $user->getKey()
        ]);

        $transaction = $this->transactionRepository->create([
            'user_id' => $user->getKey(),
            'type' => $type = Transaction::REDEEMED,
            'value' => $amount = -100,
            'description' => $description = 'Test'
        ]);

        $hash = app(TransactionHasher::class)->hash([
            'identifier' => $user->getKey(),
            'value' => $amount,
            'description' => $description,
            'type' => $type
        ]);

        $this->assertEquals($user->getKey(), $transaction->user_id);
        $this->assertEquals($type, $transaction->type);
        $this->assertEquals($amount, $transaction->value);
        $this->assertEquals($description, $transaction->description);
        $this->assertEquals($hash, $transaction->hash);
    }

    /**
     * @test
     */
    public function it_will_throw_an_exception_when_value_is_missing()
    {
        $this->expectException(\Exception::class);
        $this->expectExceptionMessage("The `value` was missing or non-numeric.");
        $this->createTransaction([]);
    }

    /**
     * @test
     */
    public function it_will_throw_an_exception_when_value_is_empty()
    {
        $this->expectException(\Exception::class);
        $this->expectExceptionMessage("The `value` was missing or non-numeric.");
        $this->createTransaction([
            'value' => '',
        ]);
    }

    /**
     * @test
     */
    public function it_will_throw_an_exception_when_value_is_non_numeric()
    {
        $this->expectException(\Exception::class);
        $this->expectExceptionMessage("The `value` was missing or non-numeric.");
        $this->createTransaction([
            'value' => 'foo',
        ]);
    }

    /**
     * @test
     */
    public function it_will_throw_an_exception_when_type_is_not_allowed()
    {
        $this->expectException(\Exception::class);
        $this->expectExceptionMessage("The `type` value 'foo' is not allowed.");
        $this->createTransaction([
            'type' => 'foo',
            'value' => '100',
        ]);
    }

    /**
     * @test
     */
    public function it_will_throw_an_exception_when_description_is_missing()
    {
        $this->expectException(\Exception::class);
        $this->expectExceptionMessage("The `description` was missing.");
        $this->createTransaction([
            'type' => Transaction::EARNED,
            'value' => '100',
        ]);
    }

    /**
     * @test
     */
    public function it_will_throw_an_exception_when_description_is_empty()
    {
        $this->expectException(\Exception::class);
        $this->expectExceptionMessage("The `description` was missing.");
        $this->createTransaction([
            'type' => Transaction::EARNED,
            'value' => '100',
            'description' => '',
        ]);
    }

    /**
     * @test
     */
    public function it_will_throw_an_exception_when_user_id_is_empty_and_nobody_is_authenticated()
    {
        $this->expectException(\Exception::class);
        $this->expectExceptionMessage("The `user_id` was missing.");
        $this->transactionRepository->create([
            'type' => Transaction::EARNED,
            'value' => '100',
            'description' => 'Test',
        ]);
    }

    /**
     * @test
     */
    public function it_autofills_valid_defaults_for_optional_valuess()
    {
        $timestamp = '2020-07-06 00:00:00';
        Carbon::setTestNow('2020-07-06 00:00:00');

        $transaction = $this->createTransaction([
            'type' => Transaction::EARNED,
            'value' => '100',
            'description' => 'Test',
        ]);

        $this->assertEquals(0, $transaction->related_id);
        $this->assertEquals(null, $transaction->related_type);
        $this->assertEquals(null, $transaction->related_name);
        $this->assertEquals($timestamp, $transaction->tax_date->format('Y-m-d H:i:s'));
        $this->assertEquals($timestamp, $transaction->transaction_date->format('Y-m-d H:i:s'));
    }

    /**
     * @test
     */
    public function it_can_parse_the_tax_date()
    {
        $timestamp = '2020-07-06 00:00:00';
        Carbon::setTestNow('2020-07-06 00:00:00');

        $transaction = $this->createTransaction([
            'type' => Transaction::EARNED,
            'value' => '100',
            'description' => 'Test',
            'tax_date' => $timestamp
        ]);

        $this->assertEquals($timestamp, $transaction->tax_date->format('Y-m-d H:i:s'));
    }

    /**
     * @test
     */
    public function it_can_parse_the_transaction_date()
    {
        $timestamp = '2020-07-06 00:00:00';
        Carbon::setTestNow('2020-07-06 00:00:00');

        $transaction = $this->createTransaction([
            'type' => Transaction::EARNED,
            'value' => '100',
            'description' => 'Test',
            'transaction_date' => $timestamp
        ]);

        $this->assertEquals($timestamp, $transaction->transaction_date->format('Y-m-d H:i:s'));
    }

    /**
     * @test
     */
    public function it_will_throw_an_exception_when_carbon_cannot_parse_the_tax_date()
    {
        $this->expectException(InvalidFormatException::class);

        $this->createTransaction([
            'type' => Transaction::EARNED,
            'value' => '100',
            'description' => 'Test',
            'tax_date' => '123'
        ]);
    }

    /**
     * @test
     */
    public function it_can_create_a_transaction_using_a_related_model()
    {
        $user = factory(User::class)->create();
        factory(Participant::class)->create([
            'user_id' => $user->getKey()
        ]);

        $transaction = $this->transactionRepository->createUsingRelatedModel([
            'user_id' => $user->getKey(),
            'type' => Transaction::REDEEMED,
            'value' => -100,
            'description' => 'Test'
        ], $user);

        $this->assertEquals($user->getKey(), $transaction->related_id);
        $this->assertEquals(get_class($user), $transaction->related_type);
        $this->assertEquals(strtoupper(class_basename($user)), $transaction->related_name);
    }

    /**
     * @test
     */
    public function it_can_create_a_transaction_of_a_specific_type_using_a_related_model()
    {
        $user = factory(User::class)->create();
        factory(Participant::class)->create([
            'user_id' => $user->getKey()
        ]);

        $transaction = $this->transactionRepository->createTransactionOfType(Transaction::REDEEMED, [
            'user_id' => $user->getKey(),
            'value' => -100,
            'description' => 'Test'
        ], $user);

        $this->assertEquals(Transaction::REDEEMED, $transaction->type);
        $this->assertEquals($user->getKey(), $transaction->related_id);
        $this->assertEquals(get_class($user), $transaction->related_type);
        $this->assertEquals(strtoupper(class_basename($user)), $transaction->related_name);
    }

    /**
     * @test
     */
    public function it_can_create_a_transaction_of_a_specific_type_without_a_related_model()
    {
        $user = factory(User::class)->create();
        factory(Participant::class)->create([
            'user_id' => $user->getKey()
        ]);

        $transaction = $this->transactionRepository->createTransactionOfType(Transaction::REDEEMED, [
            'user_id' => $user->getKey(),
            'value' => -100,
            'description' => 'Test'
        ]);

        $this->assertEquals(Transaction::REDEEMED, $transaction->type);
        $this->assertEquals(0, $transaction->related_id);
        $this->assertEquals(null, $transaction->related_type);
        $this->assertEquals(null, $transaction->related_name);
    }

    /**
     * @test
     */
    public function it_can_create_an_earned_transaction_using_a_related_model()
    {
        $user = factory(User::class)->create();
        factory(Participant::class)->create([
            'user_id' => $user->getKey()
        ]);

        $transaction = $this->transactionRepository->createEarnedTransaction([
            'user_id' => $user->getKey(),
            'value' => -100,
            'description' => 'Test'
        ], $user);

        $this->assertEquals(Transaction::EARNED, $transaction->type);
        $this->assertEquals($user->getKey(), $transaction->related_id);
        $this->assertEquals(get_class($user), $transaction->related_type);
        $this->assertEquals(strtoupper(class_basename($user)), $transaction->related_name);
    }

    /**
     * @test
     */
    public function it_can_create_an_earned_transaction_without_a_related_model()
    {
        $user = factory(User::class)->create();
        factory(Participant::class)->create([
            'user_id' => $user->getKey()
        ]);

        $transaction = $this->transactionRepository->createEarnedTransaction([
            'user_id' => $user->getKey(),
            'value' => -100,
            'description' => 'Test'
        ]);

        $this->assertEquals(Transaction::EARNED, $transaction->type);
        $this->assertEquals(0, $transaction->related_id);
        $this->assertEquals(null, $transaction->related_type);
        $this->assertEquals(null, $transaction->related_name);
    }

    /**
     * @test
     */
    public function it_can_create_a_redeem_transaction_using_a_related_model()
    {
        $user = factory(User::class)->create();
        factory(Participant::class)->create([
            'user_id' => $user->getKey()
        ]);

        $transaction = $this->transactionRepository->createRedeemTransaction([
            'user_id' => $user->getKey(),
            'value' => -100,
            'description' => 'Test'
        ], $user);

        $this->assertEquals(Transaction::REDEEMED, $transaction->type);
        $this->assertEquals($user->getKey(), $transaction->related_id);
        $this->assertEquals(get_class($user), $transaction->related_type);
        $this->assertEquals(strtoupper(class_basename($user)), $transaction->related_name);
    }

    /**
     * @test
     */
    public function it_can_create_a_redeem_transaction_without_a_related_model()
    {
        $user = factory(User::class)->create();
        factory(Participant::class)->create([
            'user_id' => $user->getKey()
        ]);

        $transaction = $this->transactionRepository->createRedeemTransaction([
            'user_id' => $user->getKey(),
            'value' => -100,
            'description' => 'Test'
        ]);

        $this->assertEquals(Transaction::REDEEMED, $transaction->type);
        $this->assertEquals(0, $transaction->related_id);
        $this->assertEquals(null, $transaction->related_type);
        $this->assertEquals(null, $transaction->related_name);
    }

    /**
     * @test
     */
    public function it_can_create_a_cancelled_transaction_using_a_related_model()
    {
        $user = factory(User::class)->create();
        factory(Participant::class)->create([
            'user_id' => $user->getKey()
        ]);

        $transaction = $this->transactionRepository->createCancelledTransaction([
            'user_id' => $user->getKey(),
            'value' => -100,
            'description' => 'Test'
        ], $user);

        $this->assertEquals(Transaction::CANCELLED, $transaction->type);
        $this->assertEquals($user->getKey(), $transaction->related_id);
        $this->assertEquals(get_class($user), $transaction->related_type);
        $this->assertEquals(strtoupper(class_basename($user)), $transaction->related_name);
    }

    /**
     * @test
     */
    public function it_can_create_a_cancelled_transaction_without_a_related_model()
    {
        $user = factory(User::class)->create();
        factory(Participant::class)->create([
            'user_id' => $user->getKey()
        ]);

        $transaction = $this->transactionRepository->createCancelledTransaction([
            'user_id' => $user->getKey(),
            'value' => -100,
            'description' => 'Test'
        ]);

        $this->assertEquals(Transaction::CANCELLED, $transaction->type);
        $this->assertEquals(0, $transaction->related_id);
        $this->assertEquals(null, $transaction->related_type);
        $this->assertEquals(null, $transaction->related_name);
    }

    /**
     * @test
     */
    public function it_can_create_an_expired_transaction_using_a_related_model()
    {
        $user = factory(User::class)->create();
        factory(Participant::class)->create([
            'user_id' => $user->getKey()
        ]);

        $transaction = $this->transactionRepository->createExpiredTransaction([
            'user_id' => $user->getKey(),
            'value' => -100,
            'description' => 'Test'
        ], $user);

        $this->assertEquals(Transaction::EXPIRED, $transaction->type);
        $this->assertEquals($user->getKey(), $transaction->related_id);
        $this->assertEquals(get_class($user), $transaction->related_type);
        $this->assertEquals(strtoupper(class_basename($user)), $transaction->related_name);
    }

    /**
     * @test
     */
    public function it_can_create_an_expired_transaction_without_a_related_model()
    {
        $user = factory(User::class)->create();
        factory(Participant::class)->create([
            'user_id' => $user->getKey()
        ]);

        $transaction = $this->transactionRepository->createExpiredTransaction([
            'user_id' => $user->getKey(),
            'value' => -100,
            'description' => 'Test'
        ]);

        $this->assertEquals(Transaction::EXPIRED, $transaction->type);
        $this->assertEquals(0, $transaction->related_id);
        $this->assertEquals(null, $transaction->related_type);
        $this->assertEquals(null, $transaction->related_name);
    }

    /**
     * @test
     */
    public function it_can_create_a_transaction_using_the_logged_in_user()
    {
        $user = $this->loginAsAdmin();

        factory(Participant::class)->create([
            'user_id' => $user->getKey()
        ]);

        $transaction = $this->transactionRepository->createEarnedTransaction([
            'value' => -100,
            'description' => 'Test'
        ]);

        $this->assertEquals($user->getKey(), $transaction->user_id);
    }

    /**
     * @test
     */
    public function it_will_throw_an_exception_when_carbon_cannot_parse_the_transaction_date()
    {
        $this->expectException(InvalidFormatException::class);

        $this->createTransaction([
            'type' => Transaction::EARNED,
            'value' => '100',
            'description' => 'Test',
            'transaction_date' => '123'
        ]);
    }

    /**
     * Create a transaction with overrides.
     *
     * @param array $overrides
     *
     * @return Transaction|\Illuminate\Database\Eloquent\Model
     * @throws \Exception
     */
    protected function createTransaction(array $overrides = [])
    {
        $user = factory(User::class)->create();
        factory(Participant::class)->create([
            'user_id' => $user->getKey()
        ]);

        return $this->transactionRepository->create(array_merge([
            'user_id' => $user->getKey(),
        ], $overrides));
    }
}
