<?php

namespace Tests\Feature;

use App\Models\AuditLog;
use App\Models\Branch;
use App\Models\Business;
use App\Models\Customer;
use App\Models\Loan;
use App\Models\LoanProduct;
use App\Models\NotificationTemplate;
use App\Models\ReminderNotificationLog;
use App\Models\Role;
use App\Models\User;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;

class ReminderNotificationWorkflowTest extends TestCase
{
    use RefreshDatabase;

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

    public function test_default_sequence_delivers_each_due_occurrence_once(): void
    {
        $target='2026-08-20';
        [$branch,$loan]=$this->loanFixture('default-sequence',[3,1,0,-1,-2],$target);

        $this->artisan('notifications:dispatch-reminders',['--date'=>$target])->assertSuccessful();

        $this->assertSame(5,ReminderNotificationLog::count());
        $this->assertSame(2,ReminderNotificationLog::where('reminder_type','pre_due')->count());
        $this->assertSame(1,ReminderNotificationLog::where('reminder_type','due')->count());
        $this->assertSame(2,ReminderNotificationLog::where('reminder_type','overdue')->count());
        $this->assertSame(5,ReminderNotificationLog::where('status','delivered')->count());
        $notificationCount=$this->inBranch((int)$loan->branch_id, fn () => Customer::findOrFail($loan->customer_id)->notifications()->count());
        $this->assertSame(5,$notificationCount);
        $this->assertSame([1],ReminderNotificationLog::pluck('attempts')->unique()->values()->all());

        $this->artisan('notifications:dispatch-reminders',['--date'=>$target])->assertSuccessful();
        $this->assertSame(5,ReminderNotificationLog::count());
        $notificationCount=$this->inBranch((int)$loan->branch_id, fn () => Customer::findOrFail($loan->customer_id)->notifications()->count());
        $this->assertSame(5,$notificationCount);
        $this->assertDatabaseMissing('notification_logs',['branch_id'=>$branch->id,'status'=>'pending']);
    }

    public function test_continued_overdue_frequency_is_configurable(): void
    {
        $target='2026-08-20';
        $business=Business::firstOrFail();
        $business->update(['reminder_pre_due_days'=>[],'reminder_due_date_enabled'=>false,'reminder_overdue_enabled'=>true,'reminder_overdue_frequency'=>'every_3_days','reminder_channels'=>['in_system']]);
        $this->loanFixture('overdue-frequency',[-1,-2,-4,-7,-8],$target);

        $this->artisan('notifications:dispatch-reminders',['--date'=>$target])->assertSuccessful();

        $this->assertSame(3,ReminderNotificationLog::count());
        $this->assertSame([1,4,7],ReminderNotificationLog::orderBy('days_from_due')->pluck('days_from_due')->all());
    }

    public function test_reminder_uses_current_remaining_amount_after_partial_payment(): void
    {
        $target='2026-08-20';
        $business=Business::firstOrFail();
        $business->update(['reminder_pre_due_days'=>[1],'reminder_due_date_enabled'=>false,'reminder_overdue_enabled'=>false,'reminder_channels'=>['in_system']]);
        [,,$installments]=$this->loanFixture('partial-remaining',[1],$target);
        $installments->first()->update(['amount_due'=>'57000.00','amount_paid'=>'45000.00','remaining'=>'12000.00','principal_paid'=>'40000.00','interest_paid'=>'5000.00']);

        $this->artisan('notifications:dispatch-reminders',['--date'=>$target])->assertSuccessful();
        $log=ReminderNotificationLog::firstOrFail();
        $this->assertStringContainsString('12000.00',$log->message);
        $this->assertStringNotContainsString('57000.00',$log->message);
    }

