<?php

declare(strict_types=1);

namespace Tests\Feature\Filament;

use App\Filament\Resources\Credits\CreditResource;
use App\Filament\Resources\HeldPayments\HeldPaymentResource;
use App\Filament\Resources\HeldPayments\Pages\ListHeldPayments;
use App\Filament\Resources\HeldPayments\Pages\ViewHeldPayment;
use App\Filament\Resources\Payments\PaymentResource;
use App\Models\Client;
use App\Models\Company;
use App\Models\Credit;
use App\Models\HeldPayment;
use App\Models\Installment;
use App\Models\Payment;
use App\Models\Subscription;
use App\Models\User;
use App\Services\Payments\HeldPaymentReviewService;
use App\Services\PaymentSyncService;
use App\Support\Format;
use Filament\Facades\Filament;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Database\QueryException;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Exceptions;
use Illuminate\Support\Str;
use Livewire\Livewire;
use Mockery;
use Mockery\MockInterface;
use PDOException;
use PHPUnit\Framework\Attributes\Test;
use RuntimeException;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

class HeldPaymentResourceTest extends TestCase
{
    use DatabaseTransactions;

    private Company $company;

    private User $admin;

    private User $collector;

    private Credit $credit;

    private HeldPayment $held;

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

        foreach (['super_admin', 'admin', 'supervisor', 'collector'] as $role) {
            Role::firstOrCreate(['name' => $role, 'guard_name' => 'web']);
        }

        $this->company = Company::factory()->create();
        $this->admin = User::factory()->create(['company_id' => $this->company->id]);
        $this->admin->assignRole('admin');
        $this->collector = User::factory()->create(['company_id' => $this->company->id]);
        $this->collector->assignRole('collector');

