<?php

namespace Tests\Feature\Importing\Api;

use App\Models\Actionlog as ActionLog;
use App\Models\Asset;
use App\Models\Company;
use App\Models\CustomField;
use App\Models\Import;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Testing\TestResponse;
use PHPUnit\Framework\Attributes\Test;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\Support\Importing\AssetsImportFileBuilder as ImportFileBuilder;
use Tests\Support\Importing\CleansUpImportFiles;

class ImportAssetsTest extends ImportDataTestCase implements TestsPermissionsRequirement
{
    use CleansUpImportFiles;
    use WithFaker;

    protected function importFileResponse(array $parameters = []): TestResponse
    {
        if (! array_key_exists('import-type', $parameters)) {
            $parameters['import-type'] = 'asset';
        }

        return parent::importFileResponse($parameters);
    }

    #[Test]
    public function test_requires_permission()
    {
        $this->actingAsForApi(User::factory()->create());

        $this->importFileResponse(['import' => 44])->assertForbidden();
    }

    #[Test]
    public function user_with_import_assets_permission_can_import_assets(): void
    {
        $this->actingAsForApi(User::factory()->canImport()->create());

        $import = Import::factory()->asset()->create();

        $this->importFileResponse(['import' => $import->id])->assertOk();
    }

    #[Test]
    public function import_asset(): void
    {

        $importFileBuilder = ImportFileBuilder::new();
        $row = $importFileBuilder->firstRow();
        $import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);

        $this->actingAsForApi(User::factory()->superuser()->create());
        $this->importFileResponse(['import' => $import->id])
            ->assertOk()
            ->assertExactJson([
                'payload' => ['tally' => ['created' => 1, 'updated' => 0, 'skipped' => 0, 'errored' => 0]],
                'status' => 'success',
                'messages' => ['redirect_url' => route('hardware.index')],
            ]);

        $newAsset = Asset::query()
            ->with(['location', 'supplier', 'company', 'assignedAssets', 'defaultLoc', 'status', 'model.category', 'model.manufacturer'])
            ->where('serial', $row['serialNumber'])
            ->sole();

        $assignee = User::query()->find($newAsset->assigned_to, ['id', 'first_name', 'last_name', 'email', 'username']);

        $activityLogs = ActionLog::query()
            ->where('item_type', Asset::class)
            ->where('item_id', $newAsset->id)
            ->get();

        $this->assertCount(2, $activityLogs);

        $this->assertEquals('checkout', $activityLogs[0]->action_type);
        $this->assertEquals(Asset::class, $activityLogs[0]->item_type);
        $this->assertEquals($assignee->id, $activityLogs[0]->target_id);
        $this->assertEquals(User::class, $activityLogs[0]->target_type);
        $this->assertEquals('Checkout from CSV Importer', $activityLogs[0]->note);
        $this->assertHasTheseActionLogs($newAsset, ['create', 'checkout']); // TODO - order reversed but passes?!

        $this->assertEquals('create', $activityLogs[1]->action_type);
        $this->assertNull($activityLogs[1]->target_id);
        $this->assertEquals(Asset::class, $activityLogs[1]->item_type);
        $this->assertNull($activityLogs[1]->note);
        $this->assertNull($activityLogs[1]->target_type);

        $this->assertEquals($row['assigneeFullName'], "{$assignee->first_name} {$assignee->last_name}");
        $this->assertEquals($row['assigneeEmail'], $assignee->email);
        $this->assertEquals($row['assigneeUsername'], $assignee->username);

