<?php

namespace Tests\Feature;

use App\Domain\Financial\Models\Expense;
use App\Domain\Financial\Services\ExpenseService;
use App\Domain\Shared\Exceptions\DomainException;
use App\Models\User;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;

/**
 * An expense recorded with a receipt attached used to arrive in the database
 * with no receipt at all: ExpenseForm uploaded the file and passed the path to
 * ExpenseService::recordExpense(), which built its Expense::create() array by
 * hand and simply never copied the two attachment keys across. The file landed
 * on disk and the row forgot about it.
 *
 * Worse, it landed on the `public` disk, which needs a storage symlink the
 * containers never create — so even a persisted path would have 404'd.
 */
class ExpenseReceiptTest extends TestCase
{
    private User $actor;

    protected function setUp(): void
    {
        parent::setUp();

        Storage::fake('local');
        $this->app->instance('current_academy', (object) ['id' => 1]);
        $this->createMinimalSchema();

        $this->actor = new User();
        $this->actor->id = 7;
        $this->actor->academy_id = 1;
        $this->actor->exists = true;
    }

    private function service(): ExpenseService
    {
        return new ExpenseService();
    }

    private function expenseData(array $overrides = []): array
    {
        return array_merge([
            'branch_id' => 1,
            'category' => 'supplies',
            'amount' => 25000,
            'description' => 'أقماع تدريب',
            'payment_method' => 'cash',
            'expense_date' => '2026-08-30',
        ], $overrides);
    }

    // ---- the reported bug -------------------------------------------------

    public function test_a_receipt_uploaded_with_a_new_expense_survives_the_write(): void
    {
        $expense = $this->service()->recordExpense($this->expenseData([
            'attachment_path' => 'expenses/receipts/abc.jpg',
            'attachment_name' => 'فاتورة.jpg',
            'attachment_disk' => 'local',
            'attachment_mime' => 'image/jpeg',
            'attachment_size' => 4096,
        ]), $this->actor);

        $this->assertSame('expenses/receipts/abc.jpg', $expense->attachment_path);
        $this->assertSame('فاتورة.jpg', $expense->attachment_name);
        $this->assertSame('local', $expense->attachment_disk);
        $this->assertSame('image/jpeg', $expense->attachment_mime);
        $this->assertSame(4096, $expense->attachment_size);
        $this->assertSame(7, $expense->attachment_uploaded_by);
        $this->assertNotNull($expense->attachment_uploaded_at);
    }

    public function test_an_expense_without_a_receipt_does_not_claim_one_was_uploaded(): void
    {
        $expense = $this->service()->recordExpense($this->expenseData(), $this->actor);

        $this->assertFalse($expense->hasAttachment());
        $this->assertNull($expense->attachment_uploaded_by);
        $this->assertNull($expense->attachment_uploaded_at);
    }

    // ---- attaching to an expense already in the ledger --------------------

    public function test_a_receipt_can_be_attached_to_an_expense_recorded_without_one(): void
    {
        $expense = $this->service()->recordExpense($this->expenseData(), $this->actor);
        Storage::disk('local')->put('expenses/receipts/scan.pdf', 'pdf-bytes');

        $expense = $this->service()->attachReceipt($expense, [
            'path' => 'expenses/receipts/scan.pdf',
            'name' => 'إيصال المورد.pdf',
            'disk' => 'local',
            'mime' => 'application/pdf',
            'size' => 9,
        ], $this->actor);

        $this->assertTrue($expense->hasAttachment());
        $this->assertTrue($expense->attachmentIsPdf());
        $this->assertSame(7, $expense->attachment_uploaded_by);
    }

    public function test_replacing_a_receipt_deletes_the_file_it_displaced(): void
    {
        Storage::disk('local')->put('expenses/receipts/old.jpg', 'old');
        Storage::disk('local')->put('expenses/receipts/new.jpg', 'new');

        $expense = $this->service()->recordExpense($this->expenseData([
            'attachment_path' => 'expenses/receipts/old.jpg',
            'attachment_name' => 'old.jpg',
            'attachment_disk' => 'local',
        ]), $this->actor);

        $expense = $this->service()->attachReceipt($expense, [
            'path' => 'expenses/receipts/new.jpg',
            'name' => 'new.jpg',
            'disk' => 'local',
            'mime' => 'image/jpeg',
            'size' => 3,
        ], $this->actor);

        Storage::disk('local')->assertMissing('expenses/receipts/old.jpg');
        Storage::disk('local')->assertExists('expenses/receipts/new.jpg');
        $this->assertSame('expenses/receipts/new.jpg', $expense->attachment_path);
    }

