<?php

namespace Tests\Feature;

use App\Domain\Financial\Services\ParticipantBillingService;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;

/**
 * The registration-card column on the group roster.
 *
 * It answered "is there an invoice line carrying this product's morph?", which
 * is how the POS writes a sale and not how most of this money was collected.
 * A receptionist taking the first instalment types "القسط الاول من القيد" into
 * a free-text line; the player has paid, and the roster called him a
 * non-buyer — nine of them on OC-Sport, every one having paid.
 *
 * Every scenario below is a real invoice from that academy.
 */
class BundledProductPaymentTest extends TestCase
{
    private const PRODUCT_ID = 2;
    private const PRODUCT_NAME = 'قيد اشتراك فريق اتحاد الكرة';
    private const PRODUCT_CLASS = 'App\\Domain\\Inventory\\Models\\Product';

    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();

        $this->program(1, 'فريق 2011/2012');
        $this->program(2, 'فريق 2015');
        $this->product(self::PRODUCT_ID, self::PRODUCT_NAME);
        $this->product(3, 'شنطة ملابس كرة القدم');
    }

    private function facts(int $participantId, bool $inferBare = false): ?array
    {
        return (new ParticipantBillingService())->bundledProductForParticipants(
            [$participantId],
            self::PRODUCT_ID,
            self::PRODUCT_NAME,
            null,
            $inferBare,
        )[$participantId] ?? null;
    }

    // ---- what the column already got right --------------------------------

    public function test_a_card_sold_through_the_till_still_reads_as_a_product_sale(): void
    {
        // The common case: the full 8,000 billed on day one, 2,500 collected
        // against a three-instalment plan.
        $this->invoice(1, 300, '2026-07-10', subtotal: 800000, total: 800000, paid: 250000)
            ->line(1, self::PRODUCT_NAME, 800000, itemableType: self::PRODUCT_CLASS, itemableId: self::PRODUCT_ID)
            ->plan(1, total: 3, paid: 1, amount: 250000);

        $facts = $this->facts(300);

        $this->assertSame(800000, $facts['billed']);
        $this->assertSame(250000, $facts['paid']);
        $this->assertTrue($facts['from_product_lines']);
        $this->assertSame(['paid' => 1, 'total' => 3, 'amount' => 250000], $facts['plan']);
    }

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

    public function test_an_instalment_typed_by_hand_is_counted(): void
    {
        // Production invoice 310 (participant 128): no product line anywhere,
        // and the roster showed him as never having bought the card.
        $this->invoice(1, 128, '2026-07-02', subtotal: 250000, total: 250000, paid: 250000)
            ->line(1, 'القسط الاول من القيد', 250000);

        $facts = $this->facts(128);

        $this->assertNotNull($facts, 'Money typed as text is still money paid toward the card.');
        $this->assertSame(250000, $facts['paid']);
        $this->assertFalse($facts['from_product_lines']);
        $this->assertTrue($facts['from_text']);
    }

    public function test_a_card_paid_on_the_same_invoice_as_other_goods_is_split_out(): void
    {
        // Production invoice 289 (participant 97): a 3,200 invoice paying 2,000
        // of the card and 1,200 for the bag, settled in full. Only the card's
        // share belongs in this column.
        $this->invoice(1, 97, '2026-07-26', subtotal: 320000, total: 320000, paid: 320000)
            ->line(1, 'القسط الاول من القيد', 200000)
            ->line(2, 'الزي', 120000);

        $facts = $this->facts(97);

        $this->assertSame(200000, $facts['billed']);
        $this->assertSame(200000, $facts['paid']);
    }

    public function test_a_part_paid_bundled_invoice_allocates_the_card_its_share(): void
    {
        // Half of a 5,000 invoice paid: the card is 3,000 of it, so 1,500 of
        // the 2,500 collected belongs to the card.
        $this->invoice(1, 217, '2026-07-23', subtotal: 500000, total: 500000, paid: 250000)
            ->line(1, 'القيد', 300000)
            ->line(2, 'اشتراك يوليو 2026', 200000);

        $facts = $this->facts(217);

        $this->assertSame(300000, $facts['billed']);
        $this->assertSame(150000, $facts['paid']);
    }

    public function test_a_card_billed_and_not_paid_shows_as_owing_rather_than_missing(): void
    {
        $this->invoice(1, 76, '2026-07-28', subtotal: 250000, total: 250000, paid: 0, status: 'sent')
            ->line(1, 'قسط قيد', 250000);

        $facts = $this->facts(76);

        $this->assertSame(250000, $facts['billed']);
        $this->assertSame(0, $facts['paid']);
    }

    // ---- training money must never land in this column --------------------

    public function test_a_month_of_training_is_not_the_card(): void
    {
        $this->invoice(1, 145, '2026-09-01', subtotal: 90000, total: 90000, paid: 90000)
            ->line(1, 'اشتراك سبتمبر 2026: فريق 2011', 90000);

        $this->assertNull($this->facts(145));
    }

    public function test_another_products_sale_is_not_the_card(): void
    {
        $this->invoice(1, 146, '2026-07-26', subtotal: 120000, total: 120000, paid: 120000)
            ->line(1, 'شنطة ملابس كرة القدم', 120000, itemableType: self::PRODUCT_CLASS, itemableId: 3);

        $this->assertNull($this->facts(146));
    }

    public function test_a_cancelled_invoice_is_not_money(): void
    {
        $this->invoice(1, 147, '2026-07-26', subtotal: 250000, total: 250000, paid: 0, status: 'cancelled')
            ->line(1, 'قسط القيد', 250000);

        $this->assertNull($this->facts(147));
    }

    // ---- the instalment that names nothing --------------------------------

    public function test_a_bare_instalment_is_left_alone_unless_the_caller_asks(): void
    {
        // Production invoice 285 (participant 219): "الزي" and "القسط الاول".
        // Nothing in the text says what the instalment pays for, so a roster
        // whose programme requires several products must not guess.
        $this->invoice(1, 219, '2026-07-25', subtotal: 370000, total: 370000, paid: 370000)
            ->line(1, 'الزي', 120000)
            ->line(2, 'القسط الاول', 250000);

        $this->assertNull($this->facts(219), 'Without context the line is not attributable.');
    }

    public function test_a_bare_instalment_counts_when_the_programme_requires_only_this_product(): void
    {
        $this->invoice(1, 219, '2026-07-25', subtotal: 370000, total: 370000, paid: 370000)
            ->line(1, 'الزي', 120000)
            ->line(2, 'القسط الاول', 250000);

        $facts = $this->facts(219, inferBare: true);

        $this->assertSame(250000, $facts['paid'], 'The one thing paid in instalments here is the card.');
        $this->assertTrue($facts['inferred']);
    }

    public function test_an_instalment_on_an_invoice_that_also_pays_for_training_is_left_alone(): void
    {
        // The instalment could as easily be part of the subscription on the
        // line above it. A guess that moves money between two columns of the
        // same roster is worse than no answer.
        $this->invoice(1, 220, '2026-07-25', subtotal: 340000, total: 340000, paid: 340000)
            ->line(1, 'اشتراك يوليو 2026: فريق 2015', 90000)
            ->line(2, 'القسط الاول', 250000);

        $this->assertNull($this->facts(220, inferBare: true));
    }

    // ---- money is never counted twice -------------------------------------

    public function test_the_card_and_the_subscription_never_claim_the_same_piastres(): void
    {
        $this->invoice(1, 221, '2026-07-25', subtotal: 340000, total: 340000, paid: 340000)
            ->line(1, 'اشتراك يوليو 2026: فريق 2015', 90000)
            ->line(2, 'قسط القيد', 250000);

        $card = $this->facts(221);
        $subscription = (new ParticipantBillingService())
            ->subscriptionForPeriod([221], '2026-07-01', '2026-08-01')[221] ?? null;

        $this->assertSame(250000, $card['paid']);
        $this->assertSame(90000, $subscription['paid']);
        $this->assertSame(340000, $card['paid'] + $subscription['paid'], 'Every piastre is in exactly one column.');
    }

    public function test_several_invoices_add_up(): void
    {
        // Production participant 128: two instalments, then the remainder
        // billed and unpaid.
        $this->invoice(1, 128, '2026-07-02', subtotal: 250000, total: 250000, paid: 250000)
            ->line(1, 'القسط الاول من القيد', 250000);
        $this->invoice(2, 128, '2026-08-05', subtotal: 250000, total: 250000, paid: 250000)
            ->line(2, 'القسط الأول من القيد', 250000);
        $this->invoice(3, 128, '2026-08-06', subtotal: 550000, total: 550000, paid: 0, status: 'sent')
            ->line(3, 'أقساط متبقية من قيد اتحاد الكرة', 550000);

        $facts = $this->facts(128);

        $this->assertSame(1050000, $facts['billed']);
        $this->assertSame(500000, $facts['paid']);
    }

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

    private int $currentInvoice = 0;

    private function program(int $id, string $nameAr): void
    {
        DB::table('training_programs')->insert(['id' => $id, 'academy_id' => 1, 'name_ar' => $nameAr, 'deleted_at' => null]);
    }

    private function product(int $id, string $nameAr): void
    {
        DB::table('products')->insert(['id' => $id, 'academy_id' => 1, 'name_ar' => $nameAr, 'deleted_at' => null]);
    }

    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,
            '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 plan(int $id, int $total, int $paid, int $amount): self
    {
        DB::table('payment_plans')->insert([
            'id' => $id,
            'invoice_id' => $this->currentInvoice,
            'status' => 'active',
            'total_installments' => $total,
            'paid_installments' => $paid,
            'installment_amount' => $amount,
        ]);

        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->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->timestamp('deleted_at')->nullable();
        });

        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->integer('total_installments')->default(0);
            $table->integer('paid_installments')->default(0);
            $table->bigInteger('installment_amount')->default(0);
        });
    }
}
