<?php

namespace Tests\Unit;

use App\Domain\Inventory\Models\Product;
use App\Domain\Inventory\Models\ProductInstallmentPlan;
use App\Domain\Pricing\Models\BasePrice;
use App\Domain\Pricing\Services\PricingService;
use Tests\TestCase;

/**
 * Two prices for one product, and the schedule you pay it on.
 *
 * A product can be given a member rate and a non-member rate on the product
 * screen, and instalment plans open to one tier or the other. Only the
 * registration wizard ever read any of it: the till resolved prices through the
 * pricing engine, the engine only knew about `base_prices`, and nothing writes
 * a product's member rate there — so every member who bought at reception paid
 * the walk-in price and no line on the receipt said so.
 *
 * These pin the precedence between the two places a price can live, and the
 * arithmetic of splitting a price into instalments.
 */
class MemberPricingAndInstalmentsTest extends TestCase
{
    // ---- which price wins -------------------------------------------------

    public function test_a_member_rate_on_the_product_beats_an_untagged_base_price(): void
    {
        $product = new Product(['selling_price' => 800000, 'member_price' => 600000]);
        $untagged = new BasePrice(['amount' => 800000, 'metadata' => []]);

        $this->assertSame(600000, $this->resolve($product, $untagged, 'member'));
    }

    public function test_a_base_price_tagged_for_this_tier_beats_the_product_column(): void
    {
        // Someone configured a members' base price deliberately, in the screen
        // built for it. Nothing outranks that.
        $product = new Product(['selling_price' => 800000, 'member_price' => 600000]);
        $tagged = new BasePrice(['amount' => 550000, 'metadata' => ['membership_type' => 'member']]);

        $this->assertSame(550000, $this->resolve($product, $tagged, 'member'));
    }

    public function test_a_non_member_gets_the_non_member_column_not_the_member_one(): void
    {
        $product = new Product([
            'selling_price' => 800000,
            'member_price' => 600000,
            'non_member_price' => 800000,
        ]);

        $this->assertSame(800000, $this->resolve($product, null, 'non_member'));
        $this->assertSame(600000, $this->resolve($product, null, 'member'));
    }

    public function test_an_unfilled_tier_column_falls_through_to_the_base_price(): void
    {
        // member_price left blank: this member is not on a member rate, he is
        // on whatever price the catalogue was given.
        $product = new Product(['selling_price' => 800000, 'non_member_price' => 800000]);
        $untagged = new BasePrice(['amount' => 750000, 'metadata' => []]);

        $this->assertSame(750000, $this->resolve($product, $untagged, 'member'));
    }

    public function test_no_configured_price_anywhere_is_a_hard_fail_not_a_guess(): void
    {
        // selling_price is the catalogue's advertised number, not a configured
        // price. Letting it answer here would make "nobody set a price" — which
        // is meant to stop the sale — impossible to detect.
        $product = new Product(['selling_price' => 800000]);

        $this->assertNull($this->resolve($product, null, 'member'));
    }

    public function test_the_engine_only_claims_a_base_price_row_it_actually_used(): void
    {
        $product = new Product(['selling_price' => 800000, 'member_price' => 600000]);

        $this->assertNull($product->explicitTierPrice('non_member'));
        $this->assertSame(600000, $product->explicitTierPrice('member'));
        $this->assertSame(800000, $product->priceForTier('non_member'), 'Falls back to the catalogue price.');
    }

    // ---- the schedule -----------------------------------------------------

    public function test_a_schedule_adds_up_to_exactly_the_price(): void
    {
        // 8,000.00 over three: 266,666 + 266,666 + 266,668 piasters. Rounding
        // each slot up instead produced 800,001 — a plan asking for a piaster
        // the invoice never billed, which can never reach `completed`.
        $schedule = $this->plan(installments: 3)->buildSchedule(800000);

        $this->assertSame(800000, array_sum($schedule));
        $this->assertCount(3, $schedule);
        $this->assertSame([266666, 266666, 266668], array_values($schedule));
    }

    public function test_a_down_payment_percentage_still_leaves_the_rest_exact(): void
    {
        $schedule = $this->plan(installments: 3, downPaymentPct: 50)->buildSchedule(800000);

        $this->assertSame(800000, array_sum($schedule));
    }

    public function test_explicit_slot_amounts_are_honoured_and_the_rest_fills_the_gap(): void
    {
        // The 2,500 first instalment the receptionists actually take, with the
        // remaining 5,500 split across two.
        $schedule = $this->plan(installments: 3, amounts: [250000, null, null])->buildSchedule(800000);

        $this->assertSame([250000, 275000, 275000], array_values($schedule));
        $this->assertSame(800000, array_sum($schedule));
    }

    public function test_a_single_instalment_plan_is_just_the_whole_price(): void
    {
        $this->assertSame([800000], array_values($this->plan(installments: 1)->buildSchedule(800000)));
    }

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

    private function resolve(Product $product, ?BasePrice $basePrice, string $tier): ?int
    {
        $method = new \ReflectionMethod(PricingService::class, 'resolveBaseAmount');
        $method->setAccessible(true);

        return $method->invoke(app(PricingService::class), $product, $basePrice, $tier);
    }

    private function plan(int $installments, int $downPaymentPct = 0, ?array $amounts = null): ProductInstallmentPlan
    {
        return new ProductInstallmentPlan([
            'membership_tier' => 'any',
            'label_ar' => 'خطة',
            'installments' => $installments,
            'frequency' => 'monthly',
            'down_payment_pct' => $downPaymentPct,
            'installment_amounts' => $amounts,
            'is_active' => true,
        ]);
    }
}
