<?php

namespace Tests\Feature;

use App\Models\Branch;
use App\Models\Business;
use App\Models\Loan;
use App\Models\LoanRestructuring;
use App\Models\RemotePaymentSubmission;
use App\Models\Role;
use App\Models\User;
use App\Services\Authorization\AuthorizationAssignmentService;
use App\Services\Authorization\FinancialMakerCheckerService;
use App\Support\Authorization\PermissionName;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Route;
use Illuminate\Validation\ValidationException;
use Tests\TestCase;

class Phase6FinancialMovementAuthorizationTest extends TestCase
{
    use RefreshDatabase;

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

    public function test_phase_six_loan_policies_require_permission_and_accessible_branch(): void
    {
        $branchA = $this->branch('P6-A');
        $branchB = $this->branch('P6-B');
        $manager = $this->staff('branch-manager', $branchA, 'phase6_manager');
        $officer = $this->staff('loan-officer', $branchA, 'phase6_officer');
        $inspector = $this->staff('loan-inspector', $branchA, 'phase6_inspector');
        $cashier = $this->staff('cashier', $branchA, 'phase6_cashier');
        $collector = $this->staff('debt-collector', $branchA, 'phase6_collector');

        $loanA = new Loan(['branch_id' => $branchA->id]);
        $loanB = new Loan(['branch_id' => $branchB->id]);

        $this->assertTrue(Gate::forUser($manager)->allows('disburse', $loanA));
        $this->assertFalse(Gate::forUser($manager)->allows('disburse', $loanB));
        $this->assertTrue(Gate::forUser($cashier)->allows('receivePayment', $loanA));
        $this->assertFalse(Gate::forUser($officer)->allows('disburse', $loanA));
        $this->assertTrue(Gate::forUser($inspector)->allows('approve', $loanA));
        $this->assertFalse(Gate::forUser($inspector)->allows('approve', $loanB));
        $this->assertTrue(Gate::forUser($officer)->allows('restructure', $loanA));
        $this->assertTrue(Gate::forUser($officer)->allows('excuseInstallment', $loanA));
        $this->assertFalse(Gate::forUser($collector)->allows('excuseInstallment', $loanA));
    }

    public function test_financial_maker_checker_blocks_same_employee_for_high_risk_decisions(): void
    {
        $branch = $this->branch('P6-MC', makerChecker: true);
        $user = $this->staff('branch-manager', $branch, 'phase6_maker');
        $branch->load('business');

        $approvedLoan = new Loan(['branch_id' => $branch->id, 'approved_by' => $user->id]);
        $approvedLoan->setRelation('branch', $branch);

        $restructuring = new LoanRestructuring(['branch_id' => $branch->id, 'requested_by' => $user->id]);
        $restructuring->setRelation('branch', $branch);

        $remote = new RemotePaymentSubmission([
            'branch_id' => $branch->id,
            'submitted_by_type' => 'staff',
            'submitted_by_user_id' => $user->id,
        ]);
        $remote->setRelation('branch', $branch);

        $checker = app(FinancialMakerCheckerService::class);
        $this->assertValidationBlocked(fn () => $checker->assertLoanDisbursementSeparated($approvedLoan, $user), 'approved this loan');
        $this->assertValidationBlocked(fn () => $checker->assertTopUpExecutionSeparated($approvedLoan, $user), 'approved this replacement loan');
        $this->assertValidationBlocked(fn () => $checker->assertRestructuringDecisionSeparated($restructuring, $user), 'requested this restructuring');
        $this->assertValidationBlocked(fn () => $checker->assertRemotePaymentVerificationSeparated($remote, $user), 'submitted this remote payment');
    }

    public function test_maker_checker_separation_is_not_applied_when_business_rule_is_disabled(): void
    {
        $branch = $this->branch('P6-OFF', makerChecker: false);
        $user = $this->staff('branch-manager', $branch, 'phase6_disabled');
        $branch->load('business');
        $loan = new Loan(['branch_id' => $branch->id, 'approved_by' => $user->id]);
        $loan->setRelation('branch', $branch);

        app(FinancialMakerCheckerService::class)->assertLoanDisbursementSeparated($loan, $user);

        $this->assertTrue(true);
    }