        $this->credit = $this->activeCredit($this->company, $this->collector);
        $this->held = $this->held();
    }

    private function activeCredit(Company $company, User $collector, ?string $clientName = null): Credit
    {
        $client = Client::factory()->create(array_filter([
            'company_id' => $company->id,
            'name' => $clientName,
        ]));
        $credit = Credit::factory()->create([
            'company_id' => $company->id,
            'client_id' => $client->id,
            'collector_user_id' => $collector->id,
            'created_by_user_id' => $collector->id,
            'status' => Credit::STATUS_ACTIVE,
            'amount' => 60_000,
            'installments_count' => 1,
            'periodicity' => 'monthly',
            'start_date' => now()->subDays(40),
            'due_date' => now()->addDays(5),
        ]);
        Installment::factory()->create([
            'company_id' => $company->id,
            'credit_id' => $credit->id,
            'installment_number' => 1,
            'status' => 'pending',
            'amount_paid' => 0,
            'total_amount' => 60_000,
            'principal_amount' => 48_000,
            'interest_amount' => 12_000,
            'due_date' => now()->addDays(5),
        ]);

        return $credit;
    }

    /** @param  array<string, mixed>  $overrides */
    private function held(array $overrides = []): HeldPayment
    {
        return HeldPayment::create(array_merge([
            'company_id' => $this->company->id,
            'idempotency_key' => (string) Str::uuid(),
            'credit_id' => $this->credit->id,
            'captured_by_user_id' => $this->collector->id,
            'synced_by_user_id' => $this->collector->id,
            'amount' => 10_000,
            'payment_method' => 'cash',
            'payment_date' => now()->subDays(20)->toDateString(),
            'reason' => HeldPayment::REASON_STALE,
            'payload' => [],
            'status' => HeldPayment::STATUS_PENDING,
        ], $overrides));
    }

    private function actingAsAdminInPanel(): void
    {
        $this->actingAs($this->admin);
        Filament::setCurrentPanel(Filament::getDefaultPanel());
    }

    /** El servicio real salvo `$method`: así la vista sigue calculando si se puede aprobar. */
    private function reviewThrows(string $method, \Throwable $e): void
    {
        $mock = Mockery::mock(HeldPaymentReviewService::class, [app(PaymentSyncService::class)])->makePartial();
        $mock->shouldReceive($method)->andThrow($e);
        $this->app->instance(HeldPaymentReviewService::class, $mock);
    }

    /** Un error de MySQL cuyo mensaje trae el SQL con un valor del cobro. */
    private function queryException(): QueryException
    {
        $pdo = new PDOException('SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded');
        $pdo->errorInfo = ['HY000', 1205, 'Lock wait timeout exceeded'];

        return new QueryException('mysql', 'update `held_payments` set `device_id` = ? where `id` = ?', ['valor-privado', 1], $pdo);
    }

    private function assertReportedWithoutData(): void
    {
        Exceptions::assertReportedCount(1);
        Exceptions::assertReported(fn (RuntimeException $e) => str_contains($e->getMessage(), 'datos del cobro omitidos')
            && ! str_contains($e->getMessage(), 'valor-privado'));
        Exceptions::assertNotReported(QueryException::class);
    }

    #[Test]
    public function solo_admin_y_super_admin_revisan_cobros_retenidos(): void
    {
        $supervisor = User::factory()->create(['company_id' => $this->company->id]);
        $supervisor->assignRole('supervisor');
        $otraEmpresa = User::factory()->create(['company_id' => Company::factory()->create()->id]);
        $otraEmpresa->assignRole('admin');

        $this->assertTrue($this->admin->can('viewAny', HeldPayment::class));
        $this->assertTrue($this->admin->can('approve', $this->held));
        $this->assertTrue($this->admin->can('reject', $this->held));
        $this->assertFalse($this->collector->can('viewAny', HeldPayment::class));
        $this->assertFalse($supervisor->can('viewAny', HeldPayment::class));
        $this->assertFalse($otraEmpresa->can('view', $this->held));
        $this->assertFalse($otraEmpresa->can('approve', $this->held));
        $this->assertFalse($otraEmpresa->can('reject', $this->held));
        $this->assertFalse($this->admin->can('create', HeldPayment::class));
        $this->assertFalse($this->admin->can('delete', $this->held));
    }

    #[Test]
    public function el_admin_ve_la_bandeja_y_aprueba_desde_la_vista(): void
    {
        $this->actingAsAdminInPanel();

        Livewire::test(ListHeldPayments::class)
            ->assertOk()
            ->assertCanSeeTableRecords([$this->held])
            ->assertTableColumnHidden('company.name');

        Livewire::test(ViewHeldPayment::class, ['record' => $this->held->getRouteKey()])
            ->assertOk()
            ->mountAction('approve')
            ->assertMountedActionModalSee([
                'Se aplicarán '.Format::money(10_000).' al crédito #'.$this->credit->id.' de '.$this->credit->client->name,
                'a nombre de '.$this->collector->name,
                'la cuota pendiente más antigua del crédito',
            ])
            // Sin pagos parecidos no hay nada de qué advertir.
            ->assertMountedActionModalDontSee('Atención')
            ->callMountedAction();

        $held = $this->held->fresh();
        $this->assertSame(HeldPayment::STATUS_APPROVED, $held->status);
        $this->assertDatabaseHas('payments', ['idempotency_key' => $this->held->idempotency_key]);

        // Resuelto: el pago creado enlaza a su vista y ya no se invita a rechazar.
        Livewire::test(ViewHeldPayment::class, ['record' => $this->held->getRouteKey()])
            ->assertOk()
            ->assertSee("Pago #{$held->payment_id}")
            ->assertSee(PaymentResource::getUrl('view', ['record' => $held->payment_id]), escape: false)
            ->assertDontSee('rechaza el retenido')
            ->assertActionHidden('approve')
            ->assertActionHidden('reject');
    }

    #[Test]
    public function la_bandeja_no_muestra_retenidos_de_otra_empresa(): void
    {
        $otra = Company::factory()->create();
        $ajeno = HeldPayment::create([
            'company_id' => $otra->id,
            'idempotency_key' => (string) Str::uuid(),
            'amount' => 10_000,
            'payment_date' => now()->subDays(20)->toDateString(),
            'reason' => HeldPayment::REASON_STALE,
            'payload' => [],
            'status' => HeldPayment::STATUS_PENDING,
        ]);

        $this->actingAsAdminInPanel();

        Livewire::test(ListHeldPayments::class)
            ->assertCanSeeTableRecords([$this->held])
            ->assertCanNotSeeTableRecords([$ajeno]);

        // El contador del menú tampoco cuenta los ajenos.
        $this->assertSame('1', HeldPaymentResource::getNavigationBadge());
    }

    #[Test]
    public function el_super_admin_ve_todas_las_empresas_y_aprueba_en_la_del_retenido(): void
    {
        // Una marca propia en los clientes: la BD dev compartida puede traer otros pendientes.
        $marca = 'Marca'.Str::random(10);
        $this->credit->client->update(['name' => "{$marca} Uno"]);

        $otra = Company::factory()->create();
        $cobradorOtra = User::factory()->create(['company_id' => $otra->id]);
        $cobradorOtra->assignRole('collector');
        $creditoOtra = $this->activeCredit($otra, $cobradorOtra, "{$marca} Dos");
        $ajeno = $this->held([
            'company_id' => $otra->id,
            'credit_id' => $creditoOtra->id,
            'captured_by_user_id' => $cobradorOtra->id,
            'synced_by_user_id' => $cobradorOtra->id,
        ]);

        $super = User::factory()->create(['company_id' => null]);
        $super->assignRole('super_admin');
        $this->actingAs($super);
        Filament::setCurrentPanel(Filament::getDefaultPanel());

        Livewire::test(ListHeldPayments::class)
            ->searchTable($marca)
            ->assertCanSeeTableRecords([$this->held, $ajeno])
            ->assertTableColumnVisible('company.name')
            ->assertSee($this->company->name)
            ->assertSee($otra->name);

        $this->assertSame(
            (string) HeldPayment::query()->withoutGlobalScopes()->pending()->count(),
            HeldPaymentResource::getNavigationBadge(),
        );

        Livewire::test(ViewHeldPayment::class, ['record' => $ajeno->getRouteKey()])
            ->assertOk()
            ->assertSee($otra->name)
            ->callAction('approve');

        $this->assertSame(HeldPayment::STATUS_APPROVED, $ajeno->fresh()->status);
        $this->assertSame($super->id, $ajeno->fresh()->resolved_by_user_id);
        $this->assertDatabaseHas('payments', [
            'idempotency_key' => $ajeno->idempotency_key,
            'company_id' => $otra->id,
            'credit_id' => $creditoOtra->id,
        ]);
    }

    #[Test]
    public function las_paginas_cargan_en_el_panel_y_el_admin_ajeno_no_ve_el_retenido(): void
    {
        Cache::flush(); // hasActiveSubscription cachea por empresa
        Subscription::factory()->active()->create([
            'company_id' => $this->company->id,
            'ends_at' => now()->addYear(),
        ]);
        Filament::setCurrentPanel(Filament::getDefaultPanel());

        $this->actingAs($this->admin)
            ->get(HeldPaymentResource::getUrl('index'))
            ->assertOk()
            ->assertSee('Cobros en revisión')
            ->assertSee($this->collector->name);

        $this->actingAs($this->admin)
            ->get(HeldPaymentResource::getUrl('view', ['record' => $this->held]))
            ->assertOk()
            ->assertSee('Posibles duplicados')
            ->assertSee('Aprobar y aplicar');

        $otra = Company::factory()->create();
        Subscription::factory()->active()->create(['company_id' => $otra->id, 'ends_at' => now()->addYear()]);
        $adminAjeno = User::factory()->create(['company_id' => $otra->id]);
        $adminAjeno->assignRole('admin');

        // Sesión nueva: AuthenticateSession cerraría la del admin anterior (otro hash de clave).
        $this->flushSession();

        $this->actingAs($adminAjeno)
            ->get(HeldPaymentResource::getUrl('view', ['record' => $this->held]))
            ->assertNotFound();
    }

    #[Test]
    public function la_vista_dice_si_se_puede_aprobar_y_lista_los_posibles_duplicados(): void
    {
        $duplicado = app(PaymentSyncService::class)->applyToCredit($this->credit, [
            'amount' => 10_000, 'payment_date' => now()->subDays(18)->toDateString(), 'payment_method' => 'cash',
            'device_id' => null, 'offline_created_at' => null, 'latitude' => null, 'longitude' => null,
        ], $this->collector->id, (string) Str::uuid());

        $this->actingAsAdminInPanel();

        Livewire::test(ViewHeldPayment::class, ['record' => $this->held->getRouteKey()])
            ->assertOk()
            ->assertSee('Por qué quedó retenido')
            ->assertSee('Sí: el crédito admite este pago.')
            ->assertSee('Posibles duplicados')
            ->assertSee('rechaza el retenido')
            ->assertSee("Pago #{$duplicado->id}")
            ->assertSee($this->collector->name)
            ->assertSee('Efectivo')
            ->assertSee(CreditResource::getUrl('view', ['record' => $this->credit->id]), escape: false)
            ->assertActionVisible('approve')
            ->assertActionVisible('reject')
            // La confirmación lo repite: es el último paso antes de aplicar dos veces el mismo cobro.
            ->mountAction('approve')
            ->assertMountedActionModalSee("Atención: hay 1 pago(s) parecido(s) en este crédito (#{$duplicado->id}); si alguno es este mismo cobro, rechaza en vez de aprobar.");
    }

    #[Test]
    public function retenido_como_posible_duplicado_la_confirmacion_lo_advierte_aunque_el_otro_pago_ya_no_este(): void
    {
        // El pago parecido se anuló después: possibleDuplicates() ya no lo trae, pero
        // el motivo sigue diciendo por qué se retuvo.
        $held = $this->held([
            'payment_date' => now()->toDateString(),
            'reason' => HeldPayment::REASON_POSSIBLE_DUPLICATE,
            'reason_detail' => 'Ya hay un pago de 10000.00 en este crédito el '.now()->toDateString().' (pago #999).',
        ]);

        $this->actingAsAdminInPanel();

        Livewire::test(ViewHeldPayment::class, ['record' => $held->getRouteKey()])
            ->assertOk()
            ->mountAction('approve')
            ->assertMountedActionModalSee([
                'Atención: se retuvo como posible duplicado',
                '(pago #999)',
                'rechaza en vez de aprobar',
            ]);
    }

    #[Test]
    public function sin_monto_ni_credito_enlazado_la_vista_dice_lo_que_mando_el_telefono(): void
    {
        $ilegible = $this->held([
            'credit_id' => null,
            'amount' => null,
            'payment_method' => null,
            'reason' => HeldPayment::REASON_INVALID,
            'reason_detail' => 'El campo amount es obligatorio.',
            'payload' => ['credit_id' => 4321],
        ]);

        $this->actingAsAdminInPanel();

        Livewire::test(ListHeldPayments::class)
            ->assertCanSeeTableRecords([$ilegible])
            ->assertSee('Ilegible');

        Livewire::test(ViewHeldPayment::class, ['record' => $ilegible->getRouteKey()])
            ->assertOk()
            ->assertSee('Ilegible')
            ->assertSee('No disponible (el teléfono indicó #4321)')
            ->assertDontSee(Format::money(0))
            ->assertActionHidden('approve');
    }

    #[Test]
    public function si_no_se_reconocio_al_capturador_no_se_muestra_como_cobrador_a_quien_subio(): void
    {
        // hold() lo guarda a nombre de quien sube cuando no reconoce al capturador.
        $noConfirmado = $this->held([
            'reason' => HeldPayment::REASON_INVALID,
            'reason_detail' => 'El usuario que capturó el cobro no pertenece a la empresa o ya no tiene rol en la app.',
            'payload' => ['captured_by_user_id' => 987654],
        ]);

        $this->actingAsAdminInPanel();

        Livewire::test(ListHeldPayments::class)
            ->assertCanSeeTableRecords([$noConfirmado])
            ->assertSee('No confirmado (el teléfono indicó #987654)');

        Livewire::test(ViewHeldPayment::class, ['record' => $noConfirmado->getRouteKey()])
            ->assertOk()
            ->assertSee('No confirmado (el teléfono indicó #987654)')
            ->assertActionHidden('approve')
            ->assertActionVisible('reject');
    }

    #[Test]
    public function si_se_borro_el_usuario_que_cobro_se_muestra_usuario_eliminado(): void
    {
        // Al borrar el usuario la FK queda en null; el id que mandó el teléfono sigue en payload.
        $huerfano = $this->held([
            'captured_by_user_id' => null,
            'payload' => ['captured_by_user_id' => 987654],
        ]);

        $this->actingAsAdminInPanel();

        Livewire::test(ListHeldPayments::class)
            ->assertCanSeeTableRecords([$huerfano])
            ->assertSee('Usuario eliminado')
            ->assertDontSee('No confirmado');

        Livewire::test(ViewHeldPayment::class, ['record' => $huerfano->getRouteKey()])
            ->assertOk()
            ->assertSee('Usuario eliminado')
            ->assertDontSee('No confirmado')
            ->assertSee('ya no existe')
            ->assertActionHidden('approve');
    }

    #[Test]
    public function un_retenido_que_no_se_puede_aplicar_explica_por_que_y_no_ofrece_aprobar(): void
    {
        $detalle = 'El usuario que capturó el cobro no pertenece a la empresa o ya no tiene rol en la app.';
        $bloqueado = $this->held(['reason' => HeldPayment::REASON_INVALID, 'reason_detail' => $detalle]);

        $this->actingAsAdminInPanel();

        // En la bandeja el motivo va con su detalle: `invalid` no siempre es "ilegible".
        Livewire::test(ListHeldPayments::class)
            ->assertCanSeeTableRecords([$bloqueado])
            ->assertSee('No se puede aplicar tal como llegó')
            ->assertSee($detalle);

        Livewire::test(ViewHeldPayment::class, ['record' => $bloqueado->getRouteKey()])
            ->assertOk()
            ->assertSee('Este cobro no se puede aplicar: '.$detalle)
            ->assertSee('Posibles duplicados')
            ->assertSee('Ninguno')
            ->assertActionHidden('approve')
            ->assertActionVisible('reject');
    }

    #[Test]
    public function forzar_la_aprobacion_de_un_retenido_bloqueado_no_crea_el_pago(): void
    {
        $bloqueado = $this->held(['reason' => HeldPayment::REASON_INVALID, 'reason_detail' => 'Capturador desconocido.']);
        // La pantalla ni siquiera llega al servicio (que igual se negaría).
        $mock = Mockery::mock(HeldPaymentReviewService::class, [app(PaymentSyncService::class)])->makePartial();
        $mock->shouldNotReceive('approve');
        $this->app->instance(HeldPaymentReviewService::class, $mock);

        $this->actingAsAdminInPanel();

        // Un cliente que arma la petición a mano, sin pasar por el botón oculto.
        Livewire::test(ViewHeldPayment::class, ['record' => $bloqueado->getRouteKey()])
            ->mountAction('approve')
            ->callMountedAction();

        $this->assertSame(HeldPayment::STATUS_PENDING, $bloqueado->fresh()->status);
        $this->assertSame(0, Payment::withoutGlobalScopes()->where('idempotency_key', $bloqueado->idempotency_key)->count());
    }

    #[Test]
    public function el_admin_rechaza_con_motivo_obligatorio(): void
    {
        $this->actingAsAdminInPanel();

        Livewire::test(ViewHeldPayment::class, ['record' => $this->held->getRouteKey()])
            ->callAction('reject', data: ['resolution_notes' => ''])
            ->assertHasActionErrors(['resolution_notes' => 'required']);

        $this->assertSame(HeldPayment::STATUS_PENDING, $this->held->fresh()->status);

        Livewire::test(ViewHeldPayment::class, ['record' => $this->held->getRouteKey()])
            ->callAction('reject', data: ['resolution_notes' => 'Ya se había cargado a mano'])
            ->assertHasNoActionErrors()
            ->assertNotified('Cobro rechazado');

        $held = $this->held->fresh();
        $this->assertSame(HeldPayment::STATUS_REJECTED, $held->status);
        $this->assertSame('Ya se había cargado a mano', $held->resolution_notes);
        $this->assertSame($this->admin->id, $held->resolved_by_user_id);
        $this->assertDatabaseMissing('payments', ['idempotency_key' => $this->held->idempotency_key]);

        // Ya resuelto: la vista muestra la resolución y no ofrece acciones.
        Livewire::test(ViewHeldPayment::class, ['record' => $this->held->getRouteKey()])
            ->assertOk()
            ->assertSee('Resolución')
            ->assertSee('Ya se había cargado a mano')
            ->assertDontSee('rechaza el retenido')
            ->assertActionHidden('approve')
            ->assertActionHidden('reject');
    }

    #[Test]
    public function un_conflicto_que_el_servicio_ya_reporto_solo_se_avisa(): void
    {
        Exceptions::fake();
        // Un choque con un índice que no es el de idempotencia: el servicio lo reporta y avisa.
        $pdo = new PDOException("SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'valor-privado' for key 'installments.installments_credit_id_installment_number_unique'");
        $pdo->errorInfo = ['23000', 1062, "Duplicate entry 'valor-privado' for key 'installments.installments_credit_id_installment_number_unique'"];
        $choque = new UniqueConstraintViolationException('mysql', 'insert into `installments` (`x`) values (?)', ['valor-privado'], $pdo);
        $this->partialMock(PaymentSyncService::class, function (MockInterface $mock) use ($choque): void {
            $mock->shouldReceive('applyToCredit')->andThrow($choque);
        });

        $this->actingAsAdminInPanel();

        Livewire::test(ViewHeldPayment::class, ['record' => $this->held->getRouteKey()])
            ->callAction('approve')
            ->assertNotified('No se pudo aplicar el cobro por un conflicto de datos. Quedó registrado para revisión técnica; recarga e inténtalo de nuevo.');

        // Una sola vez, la del servicio.
        Exceptions::assertReportedCount(1);
        Exceptions::assertNotReported(UniqueConstraintViolationException::class);
        $this->assertSame(HeldPayment::STATUS_PENDING, $this->held->fresh()->status);
    }

    #[Test]
    public function un_error_inesperado_al_aprobar_se_reporta_sin_datos_y_avisa(): void
    {
        Exceptions::fake();
        $this->reviewThrows('approve', $this->queryException());

        $this->actingAsAdminInPanel();

        Livewire::test(ViewHeldPayment::class, ['record' => $this->held->getRouteKey()])
            ->callAction('approve')
            ->assertNotified('No se pudo aplicar el cobro. Recarga la página e inténtalo de nuevo.');

        $this->assertReportedWithoutData();
        $this->assertSame(HeldPayment::STATUS_PENDING, $this->held->fresh()->status);
    }

    #[Test]
    public function si_la_relectura_tras_un_error_tambien_falla_igual_se_avisa(): void
    {
        Exceptions::fake();
        // La primera lectura del retenido después del fallo (la relectura) cae, como con la base caída.
        $armada = false;
        DB::listen(function (QueryExecuted $query) use (&$armada): void {
            if ($armada && str_starts_with($query->sql, 'select') && str_contains($query->sql, '`held_payments`')) {
                $armada = false;
                throw new RuntimeException('relectura caída');
            }
        });
        $mock = Mockery::mock(HeldPaymentReviewService::class, [app(PaymentSyncService::class)])->makePartial();
        $mock->shouldReceive('approve')->andReturnUsing(function () use (&$armada): never {
            $armada = true;

            throw new RuntimeException('falla al aprobar');
        });
        $this->app->instance(HeldPaymentReviewService::class, $mock);

        $this->actingAsAdminInPanel();

        Livewire::test(ViewHeldPayment::class, ['record' => $this->held->getRouteKey()])
            ->callAction('approve')
            ->assertNotified('No se pudo aplicar el cobro. Recarga la página e inténtalo de nuevo.');

        $this->assertFalse($armada, 'la relectura debió intentarse');
        Exceptions::assertReportedCount(2);
        Exceptions::assertReported(fn (RuntimeException $e) => $e->getMessage() === 'falla al aprobar');
        Exceptions::assertReported(fn (RuntimeException $e) => $e->getMessage() === 'relectura caída');
        $this->assertSame(HeldPayment::STATUS_PENDING, $this->held->fresh()->status);
    }

    #[Test]
    public function un_error_inesperado_al_rechazar_se_reporta_sin_datos_y_avisa(): void
    {
        Exceptions::fake();
        $this->reviewThrows('reject', $this->queryException());

        $this->actingAsAdminInPanel();

        Livewire::test(ViewHeldPayment::class, ['record' => $this->held->getRouteKey()])
            ->callAction('reject', data: ['resolution_notes' => 'Duplicado'])
            ->assertNotified('No se pudo rechazar el cobro. Recarga la página e inténtalo de nuevo.');

        $this->assertReportedWithoutData();
        $this->assertSame(HeldPayment::STATUS_PENDING, $this->held->fresh()->status);
    }
}
