<?php

namespace Tests\Feature;

use App\Domain\Financial\Services\AccountAnomalyScanner;
use App\Domain\Participant\Models\Participant;
use App\Domain\Training\Models\Enrollment;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;

/**
 * What a programme requirement obliges a player to pay.
 *
 * The reported file: a boy on the 8,000 registration card. He paid 2,500 in
 * July and 2,500 in August — 5,000 — and the settlement screen told the
 * operator he had paid 5,000 of 10,500 and still owed 5,500, on a card that
 * never cost more than 8,000.
 *
 * The 10,500 came from three hand-typed invoices: the first instalment typed
 * once in July, typed AGAIN in August as a "correction", and then a remainder
 * invoice computed as 8,000 − 2,500 that took no account of either. The reading
 * side then did `max(billed, price)` — so every duplicate line raised what he
 * was said to owe, and the "record the rest" button offered to bill the
 * inflated remainder on top.
 *
 * Hand-typed lines are instalments TOWARD a card. The card's price is the
 * obligation; anything typed past it is the same money entered twice.
 */
class BundleObligationTest extends TestCase
{
    private const PRODUCT_ID = 2;
    private const PRODUCT_NAME = 'قيد اشتراك فريق اتحاد الكرة';
    private const PROGRAM_ID = 2;
    private const CARD_PRICE = 800000;

    private int $currentInvoice = 0;

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

        if (config('database.default') !== 'sqlite') {
            $this->markTestSkipped('Builds its own schema; runs on the in-memory SQLite connection.');
        }

        $this->createMinimalSchema();

        DB::table('training_programs')->insert([
            'id' => self::PROGRAM_ID, 'academy_id' => 1, 'name_ar' => 'فريق 2015', 'deleted_at' => null,
        ]);

        DB::table('products')->insert([
            'id' => self::PRODUCT_ID,
            'academy_id' => 1,
            'name_ar' => self::PRODUCT_NAME,
            'selling_price' => self::CARD_PRICE,
            'member_price' => null,
            'non_member_price' => null,
            'deleted_at' => null,
        ]);