    public function test_phase_six_routes_keep_functional_permission_and_add_record_policy_middleware(): void
    {
        $expectations = [
            'loan-disbursements.store' => ['permission:loans.disburse', 'can:disburse,loan'],
            'loan-payments.store' => ['permission:payments.receive', 'can:receivePayment,loan'],
            'remote-payments.approve' => ['permission:payments.remote.verify', 'can:verify,submission'],
            'loan-top-ups.execute' => ['permission:loans.top-up.execute', 'can:executeTopUp,loan'],
            'loan-early-settlements.store' => ['permission:loans.settlement.execute', 'can:settle,loan'],
            'loan-restructurings.decision' => ['permission:loans.restructure.approve', 'can:decide,restructuring'],
            'loan-write-offs.decision' => ['permission:loans.writeoff.approve', 'can:decide,writeOff'],
            'arrears.show' => ['permission:arrears.view', 'can:viewArrears,loan'],
            'loans.installments.excuse.store' => ['permission:loans.installments.excuse', 'can:excuseInstallment,loan'],
        ];

        foreach ($expectations as $routeName => $middleware) {
            $route = Route::getRoutes()->getByName($routeName);
            $this->assertNotNull($route, "Expected route {$routeName} to exist.");
            $actual = $route->gatherMiddleware();
            foreach ($middleware as $expected) {
                $this->assertContains($expected, $actual, "Route {$routeName} must retain {$expected}.");
            }
        }
    }

    public function test_high_risk_services_recheck_authorization_inside_the_domain_boundary(): void
    {
        $expectations = [
            'LoanApprovalService.php' => ["authorize('approve'", "authorize('transition'"],
            'LoanDisbursementService.php' => ["authorize('disburse'", 'assertLoanDisbursementSeparated'],
            'LoanPaymentService.php' => ["authorize('receivePayment'", 'postVerifiedRemotePayment', "authorize('verify'"],
            'RemotePaymentService.php' => ["authorize('submitRemotePayment'", "authorize('verify'", 'assertRemotePaymentVerificationSeparated'],
            'LoanTopUpService.php' => ["authorize('createTopUp'", "authorize('executeTopUp'", 'assertTopUpExecutionSeparated'],
            'LoanEarlySettlementService.php' => ["authorize('settle'"],
            'LoanRestructuringService.php' => ["authorize('restructure'", "authorize('decide'", 'assertRestructuringDecisionSeparated'],
        ];

        foreach ($expectations as $file => $needles) {
            $source = File::get(app_path('Services/'.$file));
            foreach ($needles as $needle) {
                $this->assertStringContainsString($needle, $source, "{$file} is missing Phase 6 authorization safeguard {$needle}.");
            }
        }
    }

    public function test_installment_excuse_permission_replaces_prior_broad_loan_update_access_only_for_existing_roles(): void
    {
        foreach (['branch-administrator', 'branch-manager', 'loan-officer'] as $roleName) {
            $this->assertTrue(Role::where('name', $roleName)->firstOrFail()->hasPermissionTo(PermissionName::LOANS_INSTALLMENTS_EXCUSE->value));
        }

        foreach (['loan-inspector', 'cashier', 'accountant', 'debt-collector', 'hr-payroll-officer', 'auditor'] as $roleName) {
            $this->assertFalse(Role::where('name', $roleName)->firstOrFail()->hasPermissionTo(PermissionName::LOANS_INSTALLMENTS_EXCUSE->value));
        }
    }

    private function branch(string $tag, bool $makerChecker = false): Branch
    {
        $business = Business::firstOrFail();
        $business->update(['maker_checker_enabled' => $makerChecker]);

        return Branch::create([
            'business_id' => $business->id,
            'name' => 'Phase 6 '.$tag,
            'code' => 'P6'.strtoupper(substr(md5($tag), 0, 6)),
            'status' => 'active',
        ]);
    }

    private function staff(string $roleName, 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->branches()->attach($branch->id, ['is_primary' => true]);
        app(AuthorizationAssignmentService::class)->assignUserRole(
            $user,
            Role::where('name', $roleName)->firstOrFail(),
        );

        return $user->fresh();
    }

    private function assertValidationBlocked(callable $callback, string $messageFragment): void
    {
        try {
            $callback();
            $this->fail('Expected maker-checker validation to block this action.');
        } catch (ValidationException $exception) {
            $this->assertStringContainsString(
                $messageFragment,
                collect($exception->errors())->flatten()->join(' '),
            );
        }
    }
}
