<?php

declare(strict_types=1);

namespace Tests\Feature\Imports;

use App\Enums\AuditEvent;
use App\Enums\ImportStatus;
use App\Models\Asset;
use App\Models\AssetCategory;
use App\Models\AssetDocument;
use App\Models\AuditLog;
use App\Models\ImportBatch;
use App\Models\User;
use App\Services\ImportService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
use ZipArchive;

class AssetDocumentImportTest extends TestCase
{
    use RefreshDatabase;

    // ---------------------------------------------------------------- zip

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

        $file = $this->csvUpload([
            $this->row('First', $category->name, 'AUM-DOC-0001'),
            $this->row('Second', $category->name, 'AUM-DOC-0002'),
        ]);

        $zip = $this->zipUpload([
            'AUM-DOC-0001.pdf' => $this->pdfBytes(),
            'AUM-DOC-0002.pdf' => $this->pdfBytes(),
        ]);

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

        $batch = ImportBatch::query()->firstOrFail();
        $this->assertNotNull($batch->documents_manifest);

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

        $this->assertSame(ImportStatus::Completed, $batch->status);
        $this->assertSame(['matched' => 2, 'missing' => 0, 'rejected' => 0, 'unmatched' => 0], $batch->documents_summary['counts']);

        $first = Asset::query()->where('asset_tag', 'AUM-DOC-0001')->firstOrFail();
        $second = Asset::query()->where('asset_tag', 'AUM-DOC-0002')->firstOrFail();

