<?php

namespace Tests\Feature;

use App\Models\Branch;
use App\Models\Business;
use App\Models\CashReconciliation;
use App\Models\Collateral;
use App\Models\CollateralCategory;
use App\Models\Customer;
use App\Models\CustomerAppointment;
use App\Models\CustomerDocument;
use App\Models\Expense;
use App\Models\ExpenseCategory;
use App\Models\FinancialAccount;
use App\Models\Loan;
use App\Models\LoanProduct;
use App\Models\LoanWriteOff;
use App\Models\ManagementAlert;
use App\Models\PayrollPeriod;
use App\Models\RecurringExpense;
use App\Models\Role;
use App\Models\Task;
use App\Models\User;
use App\Services\ManagementAlertService;
use App\Services\ManagementCalendarService;
use App\Services\ManagementWorkSettings;
use App\Services\TaskAutomationService;
use Carbon\CarbonImmutable;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class TaskCalendarManagementAlertsTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();
        CarbonImmutable::setTestNow('2026-08-12 14:25:00');
        $this->seed(DatabaseSeeder::class);
        $this->actingAs(User::where('username','admin')->firstOrFail());
    }

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

    public function test_tasks_are_branch_scoped_assignable_and_terminal_statuses_are_permanent(): void
    {
        [$branchA,,,$managerA,$officerA] = $this->fixture('task-a');
        [$branchB,,,$managerB,$officerB] = $this->fixture('task-b');

        $response = $this->actingAs($managerA)->post(route('tasks.store'), [
            'branch_id'=>$branchA->id,'assigned_to'=>$officerA->id,'title'=>'Visit overdue customer','description'=>'Visit customer and record collection outcome.',
            'category'=>'collection','priority'=>'high','due_at'=>'2026-08-13 09:00:00',
        ]);
        $task = Task::firstOrFail();
        $response->assertRedirect(route('tasks.show',$task));
        $this->assertSame('pending',$task->status);
        $this->assertSame($officerA->id,$task->assigned_to);

        $this->actingAs($officerA)->post(route('tasks.status',$task),['status'=>'in_progress'])->assertRedirect();
        $this->assertSame('in_progress',$task->fresh()->status);
        $this->actingAs($officerA)->post(route('tasks.status',$task),['status'=>'completed'])->assertRedirect();
        $task->refresh();
        $this->assertSame('completed',$task->status);
        $this->assertSame($officerA->id,$task->completed_by);
        $this->assertNotNull($task->completed_at);

        $this->actingAs($officerA)->post(route('tasks.status',$task),['status'=>'in_progress'])->assertStatus(422);
        $this->actingAs($managerB)->get(route('tasks.show',$task))->assertForbidden();
        $this->actingAs($managerA)->post(route('tasks.store'),[
            'branch_id'=>$branchA->id,'assigned_to'=>$officerB->id,'title'=>'Wrong branch assignment','category'=>'general','priority'=>'normal',
        ])->assertStatus(422);

        $this->actingAs($managerA)->get(route('tasks.index'))->assertOk()->assertSee('Tasks')->assertSee('Visit overdue customer');
        $this->actingAs($officerA)->get(route('tasks.index'))->assertOk();
        $this->actingAs($managerB)->get(route('tasks.index'))->assertOk()->assertDontSee('Visit overdue customer');
    }

    public function test_configured_collection_tasks_are_idempotent_and_preserve_completed_history(): void
    {
        [$branch,, $product,,$officer] = $this->fixture('automation');
        $customer = $this->customer($branch,'automation');
        $loan = $this->loan($branch,$customer,$product,$officer,'AUTO',[
            'days_overdue'=>12,'total_overdue'=>'24000.00','overdue_principal'=>'20000.00','overdue_interest'=>'4000.00','arrears_bucket'=>'8-30 days',
        ]);
        app(ManagementWorkSettings::class)->store($branch->business_id,[
            'collection_tasks_enabled'=>true,'collection_task_overdue_days'=>5,'large_loan_threshold'=>0,'large_expense_threshold'=>0,
            'overdue_alert_days'=>30,'low_account_balance_threshold'=>0,'cash_shortage_threshold'=>0,'pending_approval_days'=>1,'document_expiry_days'=>30,
        ]);

        $service = app(TaskAutomationService::class);
        $this->assertSame(1,$service->process('2026-08-12')['created']);
        $task = Task::where('category','collection')->firstOrFail();
        $this->assertSame($loan->id,$task->source_id);
        $this->assertSame(Loan::class,$task->source_type);
        $this->assertSame($officer->id,$task->assigned_to);
        $this->assertSame('pending',$task->status);
        $this->assertSame(0,$service->process('2026-08-12')['created']);
        $this->assertSame(0,$service->process('2026-08-13')['created']);
        $this->assertSame(1,Task::where('source_id',$loan->id)->count());

        $this->actingAs($officer)->post(route('tasks.status',$task),['status'=>'completed'])->assertRedirect();
        $this->assertSame(1,$service->process('2026-08-13')['created']);
        $this->assertSame(2,Task::where('source_type',Loan::class)->where('source_id',$loan->id)->count());
        $this->assertSame(1,Task::where('source_type',Loan::class)->where('source_id',$loan->id)->where('status','completed')->count());
        $this->assertSame(1,Task::where('source_type',Loan::class)->where('source_id',$loan->id)->where('status','pending')->count());
    }

    public function test_calendar_contains_all_seven_roadmap_sources_and_respects_branch_scope(): void
    {
        [$branch,$account,$product,$manager,$officer] = $this->fixture('calendar');
        [$otherBranch,,,$otherManager] = $this->fixture('calendar-other');
        $customer = $this->customer($branch,'calendar');
        $loan = $this->loan($branch,$customer,$product,$officer,'CAL');
        $loan->installments()->create([
            'installment_number'=>1,'principal'=>'50000.00','interest'=>'5000.00','fees'=>'0.00','amount_due'=>'55000.00','due_date'=>'2026-08-15',
            'amount_paid'=>'0.00','remaining'=>'55000.00','status'=>'pending','principal_paid'=>'0.00','interest_paid'=>'0.00','fees_paid'=>'0.00','penalty_due'=>'0.00','penalty_paid'=>'0.00',
        ]);
        $loan->installments()->create([
            'installment_number'=>2,'principal'=>'50000.00','interest'=>'5000.00','fees'=>'0.00','amount_due'=>'55000.00','due_date'=>'2026-08-30',
            'amount_paid'=>'0.00','remaining'=>'55000.00','status'=>'pending','principal_paid'=>'0.00','interest_paid'=>'0.00','fees_paid'=>'0.00','penalty_due'=>'0.00','penalty_paid'=>'0.00',
        ]);
        Task::create(['branch_id'=>$branch->id,'title'=>'Calendar task','category'=>'general','priority'=>'normal','status'=>'pending','due_at'=>'2026-08-18 10:00:00','assigned_to'=>$officer->id,'created_by'=>$manager->id]);
        CustomerAppointment::create(['branch_id'=>$branch->id,'customer_id'=>$customer->id,'staff_id'=>$officer->id,'title'=>'Repayment meeting','scheduled_at'=>'2026-08-19 11:00:00','status'=>'scheduled','created_by'=>$manager->id]);
        PayrollPeriod::create(['period_number'=>'PAY-CAL-001','business_id'=>$branch->business_id,'branch_id'=>$branch->id,'name'=>'August payroll','start_date'=>'2026-08-01','end_date'=>'2026-08-31','pay_date'=>'2026-08-28','status'=>'draft','created_by'=>$manager->id]);
        $category = ExpenseCategory::where('business_id',$branch->business_id)->firstOrFail();
        RecurringExpense::create([
            'business_id'=>$branch->business_id,'branch_id'=>$branch->id,'expense_category_id'=>$category->id,'financial_account_id'=>$account->id,
            'description'=>'Internet service','amount'=>'25000.00','start_date'=>'2026-08-01','next_due_date'=>'2026-08-20','recurrence'=>'monthly','reminder_days_before'=>3,
            'status'=>'active','created_by'=>$manager->id,
        ]);
        CustomerDocument::create(['customer_id'=>$customer->id,'document_type'=>'National ID','original_name'=>'id.pdf','file_path'=>'private/id.pdf','expiry_date'=>'2026-08-25','uploaded_by'=>$manager->id]);
        Task::create(['branch_id'=>$otherBranch->id,'title'=>'Other branch secret task','category'=>'general','priority'=>'normal','status'=>'pending','due_at'=>'2026-08-18 10:00:00','created_by'=>$otherManager->id]);

        $events = collect(app(ManagementCalendarService::class)->events([$branch->id],'2026-08-01','2026-08-31'));
        foreach (['loan_due','loan_maturity','task','appointment','payroll','expense_due','document_expiry'] as $type) {
            $this->assertTrue($events->contains('type',$type),'Missing calendar type '.$type);
        }
        $this->assertFalse($events->contains('title','Other branch secret task'));
        $this->assertTrue($events->where('type','loan_due')->contains('date','2026-08-15'));
        $this->assertTrue($events->where('type','loan_maturity')->contains('date','2026-08-30'));

        $this->actingAs($manager)->get(route('calendar.index',['from'=>'2026-08-01','to'=>'2026-08-31']))
            ->assertOk()->assertSee('Management Calendar')->assertSee('Calendar task')->assertSee('Repayment meeting')->assertSee('August payroll');
        $this->actingAs($manager)->get(route('calendar.index',['branch_ids'=>[$otherBranch->id]]))->assertForbidden();
    }

    public function test_all_nine_management_alert_types_are_generated_idempotently(): void
    {
        [$branch,$account,$product,$manager,$officer] = $this->fixture('alerts');
        $customer = $this->customer($branch,'alerts');
        app(ManagementWorkSettings::class)->store($branch->business_id,[
            'collection_tasks_enabled'=>false,'collection_task_overdue_days'=>1,'large_loan_threshold'=>100000,'large_expense_threshold'=>100000,
            'overdue_alert_days'=>30,'low_account_balance_threshold'=>100000,'cash_shortage_threshold'=>10000,'pending_approval_days'=>1,'document_expiry_days'=>30,
        ]);

        $largeOverdue = $this->loan($branch,$customer,$product,$officer,'LARGE',[
            'principal'=>'500000.00','interest'=>'50000.00','total_repayment'=>'550000.00','principal_outstanding'=>'450000.00','interest_outstanding'=>'45000.00','total_outstanding'=>'495000.00',
            'days_overdue'=>45,'overdue_principal'=>'50000.00','overdue_interest'=>'5000.00','total_overdue'=>'55000.00','arrears_bucket'=>'31-60 days',
        ]);
        $this->loan($branch,$customer,$product,$officer,'PENDING',[
            'status'=>'under_review','principal'=>'50000.00','interest'=>'5000.00','total_repayment'=>'55000.00','principal_outstanding'=>'0.00','interest_outstanding'=>'0.00','total_outstanding'=>'0.00',
            'submitted_at'=>'2026-08-09 09:00:00','disbursed_at'=>null,'activated_at'=>null,'schedule_activated_at'=>null,
        ]);
        $category = ExpenseCategory::where('business_id',$branch->business_id)->firstOrFail();
        Expense::create([
            'expense_number'=>'EXP-ALERT-001','branch_id'=>$branch->id,'expense_category_id'=>$category->id,'financial_account_id'=>$account->id,
            'description'=>'Large office expense','amount'=>'200000.00','expense_date'=>'2026-08-10','status'=>'approved','entered_by'=>$manager->id,'approved_by'=>$manager->id,'approved_at'=>'2026-08-10 10:00:00',
        ]);
        PayrollPeriod::create(['period_number'=>'PAY-ALERT-001','business_id'=>$branch->business_id,'branch_id'=>$branch->id,'name'=>'Pending payroll','start_date'=>'2026-08-01','end_date'=>'2026-08-31','pay_date'=>'2026-08-31','status'=>'draft','created_by'=>$manager->id,'created_at'=>'2026-08-09 08:00:00','updated_at'=>'2026-08-09 08:00:00']);
        CashReconciliation::create([
            'reconciliation_number'=>'REC-ALERT-001','business_id'=>$branch->business_id,'branch_id'=>$branch->id,'financial_account_id'=>$account->id,'reconciliation_date'=>'2026-08-11',
            'version'=>1,'status'=>'active','opening_cash'=>'50000.00','cash_in'=>'0.00','cash_out'=>'0.00','expected_cash'=>'50000.00','physical_cash'=>'25000.00','variance'=>'-25000.00',
            'variance_explanation'=>'Cash shortage under investigation.','reconciled_by'=>$manager->id,'reconciled_at'=>'2026-08-11 18:00:00',
        ]);
        LoanWriteOff::create([
            'write_off_number'=>'WOF-ALERT-001','loan_id'=>$largeOverdue->id,'customer_id'=>$customer->id,'branch_id'=>$branch->id,
            'original_principal'=>'500000.00','original_total_repayment'=>'550000.00','principal_outstanding'=>'450000.00','interest_outstanding'=>'45000.00','fees_outstanding'=>'0.00','penalties_outstanding'=>'0.00',
            'write_off_amount'=>'495000.00','recovered_amount'=>'0.00','recoverable_balance'=>'495000.00','reason'=>'Management alert test','status'=>'pending','requested_by'=>$manager->id,'requested_at'=>'2026-08-11 09:00:00',
        ]);
        $collateralCategory = CollateralCategory::where('business_id',$branch->business_id)->firstOrFail();
        Collateral::create([
            'customer_id'=>$customer->id,'branch_id'=>$branch->id,'category_id'=>$collateralCategory->id,'sequence_number'=>9001,'code'=>'COL-ALERT-001','item'=>'Television',
            'estimated_value'=>'200000.00','accepted_value'=>'150000.00','received_at'=>'2026-07-01','status'=>'ready_for_release','received_by'=>$manager->id,'created_by'=>$manager->id,
        ]);
        CustomerDocument::create(['customer_id'=>$customer->id,'document_type'=>'Passport','original_name'=>'passport.pdf','file_path'=>'private/passport.pdf','expiry_date'=>'2026-08-25','uploaded_by'=>$manager->id]);

        $result = app(ManagementAlertService::class)->process('2026-08-12');
        $this->assertGreaterThanOrEqual(9,$result['created']);
        $types = ManagementAlert::pluck('type')->unique()->sort()->values()->all();
        $expected = ManagementAlert::TYPES;
        sort($expected);
        $this->assertSame($expected,$types);
        $count = ManagementAlert::count();
        app(ManagementAlertService::class)->process('2026-08-12');
        $this->assertSame($count,ManagementAlert::count());
        $this->assertDatabaseHas('management_alerts',['type'=>'low_account_balance','source_id'=>$account->id]);
        $this->assertDatabaseHas('management_alerts',['type'=>'cash_shortage','source_id'=>CashReconciliation::firstOrFail()->id]);
    }

    public function test_alert_actions_settings_permissions_and_branch_isolation_are_enforced(): void
    {
        [$branchA,,,$managerA] = $this->fixture('alert-access-a');
        [$branchB,,,$managerB] = $this->fixture('alert-access-b');
        $auditorA = $this->staff('auditor',$branchA,'step22_auditor_a');
        $business = Business::firstOrFail();
        $alert = ManagementAlert::create([
            'business_id'=>$business->id,'branch_id'=>$branchA->id,'type'=>'pending_approval','severity'=>'warning','title'=>'Pending decision','message'=>'Approval is waiting.',
            'status'=>'open','source_type'=>Loan::class,'source_id'=>999,'alert_key'=>'access-test-alert','occurred_at'=>'2026-08-12 09:00:00',
        ]);

        $this->actingAs($managerA)->put(route('management-alerts.settings'),[
            'collection_tasks_enabled'=>'1','collection_task_overdue_days'=>3,'large_loan_threshold'=>250000,'large_expense_threshold'=>100000,
            'overdue_alert_days'=>14,'low_account_balance_threshold'=>50000,'cash_shortage_threshold'=>5000,'pending_approval_days'=>2,'document_expiry_days'=>45,
        ])->assertRedirect();
        $settings = app(ManagementWorkSettings::class)->values($business);
        $this->assertTrue($settings['collection_tasks_enabled']);
        $this->assertSame(3,$settings['collection_task_overdue_days']);
        $this->assertSame(14,$settings['overdue_alert_days']);

        $this->actingAs($managerA)->post(route('management-alerts.acknowledge',$alert))->assertRedirect();
        $this->assertSame('acknowledged',$alert->fresh()->status);
        $this->actingAs($managerA)->post(route('management-alerts.resolve',$alert),['resolution_note'=>'Reviewed and assigned to branch staff.'])->assertRedirect();
        $alert->refresh();
        $this->assertSame('resolved',$alert->status);
        $this->assertSame($managerA->id,$alert->resolved_by);

        $second = ManagementAlert::create([
            'business_id'=>$business->id,'branch_id'=>$branchA->id,'type'=>'large_expense','severity'=>'warning','title'=>'Large expense','message'=>'Review expense.',
            'status'=>'open','source_type'=>Expense::class,'source_id'=>998,'alert_key'=>'access-test-alert-2','occurred_at'=>'2026-08-12 10:00:00',
        ]);
        $this->actingAs($auditorA)->get(route('management-alerts.index'))->assertOk()->assertSee('Management Alerts');
        $this->actingAs($auditorA)->post(route('management-alerts.acknowledge',$second))->assertForbidden();
        $this->actingAs($managerB)->get(route('management-alerts.index',['branch_id'=>$branchA->id]))->assertForbidden();
        $this->actingAs($managerB)->post(route('management-alerts.acknowledge',$second))->assertForbidden();
    }

    public function test_step_22_routes_navigation_and_daily_scheduler_are_registered(): void
    {
        [$branch,,,$manager,$officer] = $this->fixture('ui');
        $auditor = $this->staff('auditor',$branch,'step22_ui_auditor');
        $events = app(Schedule::class)->events();
        $event = collect($events)->first(fn ($event) => str_contains((string)$event->command,'management:process-work'));
        $this->assertNotNull($event);
        $this->assertSame('0 7 * * *',$event->expression);
        $this->assertSame(config('app.timezone'),$event->timezone);

        $this->actingAs($manager)->get(route('tasks.index'))->assertOk()->assertSee('Tasks')->assertSee('Calendar')->assertSee('Management Alerts');
        $this->actingAs($manager)->get(route('calendar.index'))->assertOk()->assertSee('Management Calendar');
        $this->actingAs($manager)->get(route('management-alerts.index'))->assertOk()->assertSee('Automation & thresholds',false);
        $this->actingAs($officer)->get(route('tasks.index'))->assertOk();
        $this->actingAs($officer)->get(route('calendar.index'))->assertOk();
        $this->actingAs($officer)->get(route('management-alerts.index'))->assertForbidden();
        $this->actingAs($auditor)->get(route('management-alerts.index'))->assertOk();
        $this->actingAs($auditor)->get(route('tasks.index'))->assertForbidden();
    }

    private function fixture(string $tag): array
    {
        $business = Business::firstOrFail();
        $branch = Branch::create(['business_id'=>$business->id,'name'=>'Step22 '.str($tag)->headline(),'status'=>'active']);
        $account = FinancialAccount::create([
            'branch_id'=>$branch->id,'name'=>'Step22 Cash '.str($tag)->headline(),'account_type'=>'cash','opening_balance'=>'50000.00','current_balance'=>'50000.00','status'=>'active',
        ]);
        $product = $this->product($business,'Step22 Product '.str($tag)->headline());
        $manager = $this->staff('branch-manager',$branch,'step22_manager_'.str_replace('-','_',$tag));
        $officer = $this->staff('loan-officer',$branch,'step22_officer_'.str_replace('-','_',$tag));
        $branch->update(['manager_user_id'=>$manager->id]);
        return [$branch,$account,$product,$manager,$officer];
    }

    private function product(Business $business, string $name): LoanProduct
    {
        return LoanProduct::create([
            'business_id'=>$business->id,'name'=>$name,'minimum_amount'=>'100.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'=>'none','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',
        ]);
    }

    private function customer(Branch $branch, string $tag): Customer
    {
        return Customer::create([
            'branch_id'=>$branch->id,'customer_number'=>'CUS-S22-'.strtoupper(substr(md5($branch->id.$tag),0,10)),'identity_mode'=>'without_nin',
            'first_name'=>'Customer','last_name'=>str($tag)->headline()->toString(),'phone'=>'06'.str_pad((string)(Customer::count()+1),8,'0',STR_PAD_LEFT),'status'=>'active',
        ]);
    }

    private function loan(Branch $branch, Customer $customer, LoanProduct $product, User $officer, string $tag, array $overrides = []): Loan
    {
        $sequence = Loan::where('branch_id',$branch->id)->count()+1;
        $defaults = [
            'branch_id'=>$branch->id,'customer_id'=>$customer->id,'loan_product_id'=>$product->id,'sequence_number'=>$sequence,'sequence_year'=>2026,
            'loan_number'=>'LN-S22-'.strtoupper($tag).'-'.$branch->id.'-'.$sequence,'principal'=>'100000.00','interest'=>'10000.00','processing_fee'=>'0.00',
            'insurance_fee'=>'0.00','other_charges'=>'0.00','fees'=>'0.00','total_repayment'=>'110000.00','principal_outstanding'=>'100000.00',
            'interest_outstanding'=>'10000.00','fees_outstanding'=>'0.00','penalties_outstanding'=>'0.00','total_outstanding'=>'110000.00','total_paid'=>'0.00',
            'overdue_principal'=>'0.00','overdue_interest'=>'0.00','overdue_fees'=>'0.00','overdue_penalties'=>'0.00','total_overdue'=>'0.00','days_overdue'=>0,
            'late_payment_penalty_type'=>'none','late_payment_penalty_value'=>'0.000000','purpose'=>'Step 22 fixture','schedule_start_date'=>'2026-08-01',
            'status'=>'active','disbursed_at'=>'2026-08-01 08:00:00','activated_at'=>'2026-08-01 08:00:00','schedule_activated_at'=>'2026-08-01 08:00:00','created_by'=>$officer->id,
        ];
        return Loan::create(array_merge($defaults,$overrides));
    }

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