        $this->assertEquals($row['category'], $newAsset->model->category->name);
        $this->assertEquals($row['manufacturerName'], $newAsset->model->manufacturer->name);
        $this->assertEquals($row['itemName'], $newAsset->name);
        $this->assertEquals($row['tag'], $newAsset->asset_tag);
        $this->assertEquals($row['model'], $newAsset->model->name);
        $this->assertEquals($row['modelNumber'], $newAsset->model->model_number);
        $this->assertEquals($row['purchaseDate'], $newAsset->purchase_date->toDateString());
        $this->assertNull($newAsset->asset_eol_date);
        $this->assertEquals(0, $newAsset->eol_explicit);
        $this->assertEquals($newAsset->location_id, $newAsset->rtd_location_id);
        $this->assertEquals($row['purchaseCost'], $newAsset->purchase_cost);
        $this->assertNull($newAsset->order_number);
        $this->assertEquals('', $newAsset->image);
        $this->assertNull($newAsset->user_id);
        $this->assertEquals(1, $newAsset->physical);
        $this->assertEquals($row['status'], $newAsset->status->name);
        $this->assertEquals(0, $newAsset->archived);
        $this->assertEquals($row['warrantyInMonths'], $newAsset->warranty_months);
        $this->assertNull($newAsset->deprecate);
        $this->assertEquals($row['supplierName'], $newAsset->supplier->name);
        $this->assertEquals(0, $newAsset->requestable);
        $this->assertEquals($row['location'], $newAsset->defaultLoc->name);
        $this->assertEquals(null, $newAsset->accepted);
        $this->assertEquals(now()->toDateString(), Carbon::parse($newAsset->last_checkout)->toDateString());
        $this->assertEquals(0, $newAsset->last_checkin);
        $this->assertEquals(0, $newAsset->expected_checkin);
        $this->assertEquals($row['companyName'], $newAsset->company->name);
        $this->assertEquals(User::class, $newAsset->assigned_type);
        $this->assertNull($newAsset->last_audit_date);
        $this->assertNull($newAsset->next_audit_date);
        $this->assertEquals($row['location'], $newAsset->location->name);
        $this->assertEquals(0, $newAsset->checkin_counter);
        $this->assertEquals(1, $newAsset->checkout_counter);
        $this->assertEquals(0, $newAsset->requests_counter);
        $this->assertEquals(0, $newAsset->byod);

