<?php

namespace Tests\Feature\Importing\Api;

use App\Models\Actionlog as ActivityLog;
use App\Models\Company;
use App\Models\Import;
use App\Models\License;
use App\Models\LicenseSeat;
use App\Models\User;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Str;
use Illuminate\Testing\TestResponse;
use PHPUnit\Framework\Attributes\Test;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\Support\Importing\CleansUpImportFiles;
use Tests\Support\Importing\LicensesImportFileBuilder as ImportFileBuilder;

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

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

        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_licenses(): void
    {
        $this->actingAsForApi(User::factory()->canImport()->create());

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

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

    #[Test]
    public function import_licenses(): void
    {
        $importFileBuilder = ImportFileBuilder::new();
        $row = $importFileBuilder->firstRow();
        $import = Import::factory()->license()->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('licenses.index')],
            ]);

        $newLicense = License::query()
            ->withCasts(['reassignable' => 'bool'])
            ->with(['category', 'company', 'manufacturer', 'supplier'])
            ->where('serial', $row['serialNumber'])
            ->sole();

        $activityLogs = ActivityLog::query()
            ->where('item_type', License::class)
            ->where('item_id', $newLicense->id)
            ->get();

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

        $this->assertEquals($row['licenseName'], $newLicense->name);
        $this->assertEquals($row['serialNumber'], $newLicense->serial);
        $this->assertEquals($row['purchaseDate'], $newLicense->purchase_date->toDateString());
        $this->assertEquals($row['purchaseCost'], $newLicense->purchase_cost);
        $this->assertEquals($row['orderNumber'], $newLicense->order_number);
        $this->assertEquals($row['seats'], $newLicense->seats);
        $this->assertEquals($row['notes'], $newLicense->notes);
        $this->assertEquals($row['licensedToName'], $newLicense->license_name);
        $this->assertEquals($row['licensedToEmail'], $newLicense->license_email);
        $this->assertEquals($row['supplierName'], $newLicense->supplier->name);
        $this->assertEquals($row['companyName'], $newLicense->company->name);
        $this->assertEquals($row['category'], $newLicense->category->name);
        $this->assertEquals($row['expirationDate'], $newLicense->expiration_date->toDateString());
        $this->assertEquals($row['isMaintained'] === 'TRUE', $newLicense->maintained);
        $this->assertEquals($row['isReassignAble'] === 'TRUE', $newLicense->reassignable);
        $this->assertEquals('', $newLicense->purchase_order);
        $this->assertNull($newLicense->depreciation_id);
        $this->assertNull($newLicense->termination_date);
        $this->assertNull($newLicense->deprecate);
        $this->assertNull($newLicense->min_amt);
    }

    #[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()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);

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

    #[Test]
    public function will_not_create_new_license_when_name_and_serial_number_already_exist(): void
    {
        $license = License::factory()->create();

        $importFileBuilder = ImportFileBuilder::times(4)->replace([
            'itemName' => $license->name,
            'serialNumber' => $license->serial,
        ]);

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

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

        $probablyNewLicenses = License::query()
            ->where('name', $license->name)
            ->where('serial', $license->serial)
            ->get();

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

    #[Test]
    public function format_attributes(): void
    {
        $importFileBuilder = ImportFileBuilder::new([
            'expirationDate' => '2022/10/10',
        ]);

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

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

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

        $this->assertEquals('2022-10-10', $newLicense->expiration_date->toDateString());
    }

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

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

        $newLicenses = License::query()
            ->whereIn('serial', $importFileBuilder->pluck('serialNumber'))
            ->get(['company_id']);

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

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

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

        $newLicenses = License::query()
            ->whereIn('serial', $importFileBuilder->pluck('serialNumber'))
            ->get(['manufacturer_id']);

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

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

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

        $newLicenses = License::query()
            ->whereIn('serial', $importFileBuilder->pluck('serialNumber'))
            ->get(['category_id']);

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

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

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

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

        $this->importFileResponse(['import' => $import->id])
            ->assertInternalServerError()
            ->assertExactJson([
                'status' => 'import-errors',
                'payload' => ['tally' => ['created' => 0, 'updated' => 0, 'skipped' => 0, 'errored' => 1]],
                'messages' => [
                    $row['licenseName'] => [
                        "License \"{$row['licenseName']}\"" => [
                            'seats' => ['The seats field is required.'],
                        ],
                    ],
                ],
            ]);

        $newLicenses = License::query()
            ->where('serial', $row['serialNumber'])
            ->get();

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

    #[Test]
    public function update_license_from_import(): void
    {
        $license = License::factory()->create();
        $importFileBuilder = ImportFileBuilder::new([
            'licenseName' => $license->name,
            'serialNumber' => $license->serial,
        ]);

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

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

        $updatedLicense = License::query()
            ->with(['manufacturer', 'category', 'supplier'])
            ->where('serial', $row['serialNumber'])
            ->sole();

        $this->assertEquals($row['licenseName'], $updatedLicense->name);
        $this->assertEquals($row['serialNumber'], $updatedLicense->serial);
        $this->assertEquals($row['purchaseDate'], $updatedLicense->purchase_date->toDateString());
        $this->assertEquals($row['purchaseCost'], $updatedLicense->purchase_cost);
        $this->assertEquals($row['orderNumber'], $updatedLicense->order_number);
        $this->assertEquals($row['seats'], $updatedLicense->seats);
        $this->assertEquals($row['notes'], $updatedLicense->notes);
        $this->assertEquals($row['licensedToName'], $updatedLicense->license_name);
        $this->assertEquals($row['licensedToEmail'], $updatedLicense->license_email);
        $this->assertEquals($row['supplierName'], $updatedLicense->supplier->name);
        $this->assertEquals($row['companyName'], $updatedLicense->company->name);
        $this->assertEquals($row['category'], $updatedLicense->category->name);
        $this->assertEquals($row['expirationDate'], $updatedLicense->expiration_date->toDateString());
        $this->assertEquals($row['isMaintained'] === 'TRUE', $updatedLicense->maintained);
        $this->assertEquals($row['isReassignAble'] === 'TRUE', $updatedLicense->reassignable);
        $this->assertEquals($license->purchase_order, $updatedLicense->purchase_order);
        $this->assertEquals($license->depreciation_id, $updatedLicense->depreciation_id);
        $this->assertEquals($license->termination_date, $updatedLicense->termination_date);
        $this->assertEquals($license->deprecate, $updatedLicense->deprecate);
        $this->assertEquals($license->min_amt, $updatedLicense->min_amt);
    }

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

        $initialFile = ImportFileBuilder::new();
        $initialRow = $initialFile->firstRow();
        $initialImport = Import::factory()->license()->create([
            'file_path' => $initialFile->saveToImportsDirectory(),
        ]);
        $this->importFileResponse(['import' => $initialImport->id])->assertOk();

        $license = License::query()
            ->where('name', $initialRow['licenseName'])
            ->where('serial', $initialRow['serialNumber'])
            ->sole();

        // Sanity: initial import should have populated the fields we are
        // about to clear. Guards against a false negative if the fixture
        // shape changes and the fields default to null.
        $this->assertNotNull($license->expiration_date);
        $this->assertNotEmpty($license->license_email);
        $this->assertNotEmpty($license->notes);

        // Re-import with these columns present but empty. Empty CSV cells
        // should clear the corresponding DB fields, not preserve them.
        $clearedRow = array_merge($initialRow, [
            'expirationDate' => '',
            'licensedToEmail' => '',
            'notes' => '',
        ]);
        $clearFile = new ImportFileBuilder([$clearedRow]);
        $clearImport = Import::factory()->license()->create([
            'file_path' => $clearFile->saveToImportsDirectory(),
        ]);
        $this->importFileResponse([
            'import' => $clearImport->id,
            'import-update' => true,
        ])->assertOk();

        $license->refresh();
        $this->assertNull($license->expiration_date);
        $this->assertNull($license->license_email);
        $this->assertNull($license->notes);
    }

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

        $initialFile = ImportFileBuilder::new();
        $initialRow = $initialFile->firstRow();
        $initialImport = Import::factory()->license()->create([
            'file_path' => $initialFile->saveToImportsDirectory(),
        ]);
        $this->importFileResponse(['import' => $initialImport->id])->assertOk();

        $license = License::query()
            ->where('name', $initialRow['licenseName'])
            ->where('serial', $initialRow['serialNumber'])
            ->sole();

        $originalEmail = $license->license_email;
        $originalNotes = $license->notes;
        $originalExpirationDate = $license->expiration_date?->toDateString();

        // Re-import with a CSV that only has the identity fields plus one
        // updated field. All other columns are absent from the CSV, so
        // their DB values must be preserved.
        // A short static order number keeps the assertion within License's
        // order_number varchar(50) limit regardless of what the initial row
        // faker produced. Prefixing the faker value overflowed on some seeds.
        $newOrderNumber = 'UPDATED-ORDER-123';

        $partialRow = [
            'licenseName' => $initialRow['licenseName'],
            'serialNumber' => $initialRow['serialNumber'],
            'orderNumber' => $newOrderNumber,
        ];
        $partialFile = new ImportFileBuilder([$partialRow]);
        $partialImport = Import::factory()->license()->create([
            'file_path' => $partialFile->saveToImportsDirectory(),
        ]);
        $this->importFileResponse([
            'import' => $partialImport->id,
            'import-update' => true,
        ])->assertOk();

        $license->refresh();
        $this->assertEquals($newOrderNumber, $license->order_number);
        $this->assertEquals($originalEmail, $license->license_email);
        $this->assertEquals($originalNotes, $license->notes);
        $this->assertEquals($originalExpirationDate, $license->expiration_date?->toDateString());
    }

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

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

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

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

        $license = License::query()
            ->where('name', $initialRow['licenseName'])
            ->where('serial', $initialRow['serialNumber'])
            ->sole();

        $updatedRow = array_merge($initialRow, [
            'orderNumber' => (string) $initialRow['orderNumber'].'-UPD',
        ]);

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

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

        $license->refresh();
        $this->assertEquals($updatedRow['orderNumber'], $license->order_number);

        $updateLog = ActivityLog::query()
            ->where('item_type', License::class)
            ->where('item_id', $license->id)
            ->where('action_type', 'update')
            ->latest('id')
            ->first();

        $this->assertNotNull($updateLog, 'Expected an update action log entry after license importer update mode.');
    }

    #[Test]
    public function custom_column_mapping(): void
    {
        $faker = ImportFileBuilder::times()->definition();
        $row = [
            'category' => $faker['supplierName'],
            'companyName' => $faker['serialNumber'],
            'expirationDate' => $faker['seats'],
            'isMaintained' => $faker['purchaseDate'],
            'isReassignAble' => $faker['purchaseCost'],
            'licensedToName' => $faker['orderNumber'],
            'licensedToEmail' => $faker['notes'],
            'licenseName' => $faker['licenseName'],
            'manufacturerName' => $faker['category'],
            'notes' => $faker['companyName'],
            'orderNumber' => $faker['expirationDate'],
            'purchaseCost' => $faker['isMaintained'],
            'purchaseDate' => $faker['isReassignAble'],
            'seats' => $faker['licensedToName'],
            'serialNumber' => $faker['licensedToEmail'],
            'supplierName' => $faker['manufacturerName'],
        ];

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

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

        $this->importFileResponse([
            'import' => $import->id,
            'column-mappings' => [
                'Category' => 'supplier',
                'Company' => 'serial',
                'expiration date' => 'seats',
                'maintained' => 'purchase_date',
                'reassignable' => 'purchase_cost',
                'Licensed To Name' => 'order_number',
                'Licensed To Email' => 'notes',
                'licenseName' => 'name',
                'manufacturer' => 'category',
                'Notes' => 'company',
                'Serial number' => 'license_email',
                'Order Number' => 'expiration_date',
                'purchase Cost' => 'maintained',
                'purchase Date' => 'reassignable',
                'seats' => 'license_name',
                'supplier' => 'manufacturer',
            ],
        ])->assertOk();

        $newLicense = License::query()
            ->with(['category', 'company', 'manufacturer', 'supplier'])
            ->where('serial', $row['companyName'])
            ->sole();

        $this->assertEquals($row['licenseName'], $newLicense->name);
        $this->assertEquals($row['companyName'], $newLicense->serial);
        $this->assertEquals($row['isMaintained'], $newLicense->purchase_date->toDateString());
        $this->assertEquals($row['isReassignAble'], $newLicense->purchase_cost);
        $this->assertEquals($row['licensedToName'], $newLicense->order_number);
        $this->assertEquals($row['expirationDate'], $newLicense->seats);
        $this->assertEquals($row['licensedToEmail'], $newLicense->notes);
        $this->assertEquals($row['seats'], $newLicense->license_name);
        $this->assertEquals($row['serialNumber'], $newLicense->license_email);
        $this->assertEquals($row['category'], $newLicense->supplier->name);
        $this->assertEquals($row['notes'], $newLicense->company->name);
        $this->assertEquals($row['manufacturerName'], $newLicense->category->name);
        $this->assertEquals($row['orderNumber'], $newLicense->expiration_date->toDateString());
        $this->assertEquals($row['purchaseCost'] === 'TRUE', $newLicense->maintained);
        $this->assertEquals($row['purchaseDate'] === 'TRUE', $newLicense->reassignable);
        $this->assertEquals('', $newLicense->purchase_order);
        $this->assertNull($newLicense->depreciation_id);
        $this->assertNull($newLicense->termination_date);
        $this->assertNull($newLicense->deprecate);
        $this->assertNull($newLicense->min_amt);
    }

    #[Test]
    public function import_license_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,
            'checkoutUsername' => $user->username,
            'seats' => 5,
        ]);

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

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

        $license = License::where('serial', $importFileBuilder->firstRow()['serialNumber'])->sole();
        $checkedOutSeat = LicenseSeat::where('license_id', $license->id)->whereNotNull('assigned_to')->first();
        $this->assertNull($checkedOutSeat, 'License seat should not be checked out when item and user companies differ under FMCS');
    }

    #[Test]
    public function import_license_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,
            'checkoutUsername' => $user->username,
            'seats' => 5,
        ]);

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

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

        $license = License::where('serial', $importFileBuilder->firstRow()['serialNumber'])->sole();
        $checkedOutSeat = LicenseSeat::where('license_id', $license->id)->where('assigned_to', $user->id)->first();
        $this->assertNotNull($checkedOutSeat, 'License seat should be checked out when companies match under FMCS');
    }

    #[Test]
    public function import_license_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,
            'checkoutUsername' => $user->username,
            'seats' => 5,
        ]);

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

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

        $license = License::where('serial', $importFileBuilder->firstRow()['serialNumber'])->sole();
        $checkedOutSeat = LicenseSeat::where('license_id', $license->id)->whereNotNull('assigned_to')->first();
        $this->assertNull($checkedOutSeat, 'License seat should not be checked out to a no-company user when floater mode is off');
    }

    #[Test]
    public function import_license_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,
            'checkoutUsername' => $user->username,
            'seats' => 5,
        ]);

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

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

        $license = License::where('serial', $importFileBuilder->firstRow()['serialNumber'])->sole();
        $checkedOutSeat = LicenseSeat::where('license_id', $license->id)->where('assigned_to', $user->id)->first();
        $this->assertNotNull($checkedOutSeat, 'License seat should be checked out to a no-company user when floater mode is on');
    }
}
