<?php

namespace Tests\Feature;

use App\Models\AccountingEntry;
use App\Models\Branch;
use App\Models\Business;
use App\Models\Expense;
use App\Models\ExpenseCategory;
use App\Models\ExpenseReminder;
use App\Models\FinancialAccount;
use App\Models\FinancialAccountTransaction;
use App\Models\RecurringExpense;
use App\Models\Role;
use App\Models\User;
use Carbon\CarbonImmutable;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;

class ExpenseManagementWorkflowTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();
        CarbonImmutable::setTestNow('2026-08-12 12:30:00');
        $this->seed(DatabaseSeeder::class);
        Storage::fake('local');
    }

    protected function tearDown(): void
    {
        CarbonImmutable::setTestNow();
        parent::tearDown();
    }

    public function test_expense_entry_is_pending_keeps_cash_unchanged_and_stores_private_receipt(): void
    {
        [$branch,$account,$category,$accountant] = $this->fixture('entry');
        $cashier = $this->staff('cashier',$branch,'expense_cashier');

        $this->actingAs($cashier)->post(route('expenses.store'),$this->expensePayload($branch,$account,$category))
            ->assertForbidden();

        $payload = $this->expensePayload($branch,$account,$category);
        $payload['receipt'] = UploadedFile::fake()->create('rent-receipt.pdf',120,'application/pdf');
        $this->actingAs($accountant)->post(route('expenses.store'),$payload)->assertRedirect();

        $expense = Expense::firstOrFail();
        $this->assertSame('pending',$expense->status);
        $this->assertSame('100000.00',$account->fresh()->current_balance);
        $this->assertNull($expense->financial_account_transaction_id);
        $this->assertSame(0,AccountingEntry::where('related_type',Expense::class)->where('related_id',$expense->id)->count());
        Storage::disk('local')->assertExists($expense->receipt_path);
        $this->assertDatabaseHas('audit_logs',['action'=>'expense.created','auditable_id'=>$expense->id]);
    }

    public function test_approval_posts_account_transaction_balanced_expense_ledger_and_prevents_double_posting(): void
    {
        [$branch,$account,$category,$accountant,$manager] = $this->fixture('approval');
        $this->actingAs($accountant)->post(route('expenses.store'),$this->expensePayload($branch,$account,$category,'25000.00'))->assertRedirect();
        $expense = Expense::firstOrFail();

        $this->actingAs($manager)->post(route('expenses.decision',$expense),['decision'=>'approve','comments'=>'Budget verified.'])->assertRedirect();
        $expense->refresh();

        $this->assertSame('approved',$expense->status);
        $this->assertSame($manager->id,$expense->approved_by);
        $this->assertSame('75000.00',$account->fresh()->current_balance);
        $this->assertDatabaseHas('financial_account_transactions',[
            'related_type'=>Expense::class,'related_id'=>$expense->id,'type'=>'expense','money_out'=>25000,'money_in'=>0,
        ]);
        $this->assertDatabaseHas('accounting_entries',[
            'related_type'=>Expense::class,'related_id'=>$expense->id,'category'=>'expenses','debit'=>25000,'credit'=>0,
        ]);
        $this->assertDatabaseHas('accounting_entries',[
            'related_type'=>Expense::class,'related_id'=>$expense->id,'category'=>'assets','debit'=>0,'credit'=>25000,
        ]);
        $this->assertSame(
            AccountingEntry::where('related_type',Expense::class)->where('related_id',$expense->id)->sum('debit'),
            AccountingEntry::where('related_type',Expense::class)->where('related_id',$expense->id)->sum('credit')
        );
        $this->assertNotNull($expense->accounting_journal_number);
        $transactionCount = FinancialAccountTransaction::where('related_type',Expense::class)->where('related_id',$expense->id)->count();

        $this->actingAs($manager)->post(route('expenses.decision',$expense),['decision'=>'approve'])->assertSessionHasErrors('expense');
        $this->assertSame('75000.00',$account->fresh()->current_balance);
        $this->assertSame($transactionCount,FinancialAccountTransaction::where('related_type',Expense::class)->where('related_id',$expense->id)->count());
    }

    public function test_maker_checker_insufficient_funds_and_rejection_never_partially_post(): void
    {
        [$branch,$account,$category,$accountant,$manager] = $this->fixture('controls');
        $branch->business->update(['maker_checker_enabled'=>true]);
        $this->actingAs($accountant)->post(route('expenses.store'),$this->expensePayload($branch,$account,$category,'5000.00'))->assertRedirect();
        $expense = Expense::firstOrFail();

        $this->actingAs($accountant)->post(route('expenses.decision',$expense),['decision'=>'approve'])->assertSessionHasErrors('expense');
        $this->assertSame('pending',$expense->fresh()->status);
        $this->assertSame('100000.00',$account->fresh()->current_balance);

        $account->update(['current_balance'=>'1000.00']);
        $this->actingAs($manager)->post(route('expenses.decision',$expense),['decision'=>'approve'])->assertSessionHasErrors('amount');
        $this->assertSame('pending',$expense->fresh()->status);
        $this->assertSame(0,FinancialAccountTransaction::where('related_type',Expense::class)->where('related_id',$expense->id)->count());

        $this->actingAs($manager)->post(route('expenses.decision',$expense),['decision'=>'reject','comments'=>'Funding unavailable; resubmit next period.'])->assertRedirect();
        $this->assertSame('rejected',$expense->fresh()->status);
        $this->assertSame('1000.00',$account->fresh()->current_balance);
        $this->assertDatabaseMissing('accounting_entries',['related_type'=>Expense::class,'related_id'=>$expense->id]);
    }

    public function test_expenses_receipts_and_views_are_branch_scoped_and_categories_are_configurable(): void
    {
        [$branchA,$accountA,$category,$accountantA,$managerA] = $this->fixture('branch-a');
        [$branchB,$accountB,,$accountantB] = $this->fixture('branch-b');
        $auditorA = $this->staff('auditor',$branchA,'expense_auditor_a');

        $payloadA=$this->expensePayload($branchA,$accountA,$category,'1100.00');
        $payloadA['description']='Branch A Internet';
        $payloadA['receipt']=UploadedFile::fake()->image('internet-a.jpg');
        $this->actingAs($accountantA)->post(route('expenses.store'),$payloadA)->assertRedirect();
        $expenseA=Expense::latest('id')->firstOrFail();

        $categoryB=ExpenseCategory::where('business_id',$branchB->business_id)->where('name','Rent')->firstOrFail();
        $payloadB=$this->expensePayload($branchB,$accountB,$categoryB,'2200.00');
        $payloadB['description']='Branch B Internet';
        $this->actingAs($accountantB)->post(route('expenses.store'),$payloadB)->assertRedirect();
        $expenseB=Expense::latest('id')->firstOrFail();

        $this->actingAs($auditorA)->get(route('expenses.index'))->assertOk()->assertSee('Branch A Internet')->assertDontSee('Branch B Internet');
        $this->actingAs($auditorA)->get(route('expenses.show',$expenseB))->assertForbidden();
        $this->actingAs($auditorA)->get(route('expenses.receipt',$expenseA))->assertOk();

        $this->actingAs($managerA)->post(route('expense-categories.store'),[
            'business_id'=>$branchA->business_id,'name'=>'Fuel','code'=>'FUEL','description'=>'Vehicle and generator fuel.',
        ])->assertRedirect();
        $this->assertDatabaseHas('expense_categories',['business_id'=>$branchA->business_id,'name'=>'Fuel','status'=>'active']);
    }

    public function test_recurring_expenses_generate_idempotent_reminders_and_pending_occurrences_without_auto_payment(): void
    {
        [$branch,$account,$category,,$manager] = $this->fixture('recurring');
        $this->assertDatabaseHas('expense_categories',['business_id'=>$branch->business_id,'name'=>'Rent']);
        $this->assertDatabaseHas('expense_categories',['business_id'=>$branch->business_id,'name'=>'Internet']);
        $this->assertDatabaseHas('expense_categories',['business_id'=>$branch->business_id,'name'=>'Security']);
        $this->assertDatabaseHas('expense_categories',['business_id'=>$branch->business_id,'name'=>'Software']);
        $this->assertDatabaseHas('expense_categories',['business_id'=>$branch->business_id,'name'=>'Insurance']);
        $this->assertDatabaseHas('expense_categories',['business_id'=>$branch->business_id,'name'=>'Other']);

        $this->actingAs($manager)->post(route('recurring-expenses.store'),[
            'branch_id'=>$branch->id,'expense_category_id'=>$category->id,'financial_account_id'=>$account->id,
            'description'=>'Monthly office rent','amount'=>'15000.00','supplier'=>'Landlord Ltd','start_date'=>'2026-08-15',
            'recurrence'=>'monthly','reminder_days_before'=>3,'status'=>'active','notes'=>'Pay after invoice review.',
        ])->assertRedirect();
        $schedule=RecurringExpense::firstOrFail();

        $this->artisan('expenses:process-recurring',['--date'=>'2026-08-12'])->assertSuccessful();
        $this->assertSame(1,ExpenseReminder::count());
        $this->assertSame(0,Expense::count());
        $this->assertSame('open',ExpenseReminder::firstOrFail()->status);
        $this->assertSame('100000.00',$account->fresh()->current_balance);

        $this->artisan('expenses:process-recurring',['--date'=>'2026-08-12'])->assertSuccessful();
        $this->assertSame(1,ExpenseReminder::count());

        $this->artisan('expenses:process-recurring',['--date'=>'2026-08-15'])->assertSuccessful();
        $this->assertSame(1,Expense::count());
        $generated=Expense::firstOrFail();
        $this->assertSame('pending',$generated->status);
        $this->assertSame($schedule->id,$generated->recurring_expense_id);
        $this->assertSame('2026-08-15',$generated->occurrence_date->toDateString());
        $this->assertSame('generated',ExpenseReminder::firstOrFail()->fresh()->status);
        $this->assertSame('2026-09-15',$schedule->fresh()->next_due_date->toDateString());
        $this->assertSame('100000.00',$account->fresh()->current_balance);

        $this->artisan('expenses:process-recurring',['--date'=>'2026-08-15'])->assertSuccessful();
        $this->assertSame(1,Expense::count());
        $this->assertSame(1,ExpenseReminder::count());

        $this->actingAs($manager)->post(route('expenses.decision',$generated),['decision'=>'approve'])->assertRedirect();
        $this->assertSame('85000.00',$account->fresh()->current_balance);
    }

    public function test_recurring_expense_scheduler_and_finance_navigation_are_registered(): void
    {
        [$branch,,,,$manager] = $this->fixture('ui');
        $events=app(Schedule::class)->events();
        $event=collect($events)->first(fn($event)=>str_contains((string)$event->command,'expenses:process-recurring'));
        $this->assertNotNull($event);
        $this->assertSame('30 7 * * *',$event->expression);
        $this->assertSame(config('app.timezone'),$event->timezone);
        $this->actingAs($manager)->get(route('expenses.index'))->assertOk()->assertSee('Expense Management')->assertSee('Recurring');
        $this->actingAs($manager)->get(route('recurring-expenses.index'))->assertOk()->assertSee('Recurring Expenses');
        $this->actingAs($manager)->get(route('expense-categories.index'))->assertOk()->assertSee('Expense Categories');
    }

    private function expensePayload(Branch $branch, FinancialAccount $account, ExpenseCategory $category, string $amount='10000.00'): array
    {
        return [
            'branch_id'=>$branch->id,'expense_category_id'=>$category->id,'financial_account_id'=>$account->id,
            'description'=>'Office rent '.$branch->id,'amount'=>$amount,'expense_date'=>'2026-08-12','supplier'=>'Supplier '.$branch->id,'notes'=>'Approved operating cost.',
        ];
    }

    private function fixture(string $tag): array
    {
        $business=Business::firstOrFail();
        $branch=Branch::create(['business_id'=>$business->id,'name'=>'Expense '.str($tag)->headline(),'status'=>'active']);
        $account=FinancialAccount::create([
            'branch_id'=>$branch->id,'name'=>'Expense Cash '.str($tag)->headline(),'account_type'=>'cash',
            'opening_balance'=>'100000.00','current_balance'=>'100000.00','status'=>'active',
        ]);
        $category=ExpenseCategory::where('business_id',$business->id)->where('name','Rent')->firstOrFail();
        $accountant=$this->staff('accountant',$branch,'expense_accountant_'.str_replace('-','_',$tag));
        $manager=$this->staff('branch-manager',$branch,'expense_manager_'.str_replace('-','_',$tag));
        return [$branch,$account,$category,$accountant,$manager];
    }

    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;
    }
}