        // Notes is never read.
        // $this->assertEquals($row['notes'], $newAsset->notes);

    }

    #[Test]
    public function will_ignore_unknown_columns_when_file_contains_unknown_columns(): void
    {
        $row = ImportFileBuilder::new()->definition();
        $row['unknownColumnInCsvFile'] = 'foo';

        $importFileBuilder = new ImportFileBuilder([$row]);

        $this->actingAsForApi(User::factory()->superuser()->create());

        $import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);

        $this->importFileResponse(['import' => $import->id])->assertOk();
    }

    #[Test]
    public function will_not_create_new_asset_when_asset_with_same_tag_already_exists(): void
    {
        $asset = Asset::factory()->create(['asset_tag' => $this->faker->uuid]);
        $importFileBuilder = ImportFileBuilder::times(4)->replace(['tag' => $asset->asset_tag]);
        $import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);

        $this->actingAsForApi(User::factory()->superuser()->create());
        $this->importFileResponse(['import' => $import->id])
            ->assertInternalServerError()
            ->assertExactJson([
                'status' => 'import-errors',
                'payload' => null,
                'messages' => [
                    '' => [
                        'asset_tag' => [
                            'asset_tag' => [
                                "An asset with the asset tag {$asset->asset_tag} already exists and an update was not requested. No change was made.",
                            ],
                        ],
                    ],
                ],
            ]);

        $assetsWithSameTag = Asset::query()->where('asset_tag', $asset->asset_tag)->get();

        $this->assertCount(1, $assetsWithSameTag);
    }

    #[Test]
    public function will_not_create_new_company_when_company_exists(): void
    {
        $importFileBuilder = ImportFileBuilder::times(4)->replace(['companyName' => Str::random()]);
        $import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);

        $this->actingAsForApi(User::factory()->superuser()->create());
        $this->importFileResponse(['import' => $import->id])->assertOk();

        $newAssets = Asset::query()
            ->whereIn('serial', $importFileBuilder->pluck('serialNumber'))
            ->get();

        $this->assertCount(1, $newAssets->pluck('company_id')->unique()->all());
    }

    #[Test]
    public function will_not_create_new_location_when_location_exists(): void
    {
        $importFileBuilder = ImportFileBuilder::times(4)->replace(['location' => Str::random()]);
        $import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);

        $this->actingAsForApi(User::factory()->superuser()->create());
        $this->importFileResponse(['import' => $import->id])->assertOk();

        $newAssets = Asset::query()
            ->whereIn('serial', $importFileBuilder->pluck('serialNumber'))
            ->get();

        $this->assertCount(1, $newAssets->pluck('location_id')->unique()->all());
    }

    #[Test]
    public function will_not_create_new_supplier_when_supplier_exists(): void
    {
        $importFileBuilder = ImportFileBuilder::times(4)->replace(['supplierName' => $this->faker->company]);
        $import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);

        $this->actingAsForApi(User::factory()->superuser()->create());
        $this->importFileResponse(['import' => $import->id])->assertOk();

        $newAssets = Asset::query()
            ->whereIn('serial', $importFileBuilder->pluck('serialNumber'))
            ->get(['supplier_id']);

        $this->assertCount(1, $newAssets->pluck('supplier_id')->unique()->all());
    }

    #[Test]
    public function will_not_create_new_manufacturer_when_manufacturer_exists(): void
    {
        $importFileBuilder = ImportFileBuilder::times(4)->replace(['manufacturerName' => $this->faker->company]);
        $import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);

        $this->actingAsForApi(User::factory()->superuser()->create());
        $this->importFileResponse(['import' => $import->id])->assertOk();

        $newAssets = Asset::query()
            ->with('model.manufacturer')
            ->whereIn('serial', $importFileBuilder->pluck('serialNumber'))
            ->get();

        $this->assertCount(1, $newAssets->pluck('model.manufacturer_id')->unique()->all());
    }

    #[Test]
    public function will_not_create_category_when_category_exists(): void
    {
        $importFileBuilder = ImportFileBuilder::times(4)->replace(['category' => $this->faker->company]);
        $import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);

        $this->actingAsForApi(User::factory()->superuser()->create());
        $this->importFileResponse(['import' => $import->id])->assertOk();

        $newAssets = Asset::query()
            ->with('model.category')
            ->whereIn('serial', $importFileBuilder->pluck('serialNumber'))
            ->get();

        $this->assertCount(1, $newAssets->pluck('model.category_id')->unique()->all());
    }

    #[Test]
    public function will_not_create_new_asset_model_when_asset_model_exists(): void
    {
        $importFileBuilder = ImportFileBuilder::times(4)->replace(['model' => Str::random()]);
        $import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);

        $this->actingAsForApi(User::factory()->superuser()->create());
        $this->importFileResponse(['import' => $import->id])->assertOk();

        $newAssets = Asset::query()
            ->with('model')
            ->whereIn('serial', $importFileBuilder->pluck('serialNumber'))
            ->get();

        $this->assertCount(1, $newAssets->pluck('model.name')->unique()->all());
    }

    #[Test]
    public function when_columns_are_missing_in_import_file(): void
    {
        $importFileBuilder = ImportFileBuilder::times()->forget([
            'purchaseCost',
            'purchaseDate',
            'status',
        ]);

        $import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);

        $this->actingAsForApi(User::factory()->superuser()->create());
        $this->importFileResponse(['import' => $import->id])->assertOk();

        $newAsset = Asset::query()
            ->with(['status'])
            ->where('serial', $importFileBuilder->firstRow()['serialNumber'])
            ->sole();

        $this->assertEquals('Ready to Deploy', $newAsset->status->name);
        $this->assertNull($newAsset->purchase_date);
        $this->assertNull($newAsset->purchase_cost);
    }

    #[Test]
    public function will_format_values(): void
    {
        $importFileBuilder = ImportFileBuilder::new([
            'warrantyInMonths' => '3 months',
            'purchaseDate' => '2022/10/10',
        ]);

        $import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);

        $this->actingAsForApi(User::factory()->superuser()->create());
        $this->importFileResponse(['import' => $import->id])->assertOk();

        $newAsset = Asset::query()
            ->where('serial', $importFileBuilder->firstRow()['serialNumber'])
            ->sole();

        $this->assertEquals(3, $newAsset->warranty_months);
        $this->assertEquals('2022-10-10', $newAsset->purchase_date->toDateString());
    }

    #[Test]
    public function when_required_columns_are_missing_in_import_file(): void
    {
        $importFileBuilder = ImportFileBuilder::times(2)
            ->forget(['tag'])
            ->replace(['model' => '']);

        $rows = $importFileBuilder->all();
        $import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);

        $this->actingAsForApi(User::factory()->superuser()->create());
        $this->importFileResponse(['import' => $import->id])
            ->assertInternalServerError()
            ->assertJson([
                'status' => 'import-errors',
                'payload' => ['tally' => ['created' => 0, 'updated' => 0, 'skipped' => 0, 'errored' => 2]],
                'messages' => [
                    $rows[0]['itemName'] => [
                        "Asset \"{$rows[0]['itemName']}\"" => [
                            'asset_tag' => [
                                'The asset tag field must be at least 1 characters.',
                            ],
                            'model_id' => [
                                'The model id field is required.',
                            ],
                        ],
                    ],
                    $rows[1]['itemName'] => [
                        "Asset \"{$rows[1]['itemName']}\"" => [
                            'asset_tag' => [
                                'The asset tag field must be at least 1 characters.',
                            ],
                            'model_id' => [
                                'The model id field is required.',
                            ],
                        ],
                    ],
                ],
            ]);

        $newAssets = Asset::query()
            ->whereIn('serial', Arr::pluck($rows, 'serialNumber'))
            ->get();

        $this->assertCount(0, $newAssets);
    }

    #[Test]
    public function update_asset_from_import(): void
    {
        $asset = Asset::factory()->create()->refresh();
        $importFileBuilder = ImportFileBuilder::times(1)->replace(['tag' => $asset->asset_tag]);
        $row = $importFileBuilder->firstRow();
        $import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);

        $this->actingAsForApi(User::factory()->superuser()->create());
        $this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk();

        $updatedAsset = Asset::query()
            ->with(['location', 'supplier', 'company', 'defaultLoc', 'status', 'model.category', 'model.manufacturer'])
            ->find($asset->id);

        $assignee = User::query()->find($updatedAsset->assigned_to, ['id', 'first_name', 'last_name', 'email', 'username']);

        $updatedAttributes = [
            'category', 'manufacturer_id', 'name', 'tag', 'model_id',
            'model_number', 'purchase_date', 'purchase_cost', 'warranty_months', 'supplier_id',
            'location_id', 'company_id', 'serial', 'assigned_to', 'status_id', 'rtd_location_id',
            'last_checkout', 'updated_at', 'checkout_counter', 'assigned_type',
        ];

        $this->assertEquals($row['assigneeFullName'], "{$assignee->first_name} {$assignee->last_name}");
        $this->assertEquals($row['assigneeEmail'], $assignee->email);
        $this->assertEquals($row['assigneeUsername'], $assignee->username);

        $this->assertEquals($row['category'], $updatedAsset->model->category->name);
        $this->assertEquals($row['manufacturerName'], $updatedAsset->model->manufacturer->name);
        $this->assertEquals($row['itemName'], $updatedAsset->name);
        $this->assertEquals($row['tag'], $updatedAsset->asset_tag);
        $this->assertEquals($row['model'], $updatedAsset->model->name);
        $this->assertEquals($row['modelNumber'], $updatedAsset->model->model_number);
        $this->assertEquals($row['purchaseDate'], $updatedAsset->purchase_date->toDateString());
        $this->assertEquals($row['purchaseCost'], $updatedAsset->purchase_cost);
        $this->assertEquals($row['status'], $updatedAsset->status->name);
        $this->assertEquals($row['warrantyInMonths'], $updatedAsset->warranty_months);
        $this->assertEquals($row['supplierName'], $updatedAsset->supplier->name);
        $this->assertEquals($row['location'], $updatedAsset->defaultLoc->name);
        $this->assertEquals($row['companyName'], $updatedAsset->company->name);
        $this->assertEquals($row['location'], $updatedAsset->location->name);
        $this->assertEquals(1, $updatedAsset->checkout_counter);
        $this->assertEquals(User::class, $updatedAsset->assigned_type);

        // The import file has no requestable column, so the flag should be untouched.
        $this->assertEquals($asset->requestable, $updatedAsset->requestable);

        $this->assertEquals(
            Arr::except($asset->attributesToArray(), $updatedAttributes),
            Arr::except($updatedAsset->attributesToArray(), $updatedAttributes),
        );
    }

    #[Test]
    public function update_mode_preserves_boolean_flags_when_columns_are_not_in_file(): void
    {
        $asset = Asset::factory()->create(['requestable' => 1, 'byod' => 1])->refresh();

        // The default import file has no "Requestable" or "BYOD" columns, so an
        // update import should leave both flags alone.
        $importFileBuilder = ImportFileBuilder::times(1)->replace(['tag' => $asset->asset_tag]);
        $import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);

        $this->actingAsForApi(User::factory()->superuser()->create());
        $this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk();

        $asset->refresh();
        $this->assertEquals(1, $asset->requestable, 'Update import without a requestable column should not reset the requestable flag.');
        $this->assertEquals(1, $asset->byod, 'Update import without a byod column should not reset the byod flag.');
    }

    #[Test]
    public function update_mode_can_clear_boolean_flags_when_explicitly_false(): void
    {
        $asset = Asset::factory()->create(['requestable' => 1, 'byod' => 1])->refresh();

        $row = ImportFileBuilder::new()->definition();
        $row['tag'] = $asset->asset_tag;
        $row['Requestable'] = 'FALSE';
        $row['BYOD'] = 'FALSE';

        $importFileBuilder = new ImportFileBuilder([$row]);
        $import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);

        $this->actingAsForApi(User::factory()->superuser()->create());
        $this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk();

        $asset->refresh();
        $this->assertEquals(0, $asset->requestable, 'Update import with requestable=FALSE should clear the requestable flag.');
        $this->assertEquals(0, $asset->byod, 'Update import with byod=FALSE should clear the byod flag.');
    }

    #[Test]
    public function update_mode_clears_field_when_csv_column_is_present_but_empty(): void
    {
        $this->actingAsForApi(User::factory()->superuser()->create());

        // Seed an asset directly with fields we intend to clear. Using the
        // factory (rather than a prior import) gives us reliable non-null
        // starting values regardless of importer-side mapping quirks.
        $asset = Asset::factory()->create([
            'notes' => 'Some pre-existing notes',
            'purchase_date' => '2022-01-01',
        ])->refresh();

        // Sanity: the pre-existing values are there for the clear to act on.
        $this->assertNotNull($asset->purchase_date);
        $this->assertNotEmpty($asset->notes);

        $row = ImportFileBuilder::new()->definition();
        $row['tag'] = $asset->asset_tag;
        $row['notes'] = '';
        $row['purchaseDate'] = '';

        $importFileBuilder = new ImportFileBuilder([$row]);
        $import = Import::factory()->asset()->create([
            'file_path' => $importFileBuilder->saveToImportsDirectory(),
        ]);

        // Explicit column-mappings so the "Notes" CSV column resolves to the
        // 'asset_notes' importer field. Matches what the wizard's auto-map
        // does in production; the default_field_map fallback used by tests
        // that omit column-mappings does not know about 'asset_notes'.
        $this->importFileResponse([
            'import' => $import->id,
            'import-update' => true,
            'column-mappings' => [
                'Asset Tag' => 'asset_tag',
                'item Name' => 'item_name',
                'Notes' => 'asset_notes',
                'Purchase Date' => 'purchase_date',
                'Serial number' => 'serial',
            ],
        ])->assertOk();

        $asset->refresh();
        $this->assertNull($asset->notes);
        $this->assertNull($asset->purchase_date);
    }

    #[Test]
    public function update_mode_preserves_fields_when_csv_column_is_absent(): void
    {
        $this->actingAsForApi(User::factory()->superuser()->create());

        $asset = Asset::factory()->create([
            'notes' => 'Do not lose this',
            'purchase_date' => '2022-01-01',
        ])->refresh();

        $originalNotes = $asset->notes;
        $originalPurchaseDate = $asset->purchase_date?->toDateString();
        $originalSerial = $asset->serial;

        // Import a CSV that only has the identity field and one other column.
        // All other Asset fields are absent from the CSV, so their DB values
        // must be preserved on update.
        $partialFile = new ImportFileBuilder([[
            'tag' => $asset->asset_tag,
            'itemName' => 'Renamed via partial import',
        ]]);
        $partialImport = Import::factory()->asset()->create([
            'file_path' => $partialFile->saveToImportsDirectory(),
        ]);
        $this->importFileResponse([
            'import' => $partialImport->id,
            'import-update' => true,
        ])->assertOk();

        $asset->refresh();
        $this->assertEquals('Renamed via partial import', $asset->name);
        $this->assertEquals($originalNotes, $asset->notes);
        $this->assertEquals($originalPurchaseDate, $asset->purchase_date?->toDateString());
        $this->assertEquals($originalSerial, $asset->serial);
    }

    #[Test]
    public function update_mode_logs_asset_update_in_actionlog(): void
    {
        $this->actingAsForApi(User::factory()->superuser()->create());

        $initialFile = ImportFileBuilder::new();
        $initialRow = $initialFile->firstRow();

        $initialImport = Import::factory()->asset()->create([
            'file_path' => $initialFile->saveToImportsDirectory(),
        ]);

        $this->importFileResponse(['import' => $initialImport->id])->assertOk();

        $asset = Asset::query()->where('asset_tag', $initialRow['tag'])->sole();

        $updatedRow = array_merge($initialRow, [
            'itemName' => $initialRow['itemName'].' Updated',
        ]);

        $updateFile = new ImportFileBuilder([$updatedRow]);
        $updateImport = Import::factory()->asset()->create([
            'file_path' => $updateFile->saveToImportsDirectory(),
        ]);

        $this->importFileResponse([
            'import' => $updateImport->id,
            'import-update' => true,
        ])->assertOk();

        $asset->refresh();
        $this->assertEquals($updatedRow['itemName'], $asset->name);

        $updateLog = ActionLog::query()
            ->where('item_type', Asset::class)
            ->where('item_id', $asset->id)
            ->where('action_type', 'update')
            ->latest('id')
            ->first();

        $this->assertNotNull($updateLog, 'Expected an update action log entry after importer update mode.');
        $this->assertStringContainsString('name', (string) $updateLog->log_meta);

        $checkoutLogsCount = ActionLog::query()
            ->where('item_type', Asset::class)
            ->where('item_id', $asset->id)
            ->where('action_type', 'checkout')
            ->count();

        $this->assertSame(1, $checkoutLogsCount, 'Re-import update should not create a duplicate checkout log for the same assignment.');
    }

    #[Test]
    public function custom_column_mapping(): void
    {
        $faker = ImportFileBuilder::new()->definition();
        $row = [
            'assigneeFullName' => $faker['supplierName'],
            'assigneeEmail' => $faker['manufacturerName'],
            'assigneeUsername' => $faker['serialNumber'],
            'category' => $faker['location'],
            'companyName' => $faker['purchaseCost'],
            'itemName' => $faker['modelNumber'],
            'location' => $faker['assigneeUsername'],
            'manufacturerName' => $faker['status'],
            'model' => $faker['itemName'],
            'modelNumber' => $faker['category'],
            'notes' => $faker['notes'],
            'purchaseCost' => $faker['model'],
            'purchaseDate' => $faker['companyName'],
            'serialNumber' => $faker['tag'],
            'supplierName' => $faker['purchaseDate'],
            'status' => $faker['warrantyInMonths'],
            'tag' => $faker['assigneeEmail'],
            'warrantyInMonths' => $faker['assigneeFullName'],
        ];

        $importFileBuilder = new ImportFileBuilder([$row]);
        $import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);

        $this->actingAsForApi(User::factory()->superuser()->create());

        $this->importFileResponse([
            'import' => $import->id,
            'column-mappings' => [
                'Asset Tag' => 'email',
                'Category' => 'location',
                'Company' => 'purchase_cost',
                'Email' => 'manufacturer',
                'Full Name' => 'supplier',
                'Item Name' => 'model_number',
                'Location' => 'username',
                'Manufacturer' => 'status',
                'Model name' => 'item_name',
                'Model Number' => 'category',
                'Notes' => 'asset_notes',
                'Purchase Cost' => 'asset_model',
                'Purchase Date' => 'company',
                'Serial number' => 'asset_tag',
                'Status' => 'warranty_months',
                'Supplier' => 'purchase_date',
                'Username' => 'serial',
                'Warranty' => 'full_name',
            ],
        ])->assertOk();

        $asset = Asset::query()
            ->with(['location', 'supplier', 'company', 'assignedAssets', 'defaultLoc', 'status', 'model.category', 'model.manufacturer'])
            ->where('serial', $row['assigneeUsername'])
            ->sole();

        $assignee = User::query()->find($asset->assigned_to, ['id', 'first_name', 'last_name', 'email', 'username']);

        $this->assertEquals($row['warrantyInMonths'], "{$assignee->first_name} {$assignee->last_name}");
        $this->assertEquals($row['tag'], $assignee->email);
        $this->assertEquals($row['location'], $assignee->username);

        $this->assertEquals($row['modelNumber'], $asset->model->category->name);
        $this->assertEquals($row['assigneeEmail'], $asset->model->manufacturer->name);
        $this->assertEquals($row['model'], $asset->name);
        $this->assertEquals($row['serialNumber'], $asset->asset_tag);
        $this->assertEquals($row['purchaseCost'], $asset->model->name);
        $this->assertEquals($row['itemName'], $asset->model->model_number);
        $this->assertEquals($row['supplierName'], $asset->purchase_date->toDateString());
        $this->assertEquals($row['companyName'], $asset->purchase_cost);
        $this->assertEquals($row['manufacturerName'], $asset->status->name);
        $this->assertEquals($row['status'], $asset->warranty_months);
        $this->assertEquals($row['assigneeFullName'], $asset->supplier->name);
        $this->assertEquals($row['category'], $asset->defaultLoc->name);
        $this->assertEquals($row['purchaseDate'], $asset->company->name);
        $this->assertEquals($row['category'], $asset->location->name);
        $this->assertEquals($row['notes'], $asset->notes);
        $this->assertNull($asset->asset_eol_date);
        $this->assertEquals(0, $asset->eol_explicit);
        $this->assertNull($asset->order_number);
        $this->assertEquals('', $asset->image);
        $this->assertNull($asset->user_id);
        $this->assertEquals(1, $asset->physical);
        $this->assertEquals(0, $asset->archived);
        $this->assertNull($asset->deprecate);
        $this->assertEquals(0, $asset->requestable);
        $this->assertEquals(null, $asset->accepted);
        $this->assertEquals(now()->toDateString(), Carbon::parse($asset->last_checkout)->toDateString());
        $this->assertEquals(0, $asset->last_checkin);
        $this->assertEquals(0, $asset->expected_checkin);
        $this->assertEquals(User::class, $asset->assigned_type);
        $this->assertNull($asset->last_audit_date);
        $this->assertNull($asset->next_audit_date);
        $this->assertEquals(0, $asset->checkin_counter);
        $this->assertEquals(1, $asset->checkout_counter);
        $this->assertEquals(0, $asset->requests_counter);
        $this->assertEquals(0, $asset->byod);
    }

    #[Test]
    public function custom_fields(): void
    {
        $macAddress = $this->faker->macAddress;

        $row = ImportFileBuilder::new()->definition();
        $row['Mac Address'] = $macAddress;

        $importFileBuilder = new ImportFileBuilder([$row]);
        $customField = CustomField::query()->where('name', 'Mac Address')->firstOrNew();

        if (! $customField->exists) {
            $customField = CustomField::factory()->macAddress()->create(['db_column' => '_snipeit_mac_address_1']);
        }

        if ($customField->field_encrypted) {
            $customField->field_encrypted = 0;
            $customField->save();
        }

        $import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);

        $this->actingAsForApi(User::factory()->superuser()->create());
        $this->importFileResponse(['import' => $import->id])->assertOk();

        $newAsset = Asset::query()->where('serial', $importFileBuilder->firstRow()['serialNumber'])->sole();

        $this->assertEquals($macAddress, $newAsset->getAttribute($customField->db_column));
    }

    #[Test]
    public function will_encrypt_custom_fields(): void
    {
        $macAddress = $this->faker->macAddress;
        $row = ImportFileBuilder::new()->definition();

        $row['Mac Address'] = $macAddress;

        $importFileBuilder = new ImportFileBuilder([$row]);
        $customField = CustomField::query()->where('name', 'Mac Address')->firstOrNew();

        if (! $customField->exists) {
            $customField = CustomField::factory()->macAddress()->create();
        }

        if (! $customField->field_encrypted) {
            $customField->field_encrypted = 1;
            $customField->save();
        }

        $import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);

        $this->actingAsForApi(User::factory()->superuser()->create());
        $this->importFileResponse(['import' => $import->id])->assertOk();

        $asset = Asset::query()->where('serial', $importFileBuilder->firstRow()['serialNumber'])->sole();
        $encryptedMacAddress = $asset->getAttribute($customField->db_column);

        $this->assertNotEquals($encryptedMacAddress, $macAddress);
    }

    #[Test]
    public function import_asset_checkout_is_blocked_when_fmcs_companies_differ(): void
    {
        [$companyA, $companyB] = Company::factory()->count(2)->create();
        $user = User::factory()->forCompany($companyB)->create();
        $this->settings->enableMultipleFullCompanySupport();

        $importFileBuilder = ImportFileBuilder::new([
            'companyName' => $companyA->name,
            'assigneeUsername' => $user->username,
        ]);

        $import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);

        $this->actingAsForApi(User::factory()->superuser()->create());
        $this->importFileResponse(['import' => $import->id])->assertOk();

        $newAsset = Asset::where('serial', $importFileBuilder->firstRow()['serialNumber'])->sole();
        $this->assertNull($newAsset->assigned_to, 'Asset should not be checked out when item and user companies differ under FMCS');
    }

    #[Test]
    public function import_asset_checkout_is_allowed_when_fmcs_companies_match(): void
    {
        $company = Company::factory()->create();
        $user = User::factory()->forCompany($company)->create();
        $this->settings->enableMultipleFullCompanySupport();

        $importFileBuilder = ImportFileBuilder::new([
            'companyName' => $company->name,
            'assigneeUsername' => $user->username,
        ]);

        $import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);

        $this->actingAsForApi(User::factory()->superuser()->create());
        $this->importFileResponse(['import' => $import->id])->assertOk();

        $newAsset = Asset::where('serial', $importFileBuilder->firstRow()['serialNumber'])->sole();
        $this->assertEquals($user->id, $newAsset->assigned_to, 'Asset should be checked out when companies match under FMCS');
    }

    #[Test]
    public function import_asset_checkout_is_blocked_when_floater_disabled_and_user_has_no_company(): void
    {
        $company = Company::factory()->create();
        $user = User::factory()->withoutCompany()->create();
        $this->settings->enableMultipleFullCompanySupport()->disableFloaterMode();

        $importFileBuilder = ImportFileBuilder::new([
            'companyName' => $company->name,
            'assigneeUsername' => $user->username,
        ]);

        $import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);

        $this->actingAsForApi(User::factory()->superuser()->create());
        $this->importFileResponse(['import' => $import->id])->assertOk();

        $newAsset = Asset::where('serial', $importFileBuilder->firstRow()['serialNumber'])->sole();
        $this->assertNull($newAsset->assigned_to, 'Asset should not be checked out to a no-company user when floater mode is off');
    }

    #[Test]
    public function import_asset_checkout_is_allowed_when_floater_enabled_and_user_has_no_company(): void
    {
        $company = Company::factory()->create();
        $user = User::factory()->withoutCompany()->create();
        $this->settings->enableFloaterMode();

        $importFileBuilder = ImportFileBuilder::new([
            'companyName' => $company->name,
            'assigneeUsername' => $user->username,
        ]);

        $import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);

        $this->actingAsForApi(User::factory()->superuser()->create());
        $this->importFileResponse(['import' => $import->id])->assertOk();

        $newAsset = Asset::where('serial', $importFileBuilder->firstRow()['serialNumber'])->sole();
        $this->assertEquals($user->id, $newAsset->assigned_to, 'Asset should be checked out to a no-company user when floater mode is on');
    }

    #[Test]
    public function asset_import_does_not_mint_a_floater_user_side_effect_for_non_superuser(): void
    {
        // #19200: when an asset CSV references a brand-new username, the base
        // Importer::createOrFetchUser side-effects a new user with no company
        // pivot. Under floater mode that previously promoted the new user to
        // system-wide visibility — exploitable as an asset import path. The
        // guard refuses the user creation for actors who can't grant floater
        // status; the asset still imports, it just doesn't get checked out.
        $this->settings->enableFloaterMode();

        $company = Company::factory()->create();
        $importer = $company->users()->save(
            User::factory()->createAssets()->editAssets()->canImport()->create(),
        );

        $importFileBuilder = ImportFileBuilder::new([
            'companyName' => $company->name,
            'assigneeUsername' => 'phantom-floater-user',
            'assigneeFullName' => 'Phantom Floater',
            'assigneeEmail' => 'phantom@example.org',
        ]);

        $this->actingAsForApi($importer);
        $import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
        $this->importFileResponse(['import' => $import->id])->assertOk();

        $this->assertDatabaseMissing('users', ['username' => 'phantom-floater-user']);

        $newAsset = Asset::where('serial', $importFileBuilder->firstRow()['serialNumber'])->sole();
        $this->assertNull($newAsset->assigned_to, 'Asset imports but is not checked out to the rejected user');
    }
}