    public function test_unconfigured_sms_fails_without_auto_retry_and_authorized_retry_can_deliver(): void
    {
        $target='2026-08-20';
        $business=Business::firstOrFail();
        $business->update(['reminder_pre_due_days'=>[1],'reminder_due_date_enabled'=>false,'reminder_overdue_enabled'=>false,'reminder_channels'=>['sms']]);
        [$branch,$loan]=$this->loanFixture('sms-retry',[1],$target);
        config(['reminders.providers.sms.endpoint'=>null,'reminders.providers.sms.token'=>null]);

        $this->artisan('notifications:dispatch-reminders',['--date'=>$target])->assertSuccessful();
        $log=ReminderNotificationLog::firstOrFail();
        $this->assertSame('failed',$log->status);
        $this->assertSame(1,$log->attempts);
        $this->assertNull($log->delivered_at);
        $this->assertStringContainsString('not configured',(string)$log->error);

        $this->artisan('notifications:dispatch-reminders',['--date'=>$target])->assertSuccessful();
        $this->assertSame(1,$log->fresh()->attempts);

        config(['reminders.providers.sms.endpoint'=>'https://sms.example.test/send','reminders.providers.sms.token'=>'test-token']);
        Http::fake(['https://sms.example.test/send'=>Http::response(['message_id'=>'sms-123'],200)]);
        $manager=$this->staff('branch-manager',$branch,'reminder_manager');
        $this->actingAs($manager)->post(route('notifications.retry',$log))->assertRedirect();

        $log->refresh();
        $this->assertSame('delivered',$log->status);
        $this->assertSame(2,$log->attempts);
        $this->assertNotNull($log->delivered_at);
        $this->assertNull($log->error);
        $this->assertDatabaseHas('audit_logs',['action'=>'notifications.delivery.retried','branch_id'=>$branch->id,'auditable_id'=>$log->id]);
        Http::assertSent(fn($request)=>$request->url()==='https://sms.example.test/send' && $request['to']===$loan->customer->phone && $request['reference']===$log->idempotency_key);
    }

    public function test_global_settings_and_templates_require_global_manage_permission_and_are_audited(): void
    {
        $admin=User::where('username','admin')->firstOrFail();
        $business=Business::firstOrFail();
        $branch=Branch::create(['business_id'=>$business->id,'name'=>'Reminder Permission Branch','status'=>'active']);
        $manager=$this->staff('branch-manager',$branch,'reminder_branch_manager');

        $this->actingAs($manager)->get(route('notification-settings.edit'))->assertForbidden();
        $this->actingAs($manager)->get(route('notifications.index'))->assertOk()->assertSee('data-standard-table',false);

        $this->actingAs($admin)->get(route('notification-settings.edit'))->assertOk()->assertSee('data-ui="form-shell"',false);
        $this->actingAs($admin)->put(route('notification-settings.update'),[
            'reminders_enabled'=>1,'reminder_pre_due_days'=>'5,2','reminder_due_date_enabled'=>1,'reminder_overdue_enabled'=>1,
            'reminder_overdue_frequency'=>'weekly','reminder_channels'=>['in_system','sms'],
        ])->assertRedirect();
        $business->refresh();
        $this->assertSame([5,2],$business->reminder_pre_due_days);
        $this->assertSame(['in_system','sms'],$business->reminder_channels);
        $this->assertSame('weekly',$business->reminder_overdue_frequency);
        $this->assertDatabaseHas('audit_logs',['action'=>'notifications.settings.updated','auditable_id'=>$business->id]);

        $template=NotificationTemplate::where('event_type','due')->where('channel','in_system')->firstOrFail();
        $this->actingAs($admin)->put(route('notification-templates.update',$template),['body'=>'Custom {loan_number} due {amount_due}','active'=>1])->assertRedirect();
        $this->assertSame('Custom {loan_number} due {amount_due}',$template->fresh()->body);
        $this->assertDatabaseHas('audit_logs',['action'=>'notifications.template.updated','auditable_id'=>$template->id]);
    }

    public function test_notification_logs_and_retries_are_branch_scoped(): void
    {
        $target='2026-08-20';
        $business=Business::firstOrFail();
        $business->update(['reminder_pre_due_days'=>[1],'reminder_due_date_enabled'=>false,'reminder_overdue_enabled'=>false,'reminder_channels'=>['sms']]);
        [$branchA,$loanA]=$this->loanFixture('branch-a',[1],$target);
        [$branchB,$loanB]=$this->loanFixture('branch-b',[1],$target);
        config(['reminders.providers.sms.endpoint'=>null]);
        $this->artisan('notifications:dispatch-reminders',['--date'=>$target])->assertSuccessful();
        $managerA=$this->staff('branch-manager',$branchA,'reminder_manager_a');
        $logB=ReminderNotificationLog::where('branch_id',$branchB->id)->firstOrFail();

        $this->actingAs($managerA)->get(route('notifications.index'))->assertOk()->assertSee($loanA->loan_number)->assertDontSee($loanB->loan_number);
        $this->actingAs($managerA)->post(route('notifications.retry',$logB))->assertForbidden();
        $this->assertSame(1,$logB->fresh()->attempts);
    }

