<?php

use App\Enums\ImportStatus;
use App\Jobs\ProcessImportChunkJob;
use App\Jobs\ValidateImportJob;
use App\Models\Branch;
use App\Models\ImportJob;
use App\Models\Member;
use App\Models\User;
use App\Services\ImportRowValidator;
use App\Services\MemberCsvColumns;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;

beforeEach(function () {
    Storage::fake('local');
});

/**
 * Builds a CSV string from the template columns; each row is an assoc
 * array of column => value (missing columns become empty cells).
 */
function buildCsv(array $rows): string
{
    $columns = MemberCsvColumns::templateColumns();
    $lines = [implode(',', $columns)];

    foreach ($rows as $row) {
        $line = array_map(function (string $column) use ($row) {
            $value = $row[$column] ?? '';

            return str_contains((string) $value, ',') ? '"'.$value.'"' : $value;
        }, $columns);
        $lines[] = implode(',', $line);
    }

    return implode("\n", $lines)."\n";
}

/**
 * Drives the import pipeline synchronously by calling the job handle()
 * methods directly (no live queue worker) until the job reaches a
 * terminal status. Mirrors what queue:work would do, just inline.
 */
function runImportSynchronously(int $importJobId): void
{
    (new ValidateImportJob($importJobId))->handle();

    $job = ImportJob::find($importJobId);
    $startRow = 0;

    while ($job->status === ImportStatus::Processing) {
        (new ProcessImportChunkJob($importJobId, $startRow))->handle(new ImportRowValidator);
        $job->refresh();
        $startRow += 500;
    }
}

test('data_clerk cannot import members', function () {
    $branch = Branch::factory()->create();
    $clerk = User::factory()->dataClerk()->create(['branch_id' => $branch->id]);

    $file = UploadedFile::fake()->createWithContent('members.csv', buildCsv([
        ['first_name' => 'Tendai', 'last_name' => 'Moyo', 'gender' => 'male'],
    ]));

    $this->actingAs($clerk)->postJson('/api/v1/imports', [
        'branch_id' => $branch->id,
        'file' => $file,
    ])->assertStatus(403);
});

test('branch_admin cannot import into another branch', function () {
    $ownBranch = Branch::factory()->create();
    $otherBranch = Branch::factory()->create();
    $admin = User::factory()->branchAdmin()->create(['branch_id' => $ownBranch->id]);

    $file = UploadedFile::fake()->createWithContent('members.csv', buildCsv([
        ['first_name' => 'Tendai', 'last_name' => 'Moyo', 'gender' => 'male'],
    ]));

    $this->actingAs($admin)->postJson('/api/v1/imports', [
        'branch_id' => $otherBranch->id,
        'file' => $file,
    ])->assertStatus(403);
});

test('a non-super_admin cannot omit branch_id for a multi-branch import', function () {
    $branch = Branch::factory()->create();
    $admin = User::factory()->branchAdmin()->create(['branch_id' => $branch->id]);

    $file = UploadedFile::fake()->createWithContent('members.csv', buildCsv([
        ['first_name' => 'Tendai', 'last_name' => 'Moyo', 'gender' => 'male', 'branch_code' => $branch->code],
    ]));

    $this->actingAs($admin)->postJson('/api/v1/imports', ['file' => $file])->assertStatus(403);
});

test('uploading a valid CSV dispatches the validation job', function () {
    Queue::fake();

    $branch = Branch::factory()->create();
    $admin = User::factory()->branchAdmin()->create(['branch_id' => $branch->id]);

    $file = UploadedFile::fake()->createWithContent('members.csv', buildCsv([
        ['first_name' => 'Tendai', 'last_name' => 'Moyo', 'gender' => 'male'],
    ]));

    $response = $this->actingAs($admin)->postJson('/api/v1/imports', [
        'branch_id' => $branch->id,
        'file' => $file,
    ]);

    $response->assertStatus(202)->assertJsonPath('data.status', 'pending');
    Queue::assertPushed(ValidateImportJob::class);
    $this->assertDatabaseHas('import_jobs', ['branch_id' => $branch->id, 'status' => 'pending']);
});

