<?php

namespace Tests\Feature;

use App\Enums\ProviderWithdrawalStatusEnum;
use App\Enums\UserTypeEnum;
use App\Exceptions\BankAccountException;
use App\Models\Bank;
use App\Models\Provider;
use App\Models\ProviderWithdrawal;
use App\Models\User;
use App\Services\ProviderBankAccountService;
use App\Services\ProviderWithdrawalService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Client\Factory;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;

class ProviderBankAccountTest extends TestCase
{
    use RefreshDatabase;

    private ProviderBankAccountService $service;

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

        $this->fakePagarme();

        $this->service = app(ProviderBankAccountService::class);

        Bank::query()->create(['code' => '260', 'name' => 'Nu Pagamentos S.A.', 'active' => true]);
        Bank::query()->create(['code' => '001', 'name' => 'Banco do Brasil S.A.', 'active' => true]);
    }

    /**
     * Regressao: com a chave de idempotencia derivada do payload, voltar para a conta A
     * reenviava a chave da primeira troca e o Pagar.me nao aplicava a alteracao.
     */
    public function test_alternar_conta_principal_a_b_a_usa_chaves_de_idempotencia_distintas(): void
    {
        $provider = $this->makeProvider();

        $accountA = $this->service->create($provider, $this->accountData('260', '1111', '111111'));
        $accountB = $this->service->create($provider, $this->accountData('001', '2222', '222222'));

        $this->assertTrue($accountA->is_primary, 'a primeira conta cadastrada deve nascer principal');
        $this->assertFalse($accountB->is_primary, 'a segunda conta nao pode virar principal sozinha');

        $this->service->update($provider, $accountB->id, ['is_primary' => true]);
        $this->service->update($provider, $accountA->id, ['is_primary' => true]);

        $keys = $this->defaultBankAccountIdempotencyKeys();

        $this->assertCount(3, $keys, 'cada troca/edicao da conta principal precisa chegar ao Pagar.me');
        $this->assertCount(3, array_unique($keys), 'as chaves de idempotencia precisam ser distintas: '.implode(' | ', $keys));

        $provider->refresh();

        $this->assertSame('111111', data_get($provider->recipient_default_bank_account, 'account_number'));
        $this->assertTrue($provider->primaryBankAccount()->first()->is($accountA));
    }

    public function test_conta_secundaria_nao_dispara_chamada_ao_pagarme(): void
    {
        $provider = $this->makeProvider();

        $this->service->create($provider, $this->accountData('260', '1111', '111111'));

        $callsAfterFirstAccount = count($this->defaultBankAccountIdempotencyKeys());

        $secondary = $this->service->create($provider, $this->accountData('001', '2222', '222222'));

        $this->service->update($provider, $secondary->id, ['branch_number' => '3333']);

        $this->assertSame(
            $callsAfterFirstAccount,
            count($this->defaultBankAccountIdempotencyKeys()),
            'criar ou editar conta nao principal e operacao local',
        );
    }

    public function test_editar_conta_principal_ressincroniza_com_o_pagarme(): void
    {
        $provider = $this->makeProvider();

        $account = $this->service->create($provider, $this->accountData('260', '1111', '111111'));

        $before = count($this->defaultBankAccountIdempotencyKeys());

        $updated = $this->service->update($provider, $account->id, ['account_number' => '999999']);

        $this->assertCount($before + 1, $this->defaultBankAccountIdempotencyKeys());
        $this->assertTrue($updated->isSyncedWithGateway());

        $provider->refresh();

        $this->assertSame('999999', data_get($provider->recipient_default_bank_account, 'account_number'));
    }

    public function test_marcar_como_principal_na_edicao_troca_a_conta_do_recebedor(): void
    {
        $provider = $this->makeProvider();

        $accountA = $this->service->create($provider, $this->accountData('260', '1111', '111111'));
        $accountB = $this->service->create($provider, $this->accountData('001', '2222', '222222'));

        $updated = $this->service->update($provider, $accountB->id, [
            'branch_number' => '3333',
            'is_primary'    => true,
        ]);

        $this->assertTrue($updated->is_primary);
        $this->assertFalse($accountA->fresh()->is_primary, 'a conta anterior precisa deixar de ser principal');

        $provider->refresh();

        $this->assertSame('222222', data_get($provider->recipient_default_bank_account, 'account_number'));
        $this->assertSame('3333', data_get($provider->recipient_default_bank_account, 'branch_number'));
    }

    public function test_nao_permite_excluir_a_conta_principal(): void
    {
        $provider = $this->makeProvider();

        $primary = $this->service->create($provider, $this->accountData('260', '1111', '111111'));

        $this->service->create($provider, $this->accountData('001', '2222', '222222'));

        $this->expectException(BankAccountException::class);
        $this->expectExceptionMessage(__('messages.bank_account_primary_requires_replacement'));

        $this->service->delete($provider, $primary->id);
    }

    public function test_nao_permite_excluir_a_ultima_conta(): void
    {
        $provider = $this->makeProvider();

        $account = $this->service->create($provider, $this->accountData('260', '1111', '111111'));

        $this->expectException(BankAccountException::class);
        $this->expectExceptionMessage(__('messages.bank_account_last_cannot_be_deleted'));

        $this->service->delete($provider, $account->id);
    }

    public function test_nao_permite_alterar_contas_com_saque_em_andamento(): void
    {
        $provider = $this->makeProvider();

        $primary   = $this->service->create($provider, $this->accountData('260', '1111', '111111'));
        $secondary = $this->service->create($provider, $this->accountData('001', '2222', '222222'));

        ProviderWithdrawal::query()->create([
            'provider_id'     => $provider->id,
            'recipient_id'    => $provider->recipient_id,
            'idempotency_key' => 'withdrawal-test-'.$provider->id,
            'gross_amount'    => 100,
            'net_amount'      => 96,
            'status'          => ProviderWithdrawalStatusEnum::PROCESSING,
        ]);

        try {
            $this->service->update($provider, $secondary->id, ['is_primary' => true]);

            $this->fail('trocar a conta principal deveria ser bloqueado durante um saque');
        } catch (BankAccountException $e) {
            $this->assertSame(__('messages.bank_account_blocked_by_withdrawal'), $e->getMessage());
        }

        $this->assertTrue($provider->primaryBankAccount()->first()->is($primary));

        $this->expectException(BankAccountException::class);
        $this->expectExceptionMessage(__('messages.bank_account_blocked_by_withdrawal'));

        $this->service->delete($provider, $secondary->id);
    }

    public function test_conta_divergente_no_gateway_e_reenviada_antes_do_saque(): void
    {
        $provider = $this->makeProvider();

        $account = $this->service->create($provider, $this->accountData('260', '1111', '111111'));

        // o recebedor esta com outra conta (ex: PATCH aplicado e commit local revertido)
        $this->fakePagarme([
            '*/recipients/re_*' => Http::response([
                'id'                   => $provider->recipient_id,
                'default_bank_account' => ['bank' => '001', 'branch_number' => '9999', 'account_number' => '888888', 'account_check_digit' => '7', 'type' => 'checking'],
            ], 200),
        ]);

        $this->service->ensureGatewayMatchesPrimary($provider);

        $this->assertCount(
            1,
            $this->defaultBankAccountIdempotencyKeys(),
            'a conta principal local precisa ser reenviada quando o gateway diverge',
        );

        $this->assertTrue($account->fresh()->isSyncedWithGateway());
    }

    public function test_saque_bloqueado_quando_nao_e_possivel_sincronizar_a_conta(): void
    {
        $provider = $this->makeProvider();

        $this->service->create($provider, $this->accountData('260', '1111', '111111'));

        $this->fakePagarme([
            '*/recipients/*/default-bank-account' => Http::response(['message' => 'erro'], 500),
            '*/recipients/re_*'                   => Http::response([
                'id'                   => $provider->recipient_id,
                'default_bank_account' => ['bank' => '001', 'branch_number' => '9999', 'account_number' => '888888', 'account_check_digit' => '7', 'type' => 'checking'],
            ], 200),
        ]);

        $this->expectException(BankAccountException::class);
        $this->expectExceptionMessage(__('messages.provider_requires_synced_bank_account'));

        app(ProviderWithdrawalService::class)->requestWithdrawal($provider->fresh());
    }

    //

    /**
     * Os stubs sao avaliados na ordem do array, entao os overrides vem primeiro.
     * Atencao: cada chamada tambem zera as requisicoes ja gravadas.
     */
    private function fakePagarme(array $overrides = []): void
    {
        // Http::fake() acumula stubs e os antigos tem precedencia, entao a factory
        // precisa ser descartada para um teste conseguir sobrescrever o stub do setUp
        Http::clearResolvedInstances();
        $this->app->forgetInstance(Factory::class);
        Http::preventStrayRequests();

        $defaults = [
            '*/recipients/*/default-bank-account' => Http::response(['id' => 're_fake'], 200),
            '*/recipients*'                       => Http::response(['id' => 're_fake'], 200),
            '*'                                   => Http::response([], 200),
        ];

        // o "+" mantem os overrides na frente e sem colisao de chave com os defaults
        Http::fake($overrides + array_diff_key($defaults, $overrides));
    }

    private function makeProvider(): Provider
    {
        $user = User::query()->create([
            'name'     => 'Prestador Teste',
            'email'    => 'prestador'.uniqid().'@teste.com',
            'password' => 'secret',
            'type'     => UserTypeEnum::PROVIDER->value,
        ]);

        return Provider::query()->create([
            'user_id'      => $user->id,
            'document'     => '06767310905',
            'birth_date'   => '1990-01-01',
            'recipient_id' => 're_'.uniqid(),
        ]);
    }

    private function accountData(string $bankCode, string $branch, string $account): array
    {
        return [
            'bank_code'           => $bankCode,
            'type'                => 'checking',
            'branch_number'       => $branch,
            'branch_check_digit'  => '1',
            'account_number'      => $account,
            'account_check_digit' => '2',
        ];
    }

    /**
     * @return array<int, string>
     */
    private function defaultBankAccountIdempotencyKeys(): array
    {
        return Http::recorded()
            ->filter(fn ($pair) => str_contains($pair[0]->url(), 'default-bank-account'))
            ->map(fn ($pair) => $pair[0]->header('Idempotency-Key')[0] ?? '')
            ->values()
            ->all();
    }

}
