<?php

declare(strict_types=1);

namespace Tests\Feature\Financial;

use App\Models\Client;
use App\Models\Company;
use App\Models\Credit;
use App\Models\Installment;
use App\Models\Payment;
use App\Models\User;
use App\Services\Payments\PaymentInvariantGuard;
use App\Services\Payments\PaymentMaterializer;
use App\Services\Payments\PaymentRegistrar;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use PHPUnit\Framework\Attributes\Test;
use RuntimeException;
use Tests\TestCase;

/**
 * Integration tests for PaymentInvariantGuard.
 *
 * The guard enforces three invariants (A, B, C):
 *
 * A) payment.applied_amount == ROUND(SUM(pivot.applied_amount), 2)
 *    for all non-voided payments
 *
 * B) installment.amount_paid == ROUND(SUM(pivot.applied_amount), 2)
 *    for all installments (joining only non-voided payments)
 *
 * C) No pivot rows may exist that reference a voided payment
 *
 * These tests verify:
 * - Guard passes on a clean, correctly materialized credit
 * - Guard throws RuntimeException when any invariant is violated
 * - Guard passes when a voided payment has had its pivots removed
 */
class PaymentInvariantGuardTest extends TestCase
{
    use DatabaseTransactions;

    private Company $company;

    private User $user;

    private Credit $credit;

    private PaymentInvariantGuard $guard;

    private PaymentRegistrar $registrar;

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

        Event::fake();

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

        $this->credit = Credit::factory()->create([
            'company_id' => $this->company->id,
            'client_id' => Client::factory()->create(['company_id' => $this->company->id])->id,
            'collector_user_id' => $this->user->id,
            'created_by_user_id' => $this->user->id,
            'status' => Credit::STATUS_ACTIVE,
            'amount' => 60_000,
            'installments_count' => 1,
            'due_date' => Carbon::today()->addDays(60),
        ]);

        Installment::factory()->create([
            'company_id' => $this->company->id,
            'credit_id' => $this->credit->id,
            'installment_number' => 1,
            'status' => Installment::STATUS_PENDING,
            'due_date' => Carbon::today()->addDays(30)->toDateString(),
            'principal_amount' => 50_000,
            'interest_amount' => 10_000,
            'total_amount' => 60_000,
            'principal_balance_after' => 60_000,
            'amount_paid' => 0,
        ]);