test('a 1000-row CSV with 950 valid and 50 invalid rows imports correctly end to end', function () {
    $branch = Branch::factory()->create();
    $admin = User::factory()->branchAdmin()->create(['branch_id' => $branch->id]);

    $rows = [];

    for ($i = 1; $i <= 950; $i++) {
        $rows[] = [
            'first_name' => 'First'.$i,
            'last_name' => 'Last'.$i,
            'gender' => $i % 2 === 0 ? 'male' : 'female',
            'phone_primary' => '077'.str_pad((string) $i, 7, '0', STR_PAD_LEFT),
        ];
    }

    for ($i = 1; $i <= 50; $i++) {
        // Missing required last_name makes the row invalid.
        $rows[] = [
            'first_name' => 'Invalid'.$i,
            'last_name' => '',
            'gender' => 'male',
        ];
    }

    $file = UploadedFile::fake()->createWithContent('members.csv', buildCsv($rows));

    $response = $this->actingAs($admin)->postJson('/api/v1/imports', [
        'branch_id' => $branch->id,
        'file' => $file,
    ]);
    $response->assertStatus(202);
    $importJobId = $response->json('data.id');

    runImportSynchronously($importJobId);

    $job = ImportJob::find($importJobId);
    expect($job->status)->toBe(ImportStatus::CompletedWithErrors);
    expect($job->total_rows)->toBe(1000);
    expect($job->created_rows)->toBe(950);
    expect($job->failed_rows)->toBe(50);
    expect(Member::where('branch_id', $branch->id)->count())->toBe(950);

    // Every created member got a number, an initial status history row, and an import audit row.
    $sample = Member::where('branch_id', $branch->id)->first();
    expect($sample->member_number)->toMatch('/^M\d{8}$/');
    $this->assertDatabaseHas('member_status_histories', ['member_id' => $sample->id, 'from_status' => null]);
    $this->assertDatabaseHas('audit_logs', ['auditable_id' => $sample->id, 'action' => 'imported']);

    expect($job->error_report_path)->not->toBeNull();
    $errorsCsv = Storage::disk('local')->get($job->error_report_path);
    $errorLines = array_filter(explode("\n", trim($errorsCsv)));
    expect($errorLines)->toHaveCount(51); // header + 50 error rows
    expect($errorsCsv)->toContain('row_number')->toContain('error');
});

test('dedupe skips a row whose phone already exists in the branch', function () {
    $branch = Branch::factory()->create();
    $admin = User::factory()->branchAdmin()->create(['branch_id' => $branch->id]);
    Member::factory()->create(['branch_id' => $branch->id, 'phone_primary' => '0771234567']);

    $file = UploadedFile::fake()->createWithContent('members.csv', buildCsv([
        ['first_name' => 'Dup', 'last_name' => 'Licate', 'gender' => 'male', 'phone_primary' => '0771234567'],
        ['first_name' => 'Fresh', 'last_name' => 'Person', 'gender' => 'female', 'phone_primary' => '0779999999'],
    ]));

    $response = $this->actingAs($admin)->postJson('/api/v1/imports', [
        'branch_id' => $branch->id,
        'file' => $file,
        'options' => ['dedupe' => 'phone_or_email'],
    ]);
    $importJobId = $response->json('data.id');

    runImportSynchronously($importJobId);

    $job = ImportJob::find($importJobId);
    expect($job->created_rows)->toBe(1);
    expect($job->failed_rows)->toBe(1);
    expect(Member::where('branch_id', $branch->id)->where('first_name', 'Fresh')->exists())->toBeTrue();
    expect(Member::where('branch_id', $branch->id)->where('first_name', 'Dup')->exists())->toBeFalse();
});