    public function test_removing_a_receipt_clears_the_row_and_the_file(): void
    {
        Storage::disk('local')->put('expenses/receipts/scan.jpg', 'bytes');

        $expense = $this->service()->recordExpense($this->expenseData([
            'attachment_path' => 'expenses/receipts/scan.jpg',
            'attachment_name' => 'scan.jpg',
            'attachment_disk' => 'local',
            'attachment_mime' => 'image/jpeg',
        ]), $this->actor);

        $expense = $this->service()->removeReceipt($expense, $this->actor);

        Storage::disk('local')->assertMissing('expenses/receipts/scan.jpg');
        $this->assertFalse($expense->hasAttachment());
        $this->assertNull($expense->attachment_mime);
        $this->assertNull($expense->attachment_uploaded_by);
    }

    public function test_a_cancelled_expense_will_not_take_a_new_receipt(): void
    {
        $expense = $this->service()->recordExpense($this->expenseData(), $this->actor);
        $this->service()->cancelExpense($expense, 'سُجل في الفرع الخطأ', $this->actor);

        $this->expectException(DomainException::class);

        $this->service()->attachReceipt($expense->fresh(), [
            'path' => 'expenses/receipts/late.jpg',
            'name' => 'late.jpg',
            'disk' => 'local',
            'mime' => 'image/jpeg',
            'size' => 1,
        ], $this->actor);
    }

    public function test_removing_a_receipt_that_was_never_there_is_refused(): void
    {
        $expense = $this->service()->recordExpense($this->expenseData(), $this->actor);

        $this->expectException(DomainException::class);

        $this->service()->removeReceipt($expense, $this->actor);
    }

    // ---- the receipt is evidence for a journal entry ----------------------

    public function test_an_expense_exposes_the_journal_entries_it_produced(): void
    {
        $expense = $this->service()->recordExpense($this->expenseData(), $this->actor);

        $this->assertCount(1, $expense->transactions);

        $this->service()->cancelExpense($expense, 'مكرر', $this->actor);

        $this->assertCount(2, $expense->fresh()->transactions);
    }

    private function createMinimalSchema(): void
    {
        Schema::create('expenses', function (Blueprint $table) {
            $table->id();
            $table->uuid('uuid')->nullable();
            $table->unsignedBigInteger('academy_id');
            $table->unsignedBigInteger('branch_id')->nullable();
            $table->string('category');
            $table->bigInteger('amount');
            $table->string('description');
            $table->string('recipient_name')->nullable();
            $table->string('payment_method');
            $table->string('receipt_reference')->nullable();
            $table->date('expense_date');
            $table->text('notes')->nullable();
            $table->string('attachment_path')->nullable();
            $table->string('attachment_name')->nullable();
            $table->string('attachment_disk')->nullable();
            $table->string('attachment_mime')->nullable();
            $table->unsignedBigInteger('attachment_size')->nullable();
            $table->unsignedBigInteger('attachment_uploaded_by')->nullable();
            $table->timestamp('attachment_uploaded_at')->nullable();
            $table->string('status')->default('active');
            $table->unsignedBigInteger('cancelled_by')->nullable();
            $table->timestamp('cancelled_at')->nullable();
            $table->text('cancellation_reason')->nullable();
            $table->unsignedBigInteger('created_by')->nullable();
            $table->timestamps();
            $table->softDeletes();
        });

        Schema::create('financial_accounts', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('academy_id');
            $table->string('code');
            $table->string('name');
            $table->string('name_ar')->nullable();
            $table->string('type')->nullable();
            $table->timestamps();
        });

        Schema::create('transactions', function (Blueprint $table) {
            $table->id();
            $table->uuid('uuid')->nullable();
            $table->unsignedBigInteger('academy_id');
            $table->unsignedBigInteger('branch_id')->nullable();
            $table->unsignedBigInteger('debit_account_id');
            $table->unsignedBigInteger('credit_account_id');
            $table->string('reference_type')->nullable();
            $table->unsignedBigInteger('reference_id')->nullable();
            $table->bigInteger('amount');
            $table->string('currency', 8)->default('EGP');
            $table->string('type');
            $table->string('description')->nullable();
            $table->date('transaction_date');
            $table->unsignedBigInteger('created_by')->nullable();
            $table->timestamp('created_at')->nullable();
        });

        foreach ([['1000', 'Cash'], ['1010', 'Bank'], ['5020', 'Supplies'], ['5030', 'Maintenance'], ['5050', 'Other']] as [$code, $name]) {
            \Illuminate\Support\Facades\DB::table('financial_accounts')->insert([
                'academy_id' => 1, 'code' => $code, 'name' => $name, 'name_ar' => $name, 'type' => 'expense',
            ]);
        }
    }
}