    public function test_paid_installments_and_inactive_loans_are_not_reminded(): void
    {
        $target='2026-08-20';
        $business=Business::firstOrFail();
        $business->update(['reminder_pre_due_days'=>[1],'reminder_channels'=>['in_system']]);
        [,,$paid]=$this->loanFixture('paid-installment',[1],$target);
        $paid->first()->update(['amount_paid'=>'10000.00','remaining'=>'0.00','status'=>'paid']);
        [, $completed]=$this->loanFixture('completed-loan',[1],$target,'completed');

        $this->artisan('notifications:dispatch-reminders',['--date'=>$target])->assertSuccessful();
        $this->assertSame(0,ReminderNotificationLog::count());
        $this->assertSame('completed',$completed->status);
    }

    public function test_daily_scheduler_is_registered_at_configured_time(): void
    {
        $events=app(Schedule::class)->events();
        $event=collect($events)->first(fn($event)=>str_contains((string)$event->command,'notifications:dispatch-reminders'));
        $this->assertNotNull($event);
        $this->assertSame('0 8 * * *',$event->expression);
        $this->assertSame(config('app.timezone'),$event->timezone);
    }

    /** @return array{0:Branch,1:Loan,2:\Illuminate\Database\Eloquent\Collection} */
    private function loanFixture(string $tag, array $relativeDueDays, string $target, string $status='active'): array
    {
        $business=Business::firstOrFail();
        $branch=Branch::create(['business_id'=>$business->id,'name'=>'Reminder '.str($tag)->headline(),'status'=>'active']);
        $customer=Customer::create([
            'branch_id'=>$branch->id,'customer_number'=>'CUS-REM-'.strtoupper(substr(md5($tag),0,8)),'identity_mode'=>'without_nin',
            'first_name'=>'Neema','last_name'=>str($tag)->headline()->toString(),'phone'=>'0756'.str_pad((string)$branch->id,6,'0',STR_PAD_LEFT),'email'=>'customer'.$branch->id.'@example.test','status'=>'active',
        ]);
        $product=LoanProduct::create([
            'business_id'=>$business->id,'name'=>'Reminder Product '.str($tag)->headline(),'minimum_amount'=>'1000.00','maximum_amount'=>'500000.00',
            'duration'=>count($relativeDueDays),'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',
        ]);
        $count=max(1,count($relativeDueDays)); $total=(string)($count*10000).'.00';
        $loan=Loan::create([
            'branch_id'=>$branch->id,'customer_id'=>$customer->id,'loan_product_id'=>$product->id,'sequence_number'=>1,'sequence_year'=>2026,'loan_number'=>'LN-REM-'.strtoupper(substr(md5($tag),0,8)),
            'principal'=>$total,'interest'=>'0.00','processing_fee'=>'0.00','insurance_fee'=>'0.00','other_charges'=>'0.00','fees'=>'0.00','total_repayment'=>$total,
            'principal_outstanding'=>$status==='active' ? $total : '0.00','interest_outstanding'=>'0.00','fees_outstanding'=>'0.00','penalties_outstanding'=>'0.00',
            'total_outstanding'=>$status==='active' ? $total : '0.00','total_paid'=>$status==='active' ? '0.00' : $total,'purpose'=>'Reminder testing','schedule_start_date'=>$target,'status'=>$status,
            'disbursed_at'=>$status==='active' ? $target : null,'activated_at'=>$status==='active' ? $target : null,'schedule_activated_at'=>$status==='active' ? $target : null,
        ]);
        foreach(array_values($relativeDueDays) as $index=>$relative){
            $due=\Carbon\CarbonImmutable::parse($target)->addDays((int)$relative)->toDateString();
            $loan->installments()->create([
                'installment_number'=>$index+1,'principal'=>'10000.00','interest'=>'0.00','fees'=>'0.00','amount_due'=>'10000.00','due_date'=>$due,
                'amount_paid'=>'0.00','remaining'=>'10000.00','status'=>$relative<0 ? 'overdue' : 'upcoming','principal_paid'=>'0.00','interest_paid'=>'0.00','fees_paid'=>'0.00','penalty_due'=>'0.00','penalty_paid'=>'0.00',
            ]);
        }
        $installments=$this->inBranch((int)$branch->id, fn () => $loan->installments()->get());
        return [$branch,$loan,$installments];
    }

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