test('super_admin can import a multi-branch CSV using branch_code', function () {
    $branchA = Branch::factory()->create(['code' => 'BR-A']);
    $branchB = Branch::factory()->create(['code' => 'BR-B']);
    $superAdmin = User::factory()->superAdmin()->create();

    $file = UploadedFile::fake()->createWithContent('members.csv', buildCsv([
        ['first_name' => 'InA', 'last_name' => 'Member', 'gender' => 'male', 'branch_code' => 'BR-A'],
        ['first_name' => 'InB', 'last_name' => 'Member', 'gender' => 'female', 'branch_code' => 'BR-B'],
        ['first_name' => 'Unknown', 'last_name' => 'Code', 'gender' => 'male', 'branch_code' => 'NOPE'],
    ]));

    $response = $this->actingAs($superAdmin)->postJson('/api/v1/imports', ['file' => $file]);
    $importJobId = $response->json('data.id');

    runImportSynchronously($importJobId);

    $job = ImportJob::find($importJobId);
    expect($job->created_rows)->toBe(2);
    expect($job->failed_rows)->toBe(1);
    expect(Member::where('branch_id', $branchA->id)->where('first_name', 'InA')->exists())->toBeTrue();
    expect(Member::where('branch_id', $branchB->id)->where('first_name', 'InB')->exists())->toBeTrue();
});

test('the error report contains the correct 1-based CSV line number', function () {
    $branch = Branch::factory()->create();
    $admin = User::factory()->branchAdmin()->create(['branch_id' => $branch->id]);

    $file = UploadedFile::fake()->createWithContent('members.csv', buildCsv([
        ['first_name' => 'Valid', 'last_name' => 'One', 'gender' => 'male'],
        ['first_name' => 'Invalid', 'last_name' => '', 'gender' => 'male'],
    ]));

    $response = $this->actingAs($admin)->postJson('/api/v1/imports', [
        'branch_id' => $branch->id,
        'file' => $file,
    ]);
    $importJobId = $response->json('data.id');

    runImportSynchronously($importJobId);

    $job = ImportJob::find($importJobId);
    $errorsCsv = Storage::disk('local')->get($job->error_report_path);
    // Header is CSV line 1, "Valid One" is line 2, "Invalid" (the failing row) is line 3.
    expect($errorsCsv)->toContain(',3,'); // row_number column holds 3
});

test('the CSV template downloads with the expected header', function () {
    $branch = Branch::factory()->create();
    $admin = User::factory()->branchAdmin()->create(['branch_id' => $branch->id]);

    $response = $this->actingAs($admin)->get('/api/v1/imports/template');

    $response->assertOk();
    expect($response->getContent())->toContain('first_name,last_name');
});

test('branch_admin of branch A gets 404 on a branch B import job', function () {
    $branchA = Branch::factory()->create();
    $branchB = Branch::factory()->create();
    $importB = ImportJob::factory()->create(['branch_id' => $branchB->id]);
    $adminA = User::factory()->branchAdmin()->create(['branch_id' => $branchA->id]);

    $this->actingAs($adminA)->getJson("/api/v1/imports/{$importB->id}")->assertStatus(404);
});

test('unauthenticated request to list imports is rejected', function () {
    $this->getJson('/api/v1/imports')->assertStatus(401);
});

test('the import job chain drains correctly through a real queue:work loop', function () {
    $branch = Branch::factory()->create();
    $admin = User::factory()->branchAdmin()->create(['branch_id' => $branch->id]);

    $file = UploadedFile::fake()->createWithContent('members.csv', buildCsv([
        ['first_name' => 'Real', 'last_name' => 'Queue', 'gender' => 'male'],
        ['first_name' => 'Second', 'last_name' => 'Row', 'gender' => 'female'],
    ]));

    $response = $this->actingAs($admin)->postJson('/api/v1/imports', [
        'branch_id' => $branch->id,
        'file' => $file,
    ]);
    $importJobId = $response->json('data.id');

    // Actually dispatched onto the database queue (no Queue::fake here) —
    // drain it exactly the way the cron-driven worker would.
    for ($i = 0; $i < 10; $i++) {
        $this->artisan('queue:work', ['--once' => true, '--stop-when-empty' => true]);
    }

    $job = ImportJob::find($importJobId);
    expect($job->status)->toBe(ImportStatus::Completed);
    expect($job->created_rows)->toBe(2);
    expect(Member::where('branch_id', $branch->id)->count())->toBe(2);
});
