<?php

namespace Tests\Feature;

use App\Jobs\RefreshCustomerCreditScore;
use App\Models\AccountingEntry;
use App\Models\Branch;
use App\Models\Business;
use App\Models\Customer;
use App\Models\FinancialAccount;
use App\Models\FinancialAccountTransaction;
use App\Models\Loan;
use App\Models\LoanDisbursement;
use App\Models\LoanProduct;
use App\Models\Role;
use App\Models\User;
use App\Services\FinancialAccountingService;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;

class LoanDisbursementWorkflowTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();
        $this->seed(DatabaseSeeder::class);
        Queue::fake();
    }

    public function test_ready_loan_disbursement_posts_finance_activates_schedule_notifies_and_generates_receipt(): void
    {
        [$business,$branch,$customer,$product,$admin] = $this->fixture();
        $business->update(['require_signed_contract_before_disbursement'=>false]);
        $loan = $this->readyLoan($admin,$branch,$customer,$product);
        $account = $this->account($admin,$branch,'Main Cash','1000000.00');

        $response = $this->actingAs($admin)->post(route('loan-disbursements.store',$loan),[
            'amount'=>'500000.00','financial_account_id'=>$account->id,'date'=>'2026-08-12','reference'=>'CASH-DSB-001','notes'=>'Paid to customer at branch counter.',
        ]);

        $disbursement = LoanDisbursement::firstOrFail();
        $response->assertRedirect(route('loan-disbursements.receipt',$disbursement));
        $this->assertMatchesRegularExpression('/^DSB-[A-Z0-9]+-2026-000001$/',$disbursement->disbursement_number);
        $this->assertSame('500000.00',$disbursement->amount);
        $this->assertSame('500000.00',$account->fresh()->current_balance);

        $transaction = FinancialAccountTransaction::where('related_type',LoanDisbursement::class)->where('related_id',$disbursement->id)->firstOrFail();
        $this->assertSame('loan_disbursement',$transaction->type);
        $this->assertSame('500000.00',$transaction->money_out);
        $this->assertSame('500000.00',$transaction->running_balance);
        $this->assertSame($transaction->id,$disbursement->fresh()->financial_account_transaction_id);

        $entries = AccountingEntry::where('event_type','loan_disbursement')->where('related_id',$disbursement->id)->get();
        $this->assertSame(2,$entries->count());
        $this->assertTrue($entries->contains(fn($entry)=>$entry->category==='assets' && $entry->credit==='500000.00' && $entry->debit==='0.00'));
        $this->assertTrue($entries->contains(fn($entry)=>$entry->category==='principal' && $entry->debit==='500000.00' && $entry->credit==='0.00'));

        $loan->refresh();
        $this->assertSame('active',$loan->status);
        $this->assertNotNull($loan->activated_at);
        $this->assertNotNull($loan->schedule_activated_at);
        $this->assertSame($admin->id,$loan->disbursed_by);
        $this->assertTrue($loan->installments->every(fn($installment)=>$installment->status==='upcoming' && $installment->remaining===$installment->amount_due));
        $this->assertDatabaseHas('loan_status_histories',['loan_id'=>$loan->id,'from_status'=>'ready_for_disbursement','to_status'=>'active']);
        $this->assertDatabaseHas('notifications',['notifiable_type'=>Customer::class,'notifiable_id'=>$customer->id]);
        $this->assertDatabaseHas('audit_logs',['action'=>'loan.disbursed','auditable_id'=>$disbursement->id]);
        Queue::assertPushed(RefreshCustomerCreditScore::class, fn(RefreshCustomerCreditScore $job): bool =>
            $job->customerId === $customer->id
            && $job->branchId === $branch->id
            && $job->trigger === 'loan_activated'
        );

        $this->actingAs($admin)->get(route('loan-disbursements.receipt',$disbursement))->assertOk()
            ->assertSee($disbursement->disbursement_number)->assertSee($loan->loan_number)->assertSee('Loan Disbursement Receipt');
    }

    public function test_signed_contract_requirement_blocks_bypass_and_allows_posting_after_signed_copy_exists(): void
    {
        [$business,$branch,$customer,$product,$admin] = $this->fixture();
        $business->update(['require_signed_contract_before_disbursement'=>true]);
        $loan = $this->readyLoan($admin,$branch,$customer,$product);
        $account = $this->account($admin,$branch,'Contract Cash','800000.00');

        $this->actingAs($admin)->from(route('loan-disbursements.create',$loan))->post(route('loan-disbursements.store',$loan),[
            'amount'=>'500000.00','financial_account_id'=>$account->id,'date'=>'2026-08-12',
        ])->assertRedirect(route('loan-disbursements.create',$loan))->assertSessionHasErrors('loan');

        $this->assertSame(0,LoanDisbursement::count());
        $this->assertSame('800000.00',$account->fresh()->current_balance);
        $this->assertSame('ready_for_disbursement',$loan->fresh()->status);

        DB::table('loan_documents')->insert([
            'loan_id'=>$loan->id,'kind'=>'signed_contract','document_type'=>'Signed Contract','original_name'=>'signed.pdf',
            'file_path'=>'private/tests/signed.pdf','mime_type'=>'application/pdf','file_size'=>100,'uploaded_by'=>$admin->id,
            'created_at'=>now(),'updated_at'=>now(),
        ]);

        $this->actingAs($admin)->post(route('loan-disbursements.store',$loan),[
            'amount'=>'500000.00','financial_account_id'=>$account->id,'date'=>'2026-08-12',
        ])->assertRedirect();

        $this->assertSame(1,LoanDisbursement::count());
        $this->assertSame('300000.00',$account->fresh()->current_balance);
        $this->assertSame('active',$loan->fresh()->status);
    }

    public function test_non_ready_or_rejected_loan_cannot_be_disbursed(): void
    {
        [$business,$branch,$customer,$product,$admin] = $this->fixture();
        $business->update(['require_signed_contract_before_disbursement'=>false]);
        $loan = $this->readyLoan($admin,$branch,$customer,$product);
        $loan->update(['status'=>'rejected']);
        $account = $this->account($admin,$branch,'Reject Cash','800000.00');

        $this->actingAs($admin)->from(route('loan-disbursements.index'))->post(route('loan-disbursements.store',$loan),[
            'amount'=>'500000.00','financial_account_id'=>$account->id,'date'=>'2026-08-12',
        ])->assertSessionHasErrors('loan');

        $this->assertSame(0,LoanDisbursement::count());
        $this->assertSame('800000.00',$account->fresh()->current_balance);
        $this->assertSame('rejected',$loan->fresh()->status);
    }

    public function test_double_disbursement_is_prevented_without_second_account_deduction(): void
    {
        [$business,$branch,$customer,$product,$admin] = $this->fixture();
        $business->update(['require_signed_contract_before_disbursement'=>false]);
        $loan = $this->readyLoan($admin,$branch,$customer,$product);
        $account = $this->account($admin,$branch,'Double Cash','1200000.00');

        $payload=['amount'=>'500000.00','financial_account_id'=>$account->id,'date'=>'2026-08-12'];
        $this->actingAs($admin)->post(route('loan-disbursements.store',$loan),$payload)->assertRedirect();
        $this->assertSame('700000.00',$account->fresh()->current_balance);

        $this->actingAs($admin)->from(route('loan-disbursements.index'))->post(route('loan-disbursements.store',$loan),$payload)
            ->assertSessionHasErrors('loan');

        $this->assertSame(1,LoanDisbursement::count());
        $this->assertSame('700000.00',$account->fresh()->current_balance);
        $this->assertSame(1,FinancialAccountTransaction::where('type','loan_disbursement')->count());
    }

    public function test_insufficient_funds_and_wrong_amount_are_atomic(): void
    {
        [$business,$branch,$customer,$product,$admin] = $this->fixture();
        $business->update(['require_signed_contract_before_disbursement'=>false]);
        $loan = $this->readyLoan($admin,$branch,$customer,$product);
        $account = $this->account($admin,$branch,'Small Cash','100000.00');
        $beforeEntries = AccountingEntry::count();
        $beforeTransactions = FinancialAccountTransaction::count();

        $this->actingAs($admin)->from(route('loan-disbursements.create',$loan))->post(route('loan-disbursements.store',$loan),[
            'amount'=>'400000.00','financial_account_id'=>$account->id,'date'=>'2026-08-12',
        ])->assertSessionHasErrors('amount');
        $this->actingAs($admin)->from(route('loan-disbursements.create',$loan))->post(route('loan-disbursements.store',$loan),[
            'amount'=>'500000.00','financial_account_id'=>$account->id,'date'=>'2026-08-12',
        ])->assertSessionHasErrors('amount');

        $this->assertSame(0,LoanDisbursement::count());
        $this->assertSame('100000.00',$account->fresh()->current_balance);
        $this->assertSame('ready_for_disbursement',$loan->fresh()->status);
        $this->assertSame($beforeEntries,AccountingEntry::count());
        $this->assertSame($beforeTransactions,FinancialAccountTransaction::count());
    }

    public function test_disbursement_permission_and_branch_isolation_are_backend_enforced(): void
    {
        [$business,$branchA,$customer,$product,$admin] = $this->fixture();
        $business->update(['require_signed_contract_before_disbursement'=>false]);
        $branchB=Branch::create(['business_id'=>$business->id,'name'=>'Mwanza Disbursement','status'=>'active']);
        $customerB=Customer::create(['branch_id'=>$branchB->id,'customer_number'=>'CUS-DSB-002','identity_mode'=>'without_nin','first_name'=>'Asha','last_name'=>'Musa','phone'=>'0755000999','status'=>'active']);
        $loanA=$this->readyLoan($admin,$branchA,$customer,$product);
        $loanB=$this->readyLoan($admin,$branchB,$customerB,$product);
        $accountA=$this->account($admin,$branchA,'Dodoma Cash','1000000.00');
        $accountB=$this->account($admin,$branchB,'Mwanza Cash','1000000.00');
        $cashier=$this->staff('cashier',$branchA,'dsb_cashier');
        $officer=$this->staff('loan-officer',$branchA,'dsb_officer');

        $this->actingAs($officer)->get(route('loan-disbursements.index'))->assertForbidden();
        $this->actingAs($cashier)->get(route('loan-disbursements.index'))->assertOk()->assertSee($loanA->loan_number)->assertDontSee($loanB->loan_number);
        $this->actingAs($cashier)->get(route('loan-disbursements.create',$loanB))->assertForbidden();
        $this->actingAs($cashier)->post(route('loan-disbursements.store',$loanA),[
            'amount'=>'500000.00','financial_account_id'=>$accountB->id,'date'=>'2026-08-12',
        ])->assertForbidden();

        $this->assertSame(0,LoanDisbursement::count());
        $this->assertSame('1000000.00',$accountA->fresh()->current_balance);
        $this->assertSame('1000000.00',$accountB->fresh()->current_balance);
    }

    public function test_step_eight_pages_use_standardized_list_and_form_patterns(): void
    {
        [$business,$branch,$customer,$product,$admin] = $this->fixture();
        $business->update(['require_signed_contract_before_disbursement'=>false]);
        $loan = $this->readyLoan($admin,$branch,$customer,$product);
        $this->account($admin,$branch,'UI Cash','1000000.00');

        $this->actingAs($admin)->get(route('loan-disbursements.index'))->assertOk()
            ->assertSee('data-ui="page-toolbar"',false)->assertSee('data-ui="table-toolbar"',false)->assertSee($loan->loan_number);
        $this->actingAs($admin)->get(route('loan-disbursements.create',$loan))->assertOk()
            ->assertSee('data-ui="form-shell"',false)->assertSee('data-ui="form-section"',false)->assertSee('Post Disbursement');
    }

    private function fixture(): array
    {
        $business=Business::firstOrFail();
        $branch=Branch::firstOrCreate(['business_id'=>$business->id,'name'=>'Dodoma Disbursement'],['status'=>'active']);
        $customer=Customer::firstOrCreate(['customer_number'=>'CUS-DSB-001'],[
            'branch_id'=>$branch->id,'identity_mode'=>'without_nin','first_name'=>'Rehema','last_name'=>'Juma','phone'=>'0755000111','status'=>'active',
        ]);
        $product=LoanProduct::firstOrCreate(['business_id'=>$business->id,'name'=>'Disbursement Product'],[
            'minimum_amount'=>'100000.00','maximum_amount'=>'1000000.00','duration'=>2,'duration_unit'=>'months','interest_rate'=>'10.000000',
            'interest_calculation_method'=>'flat','repayment_frequency'=>'monthly','grace_period_days'=>0,
            'late_payment_penalty_type'=>'fixed','late_payment_penalty_value'=>'0.000000','processing_fee_type'=>'fixed','processing_fee_value'=>'0.000000',
            'insurance_fee_type'=>'fixed','insurance_fee_value'=>'0.000000','other_charges'=>'0.00','collateral_required'=>false,'guarantor_required'=>false,'status'=>'active',
        ]);
        $admin=User::where('email','admin@microfinance.local')->firstOrFail();
        return [$business,$branch,$customer,$product,$admin];
    }

    private function readyLoan(User $creator, Branch $branch, Customer $customer, LoanProduct $product): Loan
    {
        $this->actingAs($creator)->post(route('loans.store'),[
            'branch_id'=>$branch->id,'customer_id'=>$customer->id,'loan_product_id'=>$product->id,
            'principal'=>'500000.00','purpose'=>'Business working capital','schedule_start_date'=>'2026-09-12',
        ])->assertRedirect();
        $loan=Loan::where('branch_id',$branch->id)->where('customer_id',$customer->id)->latest('id')->firstOrFail();
        $loan->update(['status'=>'ready_for_disbursement','approved_at'=>now(),'approved_by'=>$creator->id]);
        return $loan->fresh();
    }

    private function account(User $creator, Branch $branch, string $name, string $opening): FinancialAccount
    {
        return app(FinancialAccountingService::class)->createAccount([
            'branch_id'=>$branch->id,'name'=>$name,'account_type'=>'cash','opening_balance'=>$opening,'status'=>'active',
        ],$creator);
    }

    private function staff(string $role, Branch $branch, string $username): User
    {
        $user=User::factory()->create(['name'=>str($username)->replace('_',' ')->title()->toString(),'username'=>$username,'status'=>'active','primary_branch_id'=>$branch->id]);
        $user->roles()->attach(Role::where('name',$role)->firstOrFail());
        $user->branches()->attach($branch->id,['is_primary'=>true]);
        return $user;
    }
}
