<?php

namespace Tests\Feature\BranchIsolation;

use App\Models\Branch;
use App\Models\Business;
use App\Models\Role;
use App\Models\User;
use App\Services\BranchAccessService;
use App\Services\UserBranchAssignmentService;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;

class UserBranchAssignmentConsistencyTest extends TestCase
{
    use RefreshDatabase;

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

    public function test_assignment_service_always_keeps_primary_branch_in_pivot_and_marks_exactly_one_primary(): void
    {
        [$branchA, $branchB] = $this->branches();
        $user = User::factory()->create([
            'primary_branch_id' => $branchA->id,
            'username' => 'assignment.service.user',
            'status' => 'active',
        ]);
        $user->branches()->sync([$branchA->id => ['is_primary' => true]]);

        app(UserBranchAssignmentService::class)->synchronize($user, $branchB->id, [$branchA->id]);

        $this->assertSame($branchB->id, $user->fresh()->primary_branch_id);
        $this->assertDatabaseHas('branch_user', [
            'user_id' => $user->id,
            'branch_id' => $branchA->id,
            'is_primary' => 0,
        ]);
        $this->assertDatabaseHas('branch_user', [
            'user_id' => $user->id,
            'branch_id' => $branchB->id,
            'is_primary' => 1,
        ]);
        $this->assertSame(1, DB::table('branch_user')->where('user_id', $user->id)->where('is_primary', true)->count());
    }

    public function test_staff_update_changes_primary_and_pivot_assignments_together(): void
    {
        [$branchA, $branchB] = $this->branches();
        $admin = User::where('email', 'admin@microfinance.local')->firstOrFail();
        $staff = User::factory()->create([
            'name' => 'Branch Assignment Staff',
            'position' => 'Officer',
            'phone' => '255712111222',
            'username' => 'assignment_staff',
            'status' => 'active',
            'primary_branch_id' => $branchA->id,
        ]);
        $staff->branches()->sync([$branchA->id => ['is_primary' => true]]);

        $this->actingAs($admin)->put(route('admin.users.update', $staff), [
            'name' => $staff->name,
            'position' => $staff->position,
            'phone' => $staff->phone,
            'username' => $staff->username,
            'primary_branch_id' => $branchB->id,
            'branch_ids' => [$branchA->id],
        ])->assertRedirect(route('admin.users.index'));

        $this->assertSame($branchB->id, $staff->fresh()->primary_branch_id);
        $this->assertDatabaseHas('branch_user', ['user_id' => $staff->id, 'branch_id' => $branchA->id, 'is_primary' => 0]);
        $this->assertDatabaseHas('branch_user', ['user_id' => $staff->id, 'branch_id' => $branchB->id, 'is_primary' => 1]);
        $this->assertSame(1, DB::table('branch_user')->where('user_id', $staff->id)->where('is_primary', true)->count());
    }

    public function test_repair_command_dry_run_reports_but_does_not_mutate_then_repairs_legacy_mismatch(): void
    {
        [$branchA, $branchB] = $this->branches();
        $user = User::factory()->create([
            'primary_branch_id' => $branchA->id,
            'username' => 'legacy.assignment.user',
            'status' => 'active',
        ]);

        DB::table('branch_user')->insert([
            'branch_id' => $branchB->id,
            'user_id' => $user->id,
            'is_primary' => true,
            'created_at' => now(),
            'updated_at' => now(),
        ]);

        $this->artisan('branches:repair-user-assignments', ['--dry-run' => true])
            ->expectsOutputToContain('need repair')
            ->assertSuccessful();

        $this->assertDatabaseMissing('branch_user', ['user_id' => $user->id, 'branch_id' => $branchA->id]);
        $this->assertDatabaseHas('branch_user', ['user_id' => $user->id, 'branch_id' => $branchB->id, 'is_primary' => 1]);

        $this->artisan('branches:repair-user-assignments')
            ->expectsOutputToContain('repaired')
            ->assertSuccessful();

        $this->assertSame($branchA->id, $user->fresh()->primary_branch_id);
        $this->assertDatabaseHas('branch_user', ['user_id' => $user->id, 'branch_id' => $branchA->id, 'is_primary' => 1]);
        $this->assertDatabaseHas('branch_user', ['user_id' => $user->id, 'branch_id' => $branchB->id, 'is_primary' => 0]);
        $this->assertSame(1, DB::table('branch_user')->where('user_id', $user->id)->where('is_primary', true)->count());
    }