        $this->assertSame(1, AssetDocument::query()->where('asset_id', $first->id)->count());
        $this->assertSame(1, AssetDocument::query()->where('asset_id', $second->id)->count());
        $this->assertSame('invoice', AssetDocument::query()->where('asset_id', $first->id)->firstOrFail()->document_type->value);
    }

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

        $file = $this->csvUpload([
            $this->row('Mixed Case', $category->name, 'AUM-DOC-0003'),
        ]);

        $zip = $this->zipUpload([
            'aum-doc-0003.PNG' => $this->pngBytes(),
        ]);

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

        $asset = Asset::query()->where('asset_tag', 'AUM-DOC-0003')->firstOrFail();
        $document = AssetDocument::query()->where('asset_id', $asset->id)->firstOrFail();
        $this->assertSame('image/png', $document->mime_type);
    }

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

        $file = $this->csvUpload([
            $this->row('Has Document', $category->name, 'AUM-DOC-0010'),
            $this->row('No Document', $category->name, 'AUM-DOC-0011'),
        ]);

        $zip = $this->zipUpload(['AUM-DOC-0010.pdf' => $this->pdfBytes()]);

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

        $this->assertSame(2, $batch->success_rows);
        $this->assertSame(['matched' => 1, 'missing' => 1, 'rejected' => 0, 'unmatched' => 0], $batch->documents_summary['counts']);
        $this->assertContains('AUM-DOC-0011', $batch->documents_summary['missing_tags']);

        $this->assertDatabaseHas('assets', ['asset_tag' => 'AUM-DOC-0011']);
    }

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

        $file = $this->csvUpload([
            $this->row('Only Row', $category->name, 'AUM-DOC-0020'),
        ]);

        $zip = $this->zipUpload([
            'AUM-DOC-0020.pdf' => $this->pdfBytes(),
            'AUM-DOC-9999.pdf' => $this->pdfBytes(),
        ]);

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

        $preview = $this->actingAs($user)->get(route('imports.preview', $batch));
        $preview->assertOk();
        $preview->assertSee('AUM-DOC-9999');

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

        $this->assertSame(['matched' => 1, 'missing' => 0, 'rejected' => 0, 'unmatched' => 1], $batch->documents_summary['counts']);
        $this->assertSame('AUM-DOC-9999', $batch->documents_summary['unmatched'][0]['normalized_tag']);
    }

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

        $file = $this->csvUpload([$this->row('Dup', $category->name, 'AUM-DOC-0030')]);

        $zip = $this->zipUpload([
            'AUM-DOC-0030.pdf' => $this->pdfBytes(),
            'sub/AUM-DOC-0030.pdf' => $this->pdfBytes(),
        ]);

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

        $manifest = $batch->documents_manifest;
        $rejected = array_values(array_filter($manifest, fn (array $entry) => $entry['rejected_reason'] !== null));

        $this->assertCount(1, $rejected);
        $this->assertSame('Duplicate document filename.', $rejected[0]['rejected_reason']);
    }

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

        $file = $this->csvUpload([$this->row('Bad Type', $category->name, 'AUM-DOC-0040')]);

        $zip = $this->zipUpload(['AUM-DOC-0040.pdf' => 'this is not really a pdf, just plain text']);

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

        $manifest = $batch->documents_manifest;
        $this->assertSame('Unsupported file type.', $manifest[0]['rejected_reason']);
    }

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

        $file = $this->csvUpload([$this->row('Slip', $category->name, 'AUM-DOC-0050')]);

        $zip = $this->zipUpload(['../../etc/AUM-DOC-0050.pdf' => $this->pdfBytes()]);

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

        $manifest = $batch->documents_manifest;
        $this->assertSame('Unsafe file name in ZIP.', $manifest[0]['rejected_reason']);
    }

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

        $file = $this->csvUpload([$this->row('Oversized', $category->name, 'AUM-DOC-0060')]);

        $response = $this->actingAs($user)->post(route('imports.store'), [
            'module' => 'assets',
            'file' => $file,
            'documents_zip' => UploadedFile::fake()->create('huge.zip', 15361, 'application/zip'),
        ]);

        $response->assertSessionHasErrors('documents_zip');
    }

    // ------------------------------------------------------------- files

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

        $file = $this->csvUpload([$this->row('Loose', $category->name, 'AUM-DOC-0070')]);

        $this->actingAs($user)->post(route('imports.store'), [
            'module' => 'assets',
            'file' => $file,
            'documents' => [UploadedFile::fake()->create('AUM-DOC-0070.pdf', 100, 'application/pdf')],
        ]);

        $batch = ImportBatch::query()->firstOrFail();
        $this->actingAs($user)->post(route('imports.confirm', $batch));

        $asset = Asset::query()->where('asset_tag', 'AUM-DOC-0070')->firstOrFail();
        $this->assertSame(1, AssetDocument::query()->where('asset_id', $asset->id)->count());
    }

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

        $file = $this->csvUpload([$this->row('Both', $category->name, 'AUM-DOC-0080')]);

        $response = $this->actingAs($user)->post(route('imports.store'), [
            'module' => 'assets',
            'file' => $file,
            'documents' => [UploadedFile::fake()->create('AUM-DOC-0080.pdf', 100, 'application/pdf')],
            'documents_zip' => $this->zipUpload(['AUM-DOC-0080.pdf' => $this->pdfBytes()]),
        ]);

        $response->assertSessionHasErrors();
    }

    // ------------------------------------------------------------ cleanup

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

        // No category exists, so the only row fails validation and nothing
        // is ever created — commit() itself never writes any asset.
        $file = $this->csvUpload([$this->row('No Category', 'Does Not Exist', '')]);
        $zip = $this->zipUpload(['ANYTHING.pdf' => $this->pdfBytes()]);

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

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

        $this->assertSame(ImportStatus::Failed, $batch->status);
        $this->assertSame(0, AssetDocument::query()->count());
        $this->assertSame(0, Asset::query()->count());
        $this->assertEmpty(Storage::disk('local')->allFiles('imports/'.$batch->id.'/documents'));
    }

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

        $file = $this->csvUpload([$this->row('Cleaned Up', $category->name, 'AUM-DOC-0090')]);
        $zip = $this->zipUpload(['AUM-DOC-0090.pdf' => $this->pdfBytes()]);

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

        $this->assertEmpty(Storage::disk('local')->allFiles('imports/'.$batch->id.'/documents'));
    }

    // --------------------------------------------------------------- audit

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

        $file = $this->csvUpload([
            $this->row('One', $category->name, 'AUM-DOC-0100'),
            $this->row('Two', $category->name, 'AUM-DOC-0101'),
        ]);

        $zip = $this->zipUpload([
            'AUM-DOC-0100.pdf' => $this->pdfBytes(),
            'AUM-DOC-0101.pdf' => $this->pdfBytes(),
        ]);

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

        $this->assertSame(
            0,
            AuditLog::query()->where('event', AuditEvent::Created->value)
                ->where('auditable_type', (new AssetDocument)->getMorphClass())
                ->count(),
        );

        $imported = AuditLog::query()->where('event', AuditEvent::Imported->value)->orderByDesc('id')->get();
        $this->assertGreaterThanOrEqual(2, $imported->count());
        $this->assertTrue($imported->contains(fn (AuditLog $log): bool => str_contains($log->description, '2 invoice')));
    }

    // ---------------------------------------------------------- regression

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

        $file = $this->csvUpload([$this->row('Plain', $category->name, 'AUM-DOC-0200')]);

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

        $this->assertNull($batch->documents_manifest);

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

        $this->assertSame(ImportStatus::Completed, $batch->status);
        $this->assertSame(1, $batch->success_rows);
        $this->assertNull($batch->documents_summary);
        $this->assertDatabaseHas('assets', ['asset_tag' => 'AUM-DOC-0200']);
        $this->assertSame(0, AssetDocument::query()->count());
    }

    // --------------------------------------------------------------- helpers

    /**
     * @param  array<int, array<string, mixed>>  $rows
     */
    private function csvUpload(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('assets.csv', implode("\n", $lines));
    }

    /**
     * @return array<string, mixed>
     */
    private function row(string $name, string $category, string $assetTag): array
    {
        return [
            $assetTag,
            $name,
            $category,
            '',
            '',
            'Dell',
            'Latitude 5440',
            'Acme Traders',
            '',
            '',
            'In Stock',
            'Good',
            '15-01-2025',
            '85000',
            'INR',
            '',
            '',
            '',
            '',
        ];
    }

    /**
     * @param  array<string, string>  $entries  zip entry name => raw contents
     */
    private function zipUpload(array $entries): UploadedFile
    {
        $path = tempnam(sys_get_temp_dir(), 'ams-test-zip').'.zip';

        $zip = new ZipArchive;
        $zip->open($path, ZipArchive::CREATE | ZipArchive::OVERWRITE);

        foreach ($entries as $name => $contents) {
            $zip->addFromString($name, $contents);
        }

        $zip->close();

        return new UploadedFile($path, 'documents.zip', 'application/zip', null, true);
    }

    /**
     * Minimal bytes finfo reliably sniffs as application/pdf.
     */
    private function pdfBytes(): string
    {
        return "%PDF-1.4\n1 0 obj<</Type/Catalog>>endobj\ntrailer<</Root 1 0 R>>\n%%EOF";
    }

    /**
     * A real 1x1 transparent PNG, so finfo sniffs it as image/png.
     */
    private function pngBytes(): string
    {
        return base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=');
    }
}