        $this->guard = app(PaymentInvariantGuard::class);
        $this->registrar = app(PaymentRegistrar::class);
    }

    // ── Clean state ───────────────────────────────────────────────────────────

    #[Test]
    public function guard_passes_when_credit_has_no_payments(): void
    {
        // No payment registered → all installments have 0 amount_paid, no pivots
        $this->guard->assertCreditIntegrity($this->credit->fresh());
        $this->assertTrue(true); // Reached here = no exception
    }

    #[Test]
    public function guard_passes_after_valid_payment_registration(): void
    {
        // PaymentRegistrar already calls the guard internally.
        // Re-calling it manually should also pass.
        $this->registrar->forCredit($this->credit, 30_000, Carbon::today(), $this->user->id);

        $this->guard->assertCreditIntegrity($this->credit->fresh());
        $this->assertTrue(true);
    }

    #[Test]
    public function guard_passes_after_full_payment(): void
    {
        $this->registrar->forCredit($this->credit, 60_000, Carbon::today(), $this->user->id);

        $this->guard->assertCreditIntegrity($this->credit->fresh());
        $this->assertTrue(true);
    }

    #[Test]
    public function guard_passes_after_multiple_sequential_payments(): void
    {
        $this->registrar->forCredit($this->credit, 20_000, Carbon::today(), $this->user->id);
        $this->registrar->forCredit($this->credit, 20_000, Carbon::today(), $this->user->id);
        $this->registrar->forCredit($this->credit, 20_000, Carbon::today(), $this->user->id);

        $this->guard->assertCreditIntegrity($this->credit->fresh());
        $this->assertTrue(true);
    }

    // ── Invariant A violation: payment.applied_amount != SUM(pivots) ─────────

    #[Test]
    public function guard_throws_when_payment_applied_amount_does_not_match_pivot_sum(): void
    {
        $payment = $this->registrar->forCredit($this->credit, 30_000, Carbon::today(), $this->user->id);

        // Corrupt payment.applied_amount directly (without touching pivots)
        DB::table('payments')->where('id', $payment->id)->update(['applied_amount' => 99_999]);

        $this->expectException(RuntimeException::class);
        $this->expectExceptionMessageMatches('/payment\.applied_amount != SUM\(pivot\)/');

        $this->guard->assertCreditIntegrity($this->credit->fresh());
    }

    // ── Invariant B violation: installment.amount_paid != SUM(pivots) ────────

    #[Test]
    public function guard_throws_when_installment_amount_paid_does_not_match_pivot_sum(): void
    {
        $this->registrar->forCredit($this->credit, 30_000, Carbon::today(), $this->user->id);

        $installment = Installment::where('credit_id', $this->credit->id)->first();

        // Corrupt installment.amount_paid directly (without touching pivots)
        DB::table('installments')->where('id', $installment->id)->update(['amount_paid' => 99_999]);

        $this->expectException(RuntimeException::class);
        $this->expectExceptionMessageMatches('/installment\.amount_paid != SUM\(pivot\)/');

        $this->guard->assertCreditIntegrity($this->credit->fresh());
    }

    // ── Invariant C violation: voided payment with pivot entries ─────────────

    #[Test]
    public function guard_throws_when_voided_payment_still_has_pivot_entries(): void
    {
        $payment = $this->registrar->forCredit($this->credit, 30_000, Carbon::today(), $this->user->id);

        // Void the payment but do NOT remove the pivot → invariant C violation
        DB::table('payments')->where('id', $payment->id)->update(['voided' => 1]);

        $this->expectException(RuntimeException::class);
        $this->expectExceptionMessageMatches('/pivotes asociados a payments voided/');

        $this->guard->assertCreditIntegrity($this->credit->fresh());
    }

    // ── Voided payment correctly handled (pivots removed) ────────────────────

    #[Test]
    public function guard_passes_when_voided_payment_has_pivots_removed(): void
    {
        $payment = $this->registrar->forCredit($this->credit, 30_000, Carbon::today(), $this->user->id);

        // Properly void: remove pivots, void the payment, re-materialize
        DB::table('payment_installment')->where('payment_id', $payment->id)->delete();
        DB::table('payments')->where('id', $payment->id)->update([
            'voided' => 1,
            'applied_amount' => 0,
            'extra_amount' => 0,
        ]);

        // Re-materialize installments (amount_paid should drop back to 0)
        app(PaymentMaterializer::class)->materializeCredit($this->credit->fresh());

        $this->guard->assertCreditIntegrity($this->credit->fresh());
        $this->assertTrue(true);
    }

    // ── Overpayment scenario ──────────────────────────────────────────────────

    #[Test]
    public function guard_passes_when_overpayment_produces_extra_amount(): void
    {
        // Payment exceeds installment total → extra_amount > 0
        $this->registrar->forCredit($this->credit, 80_000, Carbon::today(), $this->user->id);

        $this->guard->assertCreditIntegrity($this->credit->fresh());
        $this->assertTrue(true);
    }

    // ── Multi-credit isolation ────────────────────────────────────────────────

    #[Test]
    public function guard_only_checks_the_specified_credit(): void
    {
        // Create a second credit with corrupted data in a different company
        $otherCompany = Company::factory()->create();
        $otherUser = User::factory()->create(['company_id' => $otherCompany->id]);
        $otherCredit = Credit::factory()->create([
            'company_id' => $otherCompany->id,
            'client_id' => Client::factory()->create(['company_id' => $otherCompany->id])->id,
            'collector_user_id' => $otherUser->id,
            'created_by_user_id' => $otherUser->id,
            'status' => Credit::STATUS_ACTIVE,
            'amount' => 60_000,
            'due_date' => Carbon::today()->addDays(60),
        ]);

        $otherInst = Installment::factory()->create([
            'company_id' => $otherCompany->id, 'credit_id' => $otherCredit->id,
            'installment_number' => 1, 'status' => Installment::STATUS_PENDING,
            'due_date' => Carbon::today()->addDays(30)->toDateString(),
            'total_amount' => 60_000, 'principal_amount' => 50_000,
            'interest_amount' => 10_000, 'principal_balance_after' => 60_000, 'amount_paid' => 0,
        ]);

        // Corrupt the OTHER credit's installment
        DB::table('installments')->where('id', $otherInst->id)->update(['amount_paid' => 99_999]);

        // Guard on THIS credit should still pass
        $this->guard->assertCreditIntegrity($this->credit->fresh());
        $this->assertTrue(true);
    }
}