        DB::table('program_products')->insert([
            'training_program_id' => self::PROGRAM_ID,
            'product_id' => self::PRODUCT_ID,
            'is_required' => true,
            'quantity' => 1,
        ]);
    }

    // ---- the reported file ------------------------------------------------

    public function test_hand_typed_instalments_never_raise_the_card_above_its_price(): void
    {
        $this->theReportedAccount();

        $row = $this->bundleFor(128);

        $this->assertSame(1050000, $row['billed'], 'The books really do carry three lines totalling 10,500.');
        $this->assertSame(500000, $row['paid']);
        $this->assertSame(self::CARD_PRICE, $row['expected'], 'The card costs 8,000, so 8,000 is what he owes.');
        $this->assertSame(300000, $row['expected'] - $row['paid'], 'Remaining is 3,000, not 5,500.');
    }

    public function test_the_excess_is_reported_rather_than_absorbed(): void
    {
        $this->theReportedAccount();

        $row = $this->bundleFor(128);

        $this->assertSame(250000, $row['over_billed'], 'The duplicated first instalment is 2,500 too much.');
    }

    public function test_the_account_is_flagged_for_being_billed_twice(): void
    {
        $this->theReportedAccount();

        $cases = $this->casesFor(128);

        $this->assertContains('over_billed_bundle', $cases);
    }

    // ---- what must not change ---------------------------------------------

    public function test_an_honest_part_payment_still_reads_as_part_paid(): void
    {
        // One instalment typed, nothing duplicated: 2,500 of an 8,000 card.
        $this->invoice(310, 128, '2026-07-02', subtotal: 250000, total: 250000, paid: 250000)
            ->line(1, 'القسط الاول من القيد', 250000);

        $row = $this->bundleFor(128);

        $this->assertSame(self::CARD_PRICE, $row['expected']);
        $this->assertSame(250000, $row['paid']);
        $this->assertSame('partial', $row['status']);
        $this->assertSame(0, $row['over_billed']);
    }

    public function test_a_real_sale_keeps_the_price_it_froze(): void
    {
        // Sold through the till at an agreed 9,000 — above today's list price.
        // An invoice's prices freeze at creation, so the sale is the obligation
        // and the product record does not get to overrule it.
        $this->invoice(400, 129, '2026-07-02', subtotal: 900000, total: 900000, paid: 900000)
            ->line(1, self::PRODUCT_NAME, 900000, itemableType: 'App\\Domain\\Inventory\\Models\\Product', itemableId: self::PRODUCT_ID);

        $row = $this->bundleFor(129);

        $this->assertSame(900000, $row['expected']);
        $this->assertSame('paid', $row['status']);
        $this->assertSame(0, $row['over_billed'], 'A frozen sale price is never "over-billed".');
    }

    public function test_a_card_nobody_billed_is_still_missing(): void
    {
        $this->invoice(500, 130, '2026-07-02', subtotal: 90000, total: 90000, paid: 90000)
            ->line(1, 'اشتراك يوليو 2026 — فريق 2015', 90000);

        $row = $this->bundleFor(130);
        $this->assertSame('missing', $row['status']);
        $this->assertSame(self::CARD_PRICE, $row['expected'], 'Nothing billed: the price is what he would be charged.');
    }

    // ---- fixtures ---------------------------------------------------------

    /**
     * The three invoices from the reported profile, exactly as they stand.
     */
    private function theReportedAccount(): void
    {
        $this->invoice(310, 128, '2026-07-02', subtotal: 250000, total: 250000, paid: 250000)
            ->line(1, 'القسط الاول من القيد', 250000);

        $this->invoice(588, 128, '2026-08-05', subtotal: 250000, total: 250000, paid: 250000)
            ->line(2, 'القسط الأول من القيد — تصحيح من قيد كامل', 250000);

        $this->invoice(593, 128, '2026-08-06', subtotal: 550000, total: 550000, paid: 0, status: 'sent')
            ->line(3, 'أقساط متبقية من القيد (8000 - 2500 قسط أول = 5500)', 550000);
    }

    private function participant(int $id): Participant
    {
        $participant = new Participant();
        $participant->id = $id;
        $participant->is_free = false;
        $participant->membership_type = 'non_member';
        $participant->setRelation('enrollments', collect([
            tap(new Enrollment(), function ($e) use ($id) {
                $e->id = $id;
                $e->training_program_id = self::PROGRAM_ID;
            }),
        ]));

        return $participant;
    }

    /**
     * bundleStatus() is private — it is reached the way the settlement wizard
     * reaches it, through the public scan of one account.
     */
    private function bundleFor(int $participantId): array
    {
        $scanner = new AccountAnomalyScanner();
        $method = new \ReflectionMethod($scanner, 'bundleStatus');
        $method->setAccessible(true);

        $out = $method->invoke($scanner, collect([$this->participant($participantId)]));

        return $out[$participantId][0] ?? [];
    }

    /**
     * @return array<int, string>
     */
    private function casesFor(int $participantId): array
    {
        $row = $this->bundleFor($participantId);
        $cases = [];

        if (($row['over_billed'] ?? 0) > 0) {
            $cases[] = 'over_billed_bundle';
        }
        if (($row['status'] ?? '') === 'missing') {
            $cases[] = 'missing_bundle';
        }

        $this->assertArrayHasKey('over_billed_bundle', AccountAnomalyScanner::CASES);

        return $cases;
    }

    private function invoice(
        int $id,
        int $participantId,
        string $issueDate,
        int $subtotal,
        int $total,
        int $paid,
        string $status = 'paid',
    ): self {
        DB::table('invoices')->insert([
            'id' => $id,
            'number' => 'INV-' . str_pad((string) $id, 6, '0', STR_PAD_LEFT),
            'academy_id' => 1,
            'branch_id' => null,
            'billable_type' => 'App\\Domain\\Participant\\Models\\Participant',
            'billable_id' => $participantId,
            'subtotal_amount' => $subtotal,
            'total_amount' => $total,
            'issue_date' => $issueDate,
            'status' => $status,
            'notes' => null,
            'metadata' => null,
            'deleted_at' => null,
        ]);

        if ($paid > 0) {
            DB::table('payments')->insert([
                'id' => $id,
                'invoice_id' => $id,
                'amount' => $paid,
                'status' => 'confirmed',
                'direction' => 'inbound',
                'deleted_at' => null,
            ]);
        }

        $this->currentInvoice = $id;

        return $this;
    }

    private function line(
        int $id,
        string $description,
        int $total,
        ?string $itemableType = null,
        ?int $itemableId = null,
    ): self {
        DB::table('invoice_items')->insert([
            'id' => $id,
            'invoice_id' => $this->currentInvoice,
            'itemable_type' => $itemableType,
            'itemable_id' => $itemableId,
            'description' => $description,
            'quantity' => 1,
            'total_amount' => $total,
            'metadata' => null,
        ]);

        return $this;
    }

    private function createMinimalSchema(): void
    {
        Schema::create('invoices', function (Blueprint $table) {
            $table->unsignedBigInteger('id')->primary();
            $table->string('number')->nullable();
            $table->unsignedBigInteger('academy_id')->nullable();
            $table->unsignedBigInteger('branch_id')->nullable();
            $table->string('billable_type')->nullable();
            $table->unsignedBigInteger('billable_id')->nullable();
            $table->bigInteger('subtotal_amount')->default(0);
            $table->bigInteger('total_amount')->default(0);
            $table->date('issue_date')->nullable();
            $table->string('status')->nullable();
            $table->text('notes')->nullable();
            $table->text('metadata')->nullable();
            $table->timestamp('deleted_at')->nullable();
        });

        Schema::create('training_programs', function (Blueprint $table) {
            $table->unsignedBigInteger('id')->primary();
            $table->unsignedBigInteger('academy_id')->nullable();
            $table->string('name_ar')->nullable();
            $table->timestamp('deleted_at')->nullable();
        });

        Schema::create('products', function (Blueprint $table) {
            $table->unsignedBigInteger('id')->primary();
            $table->unsignedBigInteger('academy_id')->nullable();
            $table->string('name_ar')->nullable();
            $table->bigInteger('selling_price')->default(0);
            $table->bigInteger('member_price')->nullable();
            $table->bigInteger('non_member_price')->nullable();
            $table->timestamp('deleted_at')->nullable();
        });

        Schema::create('program_products', function (Blueprint $table) {
            $table->unsignedBigInteger('training_program_id');
            $table->unsignedBigInteger('product_id');
            $table->boolean('is_required')->default(true);
            $table->integer('quantity')->default(1);
        });

        Schema::create('invoice_items', function (Blueprint $table) {
            $table->unsignedBigInteger('id')->primary();
            $table->unsignedBigInteger('invoice_id');
            $table->string('itemable_type')->nullable();
            $table->unsignedBigInteger('itemable_id')->nullable();
            $table->text('description')->nullable();
            $table->integer('quantity')->default(1);
            $table->bigInteger('total_amount')->default(0);
            $table->text('metadata')->nullable();
        });

        Schema::create('payments', function (Blueprint $table) {
            $table->unsignedBigInteger('id')->primary();
            $table->unsignedBigInteger('invoice_id');
            $table->bigInteger('amount')->default(0);
            $table->string('status')->nullable();
            $table->string('direction')->nullable();
            $table->timestamp('deleted_at')->nullable();
        });

        Schema::create('payment_plans', function (Blueprint $table) {
            $table->unsignedBigInteger('id')->primary();
            $table->unsignedBigInteger('invoice_id');
            $table->string('status')->nullable();
            $table->date('next_due_date')->nullable();
            $table->integer('total_installments')->default(0);
            $table->integer('paid_installments')->default(0);
            $table->bigInteger('installment_amount')->default(0);
        });
    }
}
