<?php

namespace Tests\Feature;

use App\Models\Branch;
use App\Models\Business;
use App\Models\Collateral;
use App\Models\Customer;
use App\Models\Loan;
use App\Models\LoanProduct;
use App\Models\Permission;
use App\Models\Role;
use App\Models\User;
use App\Services\Authorization\AuthorizationAssignmentService;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Route;
use Tests\TestCase;

class Phase5AuthorizationPolicyTest extends TestCase
{
    use RefreshDatabase;

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

    public function test_branch_owned_policies_require_permission_and_accessible_branch(): void
    {
        $allowed = $this->branch('P5A001');
        $blocked = $this->branch('P5B001');
        $user = $this->userWithPermissions($allowed, [
            'customers.view',
            'loans.view',
            'collateral.view',
        ]);

        $this->assertTrue(Gate::forUser($user)->allows('view', new Customer(['branch_id' => $allowed->id])));
        $this->assertFalse(Gate::forUser($user)->allows('view', new Customer(['branch_id' => $blocked->id])));

        $this->assertTrue(Gate::forUser($user)->allows('view', new Loan(['branch_id' => $allowed->id])));
        $this->assertFalse(Gate::forUser($user)->allows('view', new Loan(['branch_id' => $blocked->id])));

        $this->assertTrue(Gate::forUser($user)->allows('view', new Collateral(['branch_id' => $allowed->id])));
        $this->assertFalse(Gate::forUser($user)->allows('view', new Collateral(['branch_id' => $blocked->id])));
    }

    public function test_permission_without_branch_assignment_does_not_grant_branch_owned_record_access(): void
    {
        $branch = $this->branch('P5N001');
        $user = $this->userWithPermissions(null, ['customers.view', 'loans.view', 'collateral.view']);

        $this->assertFalse(Gate::forUser($user)->allows('view', new Customer(['branch_id' => $branch->id])));
        $this->assertFalse(Gate::forUser($user)->allows('view', new Loan(['branch_id' => $branch->id])));
        $this->assertFalse(Gate::forUser($user)->allows('view', new Collateral(['branch_id' => $branch->id])));
    }

    public function test_customer_subresources_keep_separate_permissions(): void
    {
        $branch = $this->branch('P5C001');
        $reader = $this->userWithPermissions($branch, ['customers.view'], 'phase5-customer-reader');
        $operator = $this->userWithPermissions($branch, [
            'customers.documents',
            'customers.guarantors',
            'customers.watchlist',
        ], 'phase5-customer-operator');
        $customer = new Customer(['branch_id' => $branch->id]);

        $this->assertFalse(Gate::forUser($reader)->allows('manageDocuments', $customer));
        $this->assertFalse(Gate::forUser($reader)->allows('manageGuarantors', $customer));
        $this->assertFalse(Gate::forUser($reader)->allows('manageWatchlist', $customer));

        $this->assertTrue(Gate::forUser($operator)->allows('manageDocuments', $customer));
        $this->assertTrue(Gate::forUser($operator)->allows('manageGuarantors', $customer));
        $this->assertTrue(Gate::forUser($operator)->allows('manageWatchlist', $customer));
    }

    public function test_loan_product_policy_separates_read_from_management(): void
    {
        $branch = $this->branch('P5P001');
        $reader = $this->userWithPermissions($branch, ['loan-products.view'], 'phase5-product-reader');
        $product = new LoanProduct(['business_id' => $branch->business_id]);

        $this->assertTrue(Gate::forUser($reader)->allows('view', $product));
        $this->assertTrue(Gate::forUser($reader)->allows('preview', $product));
        $this->assertFalse(Gate::forUser($reader)->allows('update', $product));
    }

    public function test_super_administrator_gate_bypasses_record_policy_intentionally(): void
    {
        $branch = $this->branch('P5S001');
        $superAdmin = User::where('email', 'admin@microfinance.local')->firstOrFail();

        $this->assertTrue(Gate::forUser($superAdmin)->allows('view', new Customer(['branch_id' => $branch->id])));
        $this->assertTrue(Gate::forUser($superAdmin)->allows('update', new Loan(['branch_id' => $branch->id])));
        $this->assertTrue(Gate::forUser($superAdmin)->allows('move', new Collateral(['branch_id' => $branch->id])));
    }

    public function test_phase5_record_routes_run_policy_middleware_in_addition_to_functional_permissions(): void
    {
        $expected = [
            'customers.show' => 'can:view,customer',
            'customers.update' => 'can:update,customer',
            'customers.documents.store' => 'can:manageDocuments,customer',
            'customers.guarantors.store' => 'can:manageGuarantors,customer',
            'customers.watchlist.store' => 'can:manageWatchlist,customer',
            'loan-products.show' => 'can:view,loanProduct',
            'loan-products.update' => 'can:update,loanProduct',
            'loans.show' => 'can:view,loan',
            'loans.update' => 'can:update,loan',
            'loans.transition' => 'can:transition,loan',
            'loans.contract' => 'can:generateContract,loan',
            'loans.documents.store' => 'can:manageDocuments,loan',
            'collateral.show' => 'can:view,collateral',
            'collateral.update' => 'can:update,collateral',
            'collateral.move' => 'can:move,collateral',
            'collateral.files.store' => 'can:manageDocuments,collateral',
        ];

        foreach ($expected as $routeName => $policyMiddleware) {
            $route = Route::getRoutes()->getByName($routeName);
            $this->assertNotNull($route, "Missing route {$routeName}.");
            $this->assertContains($policyMiddleware, $route->gatherMiddleware(), "{$routeName} is missing {$policyMiddleware}.");
            $this->assertTrue(
                collect($route->gatherMiddleware())->contains(fn ($middleware) => is_string($middleware) && str_starts_with($middleware, 'permission:')),
                "{$routeName} lost its functional permission boundary."
            );
        }
    }

    public function test_cross_branch_customer_route_is_denied_by_policy_boundary(): void
    {
        $allowed = $this->branch('P5R001');
        $blocked = $this->branch('P5R002');
        $user = $this->userWithPermissions($allowed, ['customers.view']);
        $customer = Customer::factory()->create(['branch_id' => $blocked->id]);

        $this->actingAs($user)
            ->get(route('customers.show', $customer))
            ->assertForbidden();
    }

    private function branch(string $code): Branch
    {
        return Branch::create([
            'business_id' => Business::firstOrFail()->id,
            'name' => $code.' Branch',
            'code' => $code,
            'status' => 'active',
        ]);
    }

    /** @param array<int, string> $permissionNames */
    private function userWithPermissions(?Branch $branch, array $permissionNames, string $roleName = 'phase5-policy-role'): User
    {
        $user = User::factory()->create([
            'employee_number' => 'EMP-'.fake()->unique()->numerify('######'),
            'username' => fake()->unique()->userName(),
            'status' => 'active',
            'primary_branch_id' => $branch?->id,
        ]);

        $role = Role::query()->create([
            'name' => $roleName,
            'guard_name' => 'web',
            'display_name' => str($roleName)->headline()->toString(),
            'description' => 'Phase 5 authorization test role.',
            'is_system' => false,
        ]);

        $permissionIds = Permission::query()
            ->whereIn('name', $permissionNames)
            ->pluck('id')
            ->map(fn ($id) => (int) $id)
            ->all();

        app(AuthorizationAssignmentService::class)->syncRolePermissionIds($role, $permissionIds);
        app(AuthorizationAssignmentService::class)->syncUserRoleIds($user, [$role->id]);

        if ($branch) {
            $user->branches()->attach($branch->id, ['is_primary' => true]);
        }

        return $user->fresh('roles');
    }
}