    public function test_repair_command_recovers_primary_branch_from_single_existing_primary_pivot(): void
    {
        [, $branchB] = $this->branches();
        $user = User::factory()->create([
            'primary_branch_id' => null,
            'username' => 'recover.primary.user',
            'status' => 'active',
        ]);
        $user->branches()->sync([$branchB->id => ['is_primary' => true]]);

        $this->artisan('branches:repair-user-assignments')->assertSuccessful();

        $this->assertSame($branchB->id, $user->fresh()->primary_branch_id);
        $this->assertDatabaseHas('branch_user', ['user_id' => $user->id, 'branch_id' => $branchB->id, 'is_primary' => 1]);
    }

    public function test_user_branch_access_helpers_match_the_canonical_service(): void
    {
        [$branchA, $branchB] = $this->branches();
        $service = app(BranchAccessService::class);

        $primaryOnly = User::factory()->create([
            'primary_branch_id' => $branchA->id,
            'username' => 'branch.access.primary.only',
            'status' => 'active',
        ]);

        $this->assertSame($service->accessibleBranchIds($primaryOnly), $primaryOnly->accessibleBranchIds());
        $this->assertTrue($primaryOnly->canAccessBranch($branchA->id));
        $this->assertFalse($primaryOnly->canAccessBranch($branchB->id));

        $multiBranch = User::factory()->create([
            'primary_branch_id' => $branchA->id,
            'username' => 'branch.access.multi',
            'status' => 'active',
        ]);
        $multiBranch->branches()->attach($branchB->id, ['is_primary' => false]);

        $this->assertSame($service->accessibleBranchIds($multiBranch), $multiBranch->accessibleBranchIds());
        $this->assertTrue($multiBranch->canAccessBranch($branchA->id));
        $this->assertTrue($multiBranch->canAccessBranch($branchB->id));

        $noBranch = User::factory()->create([
            'primary_branch_id' => null,
            'username' => 'branch.access.none',
            'status' => 'active',
        ]);

        $this->assertSame([], $noBranch->accessibleBranchIds());
        $this->assertFalse($noBranch->canAccessBranch($branchA->id));
        $this->assertFalse($noBranch->canAccessBranch(0));

        $superAdmin = User::factory()->create([
            'primary_branch_id' => null,
            'username' => 'branch.access.super',
            'status' => 'active',
        ]);
        $superAdmin->roles()->attach(Role::where('name', 'super-administrator')->firstOrFail());

        $this->assertSame($service->accessibleBranchIds($superAdmin), $superAdmin->accessibleBranchIds());
        $this->assertTrue($superAdmin->canAccessBranch($branchA->id));
        $this->assertTrue($superAdmin->canAccessBranch($branchB->id));
    }

    public function test_branch_scope_bypasses_match_the_reviewed_inventory(): void
    {
        $approved = [
            'app/Http/Controllers/ExpenseController.php',
            'app/Http/Controllers/FinancialTransferController.php',
            'app/Http/Controllers/LoanDisbursementController.php',
            'app/Http/Controllers/LoanPaymentController.php',
        ];
        $found = [];

        foreach (\Illuminate\Support\Facades\File::allFiles(app_path()) as $file) {
            if (! preg_match('/withoutGlobalScope\s*\([^)]*BranchScope::class/', $file->getContents())) {
                continue;
            }

            $relative = str_replace(base_path().DIRECTORY_SEPARATOR, '', $file->getPathname());
            $found[] = str_replace('\\', '/', $relative);
        }

        sort($approved);
        sort($found);

        $this->assertSame(
            $approved,
            $found,
            'BranchScope bypass inventory changed. Review and document every new bypass before updating this allowlist.'
        );
    }

    /** @return array{0: Branch, 1: Branch} */
    private function branches(): array
    {
        $business = Business::firstOrFail();

        return [
            Branch::create(['business_id' => $business->id, 'name' => 'Consistency A', 'status' => 'active']),
            Branch::create(['business_id' => $business->id, 'name' => 'Consistency B', 'status' => 'active']),
        ];
    }
}
