<?php

declare(strict_types=1);

namespace Tests\Feature\Imports;

use App\Enums\AssetStatus;
use App\Enums\AuditEvent;
use App\Enums\ImportStatus;
use App\Models\Asset;
use App\Models\AssetCategory;
use App\Models\AuditLog;
use App\Models\ImportBatch;
use App\Models\Location;
use App\Models\Team;
use App\Models\User;
use App\Services\ImportService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

class ImportManagementTest extends TestCase
{
    use RefreshDatabase;

    private static int $tagSequence = 0;

    // -------------------------------------------------------------- access

    #[Test]
    public function a_guest_cannot_reach_imports(): void
    {
        $this->get(route('imports.index'))->assertRedirect(route('login'));
        $this->get(route('imports.create'))->assertRedirect(route('login'));
    }

    #[Test]
    public function any_signed_in_user_can_view_and_create_imports(): void
    {
        $user = User::factory()->create();

        $this->actingAs($user)->get(route('imports.index'))->assertOk();
        $this->actingAs($user)->get(route('imports.create'))->assertOk();
    }

    #[Test]
    public function the_template_can_be_downloaded(): void
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user)->get(route('imports.template', ['module' => 'assets']));

        $response->assertOk();
        $response->assertHeader('content-type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
    }

    #[Test]
    public function an_unknown_import_module_404s(): void
    {
        $user = User::factory()->create();

        $this->actingAs($user)->get(route('imports.template', ['module' => 'vendors']))->assertNotFound();
    }

    // ------------------------------------------------------------- upload

    #[Test]
    public function uploading_a_valid_file_leads_to_a_preview_with_every_row_ready(): void
    {
        $user = User::factory()->create();
        $category = AssetCategory::factory()->create(['name' => 'Laptops']);

        $file = $this->csvUpload('assets.csv', [
            $this->row(name: 'Dell Latitude 5440', category: $category->name, serial: 'SN-001'),
            $this->row(name: 'HP EliteBook 840', category: $category->name, serial: 'SN-002'),
        ]);

        $response = $this->actingAs($user)->post(route('imports.store'), ['module' => 'assets', 'file' => $file]);

        $batch = ImportBatch::query()->firstOrFail();
        $response->assertRedirect(route('imports.preview', $batch));

        $this->assertSame(2, $batch->total_rows);
        $this->assertSame(ImportStatus::Pending, $batch->status);

        $preview = $this->actingAs($user)->get(route('imports.preview', $batch));
        $preview->assertOk();
        $preview->assertSee('Ready', escape: false);
        $preview->assertDontSee('Error', escape: false);
    }

    #[Test]
    public function a_file_with_the_wrong_columns_is_rejected_immediately(): void
    {
        $user = User::factory()->create();

        $file = UploadedFile::fake()->createWithContent(
            'wrong.csv',
            "Item,Type\nSomething,Other\n",
        );

        $response = $this->actingAs($user)->post(route('imports.store'), ['module' => 'assets', 'file' => $file]);

        $batch = ImportBatch::query()->firstOrFail();
        $response->assertRedirect(route('imports.show', $batch));

        $this->assertSame(ImportStatus::Failed, $batch->status);
        $this->assertNotNull($batch->error_summary);
        $this->assertSame(0, $batch->total_rows);
    }

    // ------------------------------------------------------------ commit

    #[Test]
    public function confirming_a_fully_valid_import_creates_every_asset(): void
    {
        $user = User::factory()->create();
        $category = AssetCategory::factory()->create(['name' => 'Laptops']);

        $file = $this->csvUpload('assets.csv', [
            $this->row(name: 'Dell Latitude 5440', category: $category->name, serial: 'SN-100', assetTag: 'AUM-TEST-100'),
            $this->row(name: 'HP EliteBook 840', category: $category->name, serial: 'SN-101', assetTag: 'AUM-TEST-101'),
        ]);

        $this->actingAs($user)->post(route('imports.store'), ['module' => 'assets', 'file' => $file]);
        $batch = ImportBatch::query()->firstOrFail();

        $response = $this->actingAs($user)->post(route('imports.confirm', $batch));
        $response->assertRedirect(route('imports.show', $batch));

        $batch->refresh();
        $this->assertSame(ImportStatus::Completed, $batch->status);
        $this->assertSame(2, $batch->success_rows);
        $this->assertSame(0, $batch->failed_rows);
        $this->assertNull($batch->error_report_path);

        $this->assertDatabaseHas('assets', ['name' => 'Dell Latitude 5440', 'serial_number' => 'SN-100']);
        $this->assertDatabaseHas('assets', ['name' => 'HP EliteBook 840', 'serial_number' => 'SN-101']);

        $imported = Asset::query()->where('serial_number', 'SN-100')->firstOrFail();
        $this->assertSame(AssetStatus::InStock, $imported->status);
        // Import never auto-generates — the tag is whatever the row supplied.
        $this->assertSame('AUM-TEST-100', $imported->asset_tag);
    }

    #[Test]
    public function a_row_missing_the_asset_tag_is_reported_but_the_rest_still_import(): void
    {
        $user = User::factory()->create();
        $category = AssetCategory::factory()->create();

        $file = $this->csvUpload('assets.csv', [
            $this->row(name: 'No Tag', category: $category->name, serial: 'SN-310', assetTag: ''),
            $this->row(name: 'Good Row', category: $category->name, serial: 'SN-311', assetTag: 'AT-311'),
        ]);

        $this->actingAs($user)->post(route('imports.store'), ['module' => 'assets', 'file' => $file]);
        $batch = ImportBatch::query()->firstOrFail();
        $this->actingAs($user)->post(route('imports.confirm', $batch));

        $batch->refresh();
        $this->assertSame(ImportStatus::CompletedWithErrors, $batch->status);
        $this->assertSame(1, $batch->success_rows);
        $this->assertSame(1, $batch->failed_rows);

        $this->assertDatabaseHas('assets', ['serial_number' => 'SN-311']);
        $this->assertDatabaseMissing('assets', ['serial_number' => 'SN-310']);
    }

    #[Test]
    public function an_asset_tag_duplicated_within_the_file_is_flagged_once(): void
    {
        $user = User::factory()->create();
        $category = AssetCategory::factory()->create();

        $file = $this->csvUpload('assets.csv', [
            $this->row(name: 'First', category: $category->name, serial: 'SN-320', assetTag: 'DUPLICATE-TAG'),
            $this->row(name: 'Second', category: $category->name, serial: 'SN-321', assetTag: 'DUPLICATE-TAG'),
        ]);

        $this->actingAs($user)->post(route('imports.store'), ['module' => 'assets', 'file' => $file]);
        $batch = ImportBatch::query()->firstOrFail();
        $this->actingAs($user)->post(route('imports.confirm', $batch));

        $batch->refresh();
        $this->assertSame(1, $batch->success_rows);
        $this->assertSame(1, $batch->failed_rows);
        $this->assertSame(1, Asset::query()->where('asset_tag', 'DUPLICATE-TAG')->count());
    }

    #[Test]
    public function an_asset_tag_that_already_exists_fails_the_row(): void
    {
        $user = User::factory()->create();
        $category = AssetCategory::factory()->create();
        Asset::factory()->inCategory($category)->create(['asset_tag' => 'EXISTING-TAG']);

        $file = $this->csvUpload('assets.csv', [
            $this->row(name: 'Reused Tag', category: $category->name, serial: 'SN-330', assetTag: 'EXISTING-TAG'),
        ]);

        $this->actingAs($user)->post(route('imports.store'), ['module' => 'assets', 'file' => $file]);
        $batch = ImportBatch::query()->firstOrFail();
        $this->actingAs($user)->post(route('imports.confirm', $batch));

        $this->assertSame(0, $batch->refresh()->success_rows);
        $this->assertSame(1, Asset::query()->where('asset_tag', 'EXISTING-TAG')->count());
    }

    #[Test]
    public function imported_assets_do_not_each_get_their_own_created_audit_entry(): void
    {
        $user = User::factory()->create();
        $category = AssetCategory::factory()->create();

        $file = $this->csvUpload('assets.csv', [
            $this->row(name: 'Dell Latitude 5440', category: $category->name, serial: 'SN-200'),
        ]);

        $this->actingAs($user)->post(route('imports.store'), ['module' => 'assets', 'file' => $file]);
        $batch = ImportBatch::query()->firstOrFail();
        $this->actingAs($user)->post(route('imports.confirm', $batch));

        $asset = Asset::query()->where('serial_number', 'SN-200')->firstOrFail();
        $this->assertSame(
            0,
            AuditLog::query()->where('event', 'created')->where('auditable_type', $asset->getMorphClass())->count(),
        );

        $imported = AuditLog::query()->where('event', AuditEvent::Imported->value)->firstOrFail();
        $this->assertStringContainsString('1 of 1', $imported->description);
    }

    #[Test]
    public function a_row_missing_the_asset_name_is_reported_but_the_rest_still_import(): void
    {
        $user = User::factory()->create();
        $category = AssetCategory::factory()->create();

        $file = $this->csvUpload('assets.csv', [
            $this->row(name: '', category: $category->name, serial: 'SN-300'),
            $this->row(name: 'Good Row', category: $category->name, serial: 'SN-301'),
        ]);

        $this->actingAs($user)->post(route('imports.store'), ['module' => 'assets', 'file' => $file]);
        $batch = ImportBatch::query()->firstOrFail();
        $this->actingAs($user)->post(route('imports.confirm', $batch));

        $batch->refresh();
        $this->assertSame(ImportStatus::CompletedWithErrors, $batch->status);
        $this->assertSame(1, $batch->success_rows);
        $this->assertSame(1, $batch->failed_rows);
        $this->assertNotNull($batch->error_report_path);

        $this->assertDatabaseHas('assets', ['serial_number' => 'SN-301']);
        $this->assertDatabaseMissing('assets', ['serial_number' => 'SN-300']);

        $this->actingAs($user)->get(route('imports.errors', $batch))->assertOk();
    }

    #[Test]
    public function an_unknown_category_name_fails_the_row(): void
    {
        $user = User::factory()->create();

        $file = $this->csvUpload('assets.csv', [
            $this->row(name: 'Mystery Item', category: 'Does Not Exist', serial: 'SN-400'),
        ]);

        $this->actingAs($user)->post(route('imports.store'), ['module' => 'assets', 'file' => $file]);
        $batch = ImportBatch::query()->firstOrFail();

        $preview = $this->actingAs($user)->get(route('imports.preview', $batch));
        $preview->assertSee('Does Not Exist');
        $preview->assertSee('was not found');
    }

    #[Test]
    public function a_serial_number_duplicated_within_the_file_is_flagged_once(): void
    {
        $user = User::factory()->create();
        $category = AssetCategory::factory()->create();

        $file = $this->csvUpload('assets.csv', [
            $this->row(name: 'First', category: $category->name, serial: 'DUPLICATE'),
            $this->row(name: 'Second', category: $category->name, serial: 'DUPLICATE'),
        ]);

        $this->actingAs($user)->post(route('imports.store'), ['module' => 'assets', 'file' => $file]);
        $batch = ImportBatch::query()->firstOrFail();
        $this->actingAs($user)->post(route('imports.confirm', $batch));

        $batch->refresh();
        $this->assertSame(1, $batch->success_rows);
        $this->assertSame(1, $batch->failed_rows);
        $this->assertSame(1, Asset::query()->where('serial_number', 'DUPLICATE')->count());
    }

    #[Test]
    public function a_serial_number_that_already_exists_fails_the_row(): void
    {
        $user = User::factory()->create();
        $category = AssetCategory::factory()->create();
        Asset::factory()->inCategory($category)->create(['serial_number' => 'EXISTING-SN']);

        $file = $this->csvUpload('assets.csv', [
            $this->row(name: 'Reused Serial', category: $category->name, serial: 'EXISTING-SN'),
        ]);

        $this->actingAs($user)->post(route('imports.store'), ['module' => 'assets', 'file' => $file]);
        $batch = ImportBatch::query()->firstOrFail();
        $this->actingAs($user)->post(route('imports.confirm', $batch));

        $this->assertSame(0, $batch->refresh()->success_rows);
        $this->assertSame(1, Asset::query()->where('serial_number', 'EXISTING-SN')->count());
    }

    #[Test]
    public function a_mac_address_is_imported_and_uppercased(): void
    {
        $user = User::factory()->create();
        $category = AssetCategory::factory()->create();

        // No format is enforced on import either — whatever's in the sheet
        // is kept as typed, only uppercased for consistent uniqueness.
        $row = $this->row(name: 'MAC Asset', category: $category->name, serial: 'SN-MAC-1');
        $row[4] = 'se5r46h5dh65';

        $file = $this->csvUpload('assets.csv', [$row]);

        $this->actingAs($user)->post(route('imports.store'), ['module' => 'assets', 'file' => $file]);
        $batch = ImportBatch::query()->firstOrFail();
        $this->actingAs($user)->post(route('imports.confirm', $batch));

        $this->assertSame(1, $batch->refresh()->success_rows);
        $this->assertDatabaseHas('assets', [
            'name' => 'MAC Asset',
            'mac_address' => 'SE5R46H5DH65',
        ]);
    }

    #[Test]
    public function a_duplicate_mac_address_within_the_file_is_flagged_once(): void
    {
        $user = User::factory()->create();
        $category = AssetCategory::factory()->create();

        $first = $this->row(name: 'First MAC', category: $category->name, serial: 'SN-MAC-A');
        $first[4] = 'AA:BB:CC:DD:EE:FF';

        $second = $this->row(name: 'Second MAC', category: $category->name, serial: 'SN-MAC-B');
        $second[4] = 'AA:BB:CC:DD:EE:FF';

        $file = $this->csvUpload('assets.csv', [$first, $second]);

        $this->actingAs($user)->post(route('imports.store'), ['module' => 'assets', 'file' => $file]);
        $batch = ImportBatch::query()->firstOrFail();
        $this->actingAs($user)->post(route('imports.confirm', $batch));

        $batch->refresh();
        $this->assertSame(1, $batch->success_rows);
        $this->assertSame(1, $batch->failed_rows);
        $this->assertSame(1, Asset::query()->where('mac_address', 'AA:BB:CC:DD:EE:FF')->count());
    }

    #[Test]
    public function specifications_are_parsed_from_key_value_pairs(): void
    {
        $user = User::factory()->create();
        $category = AssetCategory::factory()->create();

        $file = $this->csvUpload('assets.csv', [
            $this->row(
                name: 'Spec Laptop',
                category: $category->name,
                serial: 'SN-SPEC-1',
                specifications: 'RAM: 16GB; CPU: Intel i7; Storage: 512GB SSD',
            ),
        ]);

        $this->actingAs($user)->post(route('imports.store'), ['module' => 'assets', 'file' => $file]);
        $batch = ImportBatch::query()->firstOrFail();
        $this->actingAs($user)->post(route('imports.confirm', $batch));

        $this->assertSame(1, $batch->refresh()->success_rows);

        $imported = Asset::query()->where('serial_number', 'SN-SPEC-1')->firstOrFail();
        // MySQL's JSON column does not guarantee key order is preserved, so
        // this compares content, not order.
        $this->assertEqualsCanonicalizing([
            'RAM' => '16GB',
            'CPU' => 'Intel i7',
            'Storage' => '512GB SSD',
        ], $imported->specifications);
    }

    #[Test]
    public function a_malformed_specifications_cell_fails_the_row(): void
    {
        $user = User::factory()->create();
        $category = AssetCategory::factory()->create();

        $file = $this->csvUpload('assets.csv', [
            $this->row(
                name: 'Bad Spec Laptop',
                category: $category->name,
                serial: 'SN-SPEC-2',
                specifications: 'RAM 16GB',
            ),
        ]);

        $this->actingAs($user)->post(route('imports.store'), ['module' => 'assets', 'file' => $file]);
        $batch = ImportBatch::query()->firstOrFail();
        $this->actingAs($user)->post(route('imports.confirm', $batch));

        $this->assertSame(0, $batch->refresh()->success_rows);
        $this->assertDatabaseMissing('assets', ['serial_number' => 'SN-SPEC-2']);
    }

    #[Test]
    public function nothing_is_created_when_every_row_fails(): void
    {
        $user = User::factory()->create();

        $file = $this->csvUpload('assets.csv', [
            $this->row(name: '', category: '', serial: null),
        ]);

        $this->actingAs($user)->post(route('imports.store'), ['module' => 'assets', 'file' => $file]);
        $batch = ImportBatch::query()->firstOrFail();
        $this->actingAs($user)->post(route('imports.confirm', $batch));

        $batch->refresh();
        $this->assertSame(ImportStatus::Failed, $batch->status);
        $this->assertSame(0, $batch->success_rows);
        $this->assertSame(0, Asset::query()->count());
    }

    #[Test]
    public function confirming_an_already_finished_batch_does_not_reprocess_it(): void
    {
        $user = User::factory()->create();
        $category = AssetCategory::factory()->create();

        $file = $this->csvUpload('assets.csv', [
            $this->row(name: 'Once Only', category: $category->name, serial: 'SN-500'),
        ]);

        $this->actingAs($user)->post(route('imports.store'), ['module' => 'assets', 'file' => $file]);
        $batch = ImportBatch::query()->firstOrFail();
        $this->actingAs($user)->post(route('imports.confirm', $batch));

        $this->actingAs($user)->post(route('imports.confirm', $batch));

        $this->assertSame(1, Asset::query()->where('serial_number', 'SN-500')->count());
    }

    #[Test]
    public function location_and_team_names_are_resolved(): void
    {
        $user = User::factory()->create();
        $category = AssetCategory::factory()->create();
        $location = Location::factory()->create(['name' => 'Head Office']);
        $team = Team::factory()->create(['name' => 'Engineering']);

        $file = $this->csvUpload('assets.csv', [
            $this->row(
                name: 'Located Asset',
                category: $category->name,
                serial: 'SN-600',
                location: $location->name,
                team: $team->name,
            ),
        ]);

        $this->actingAs($user)->post(route('imports.store'), ['module' => 'assets', 'file' => $file]);
        $batch = ImportBatch::query()->firstOrFail();
        $this->actingAs($user)->post(route('imports.confirm', $batch));

        $imported = Asset::query()->where('serial_number', 'SN-600')->firstOrFail();
        $this->assertSame($location->id, $imported->location_id);
        $this->assertSame($team->id, $imported->team_id);
    }

    #[Test]
    public function a_file_over_the_row_cap_is_rejected(): void
    {
        $user = User::factory()->create();
        $category = AssetCategory::factory()->create();

        $rows = [];
        for ($i = 0; $i < ImportService::MAX_ROWS + 1; $i++) {
            $rows[] = $this->row(name: 'Row '.$i, category: $category->name, serial: null);
        }

        $file = $this->csvUpload('huge.csv', $rows);

        $this->actingAs($user)->post(route('imports.store'), ['module' => 'assets', 'file' => $file]);
        $batch = ImportBatch::query()->firstOrFail();

        $this->assertSame(ImportStatus::Failed, $batch->status);
        $this->assertStringContainsString((string) ImportService::MAX_ROWS, (string) $batch->error_summary);
    }

    // ------------------------------------------------------------- history

    #[Test]
    public function the_import_history_lists_past_batches(): void
    {
        $user = User::factory()->create();
        ImportBatch::factory()->for($user)->completed()->create(['original_filename' => 'completed.xlsx']);
        ImportBatch::factory()->for($user)->failed()->create(['original_filename' => 'failed.xlsx']);

        $response = $this->actingAs($user)->get(route('imports.index'));

        $response->assertOk();
        $response->assertSee('completed.xlsx');
        $response->assertSee('failed.xlsx');
    }

    #[Test]
    public function the_import_history_can_be_filtered_by_status(): void
    {
        $user = User::factory()->create();
        ImportBatch::factory()->for($user)->completed()->create(['original_filename' => 'completed.xlsx']);
        ImportBatch::factory()->for($user)->failed()->create(['original_filename' => 'failed.xlsx']);

        $response = $this->actingAs($user)->get(route('imports.index', ['status' => 'failed']));

        $response->assertOk();
        $response->assertSee('failed.xlsx');
        $response->assertDontSee('completed.xlsx');
    }

    /**
     * @param  array<int, array<string, mixed>>  $rows
     */
    private function csvUpload(string $filename, array $rows): UploadedFile
    {
        $lines = [implode(',', ImportService::ASSET_HEADINGS)];

        foreach ($rows as $row) {
            $lines[] = implode(',', array_map(
                fn (mixed $value): string => '"'.str_replace('"', '""', (string) ($value ?? '')).'"',
                $row,
            ));
        }

        return UploadedFile::fake()->createWithContent($filename, implode("\n", $lines));
    }

    /**
     * @return array<string, mixed>
     */
    private function row(
        string $name,
        string $category,
        ?string $serial,
        ?string $location = null,
        ?string $team = null,
        ?string $assetTag = null,
        ?string $specifications = null,
    ): array {
        // Import never auto-generates a tag, so every row needs one — a
        // sequence-based default keeps callers that don't care about the
        // exact value collision-free without having to invent one each time.
        $assetTag ??= 'AT-'.str_pad((string) (++self::$tagSequence), 5, '0', STR_PAD_LEFT);

        return [
            $assetTag,
            $name,
            $category,
            $serial,
            '',
            'Dell',
            'Latitude 5440',
            'Acme Traders',
            $location,
            $team,
            'In Stock',
            'Good',
            '15-01-2025',
            '85000',
            'INR',
            '',
            '',
            '',
            $specifications,
        ];
    }
}
