<?php

namespace Tests\Unit;

use App\Domain\Pricing\Models\PricingRule;
use App\Domain\Pricing\Services\PricingService;
use App\Domain\Pricing\Support\ConditionSchema;
use ReflectionMethod;
use Tests\TestCase;

/**
 * A sibling discount that applies to only one membership tier.
 *
 * OC-Sport's compounds charge a resident 2,200 and a non-resident 2,800, and the
 * second child of a non-resident 2,400 — while a resident's second child stays at
 * 2,200, because the resident price is already the discounted one. A
 * `sibling_order` rule could express the "second child" half but not the "and only
 * for non-residents" half, and the arithmetic has no shortcut: a flat 400 off
 * takes a resident to 1,800, and a percentage that lands on 2,400 from 2,800
 * lands on 1,886 from 2,200.
 *
 * The alternative was a second stacked rule holding `fixed_price 220000` to push
 * residents back up — which puts the resident base price in two places. The day
 * fees rise and only `base_prices` is edited, that rule silently forces every
 * resident back to the old number, with no error and no missing-price failure to
 * notice. So the tier filter lives on the rule that owns the condition.
 *
 * The key is optional. Every rule authored before it existed has no
 * `membership_type` in its conditions, so the first test here is the one that
 * matters to the other tenants: absent key, unchanged behaviour.
 */
class SiblingDiscountByMembershipTierTest extends TestCase
{
    // ---- the engine -------------------------------------------------------

    public function test_a_sibling_rule_without_the_tier_key_still_matches_both_tiers(): void
    {
        $rule = $this->rule('sibling_order', ['min' => 2]);

        $this->assertTrue($this->ruleMatches($rule, ['sibling_order' => 2, 'membership_type' => 'member']));
        $this->assertTrue($this->ruleMatches($rule, ['sibling_order' => 2, 'membership_type' => 'non_member']));
    }

    public function test_a_sibling_rule_scoped_to_non_members_skips_a_member(): void
    {
        $rule = $this->rule('sibling_order', ['min' => 2, 'membership_type' => ['non_member']]);

        $this->assertFalse($this->ruleMatches($rule, ['sibling_order' => 2, 'membership_type' => 'member']));
    }

    public function test_a_sibling_rule_scoped_to_non_members_matches_a_non_member(): void
    {
        $rule = $this->rule('sibling_order', ['min' => 2, 'membership_type' => ['non_member']]);

        $this->assertTrue($this->ruleMatches($rule, ['sibling_order' => 2, 'membership_type' => 'non_member']));
    }

    public function test_the_tier_filter_does_not_widen_the_range(): void
    {
        // The right tier is still not enough — a first child is not a sibling.
        $rule = $this->rule('sibling_order', ['min' => 2, 'membership_type' => ['non_member']]);

        $this->assertFalse($this->ruleMatches($rule, ['sibling_order' => 1, 'membership_type' => 'non_member']));
    }

    public function test_an_unknown_tier_in_the_context_fails_closed(): void
    {
        // evaluateInList fails closed, so a rule scoped to a tier never becomes an
        // academy-wide discount because the buyer's tier could not be resolved.
        $rule = $this->rule('sibling_order', ['min' => 2, 'membership_type' => ['non_member']]);

        $this->assertFalse($this->ruleMatches($rule, ['sibling_order' => 2, 'membership_type' => null]));
    }

    public function test_family_size_carries_the_same_filter(): void
    {
        $rule = $this->rule('family_size', ['min' => 2, 'membership_type' => ['member']]);

        $this->assertTrue($this->ruleMatches($rule, ['family_size' => 3, 'membership_type' => 'member']));
        $this->assertFalse($this->ruleMatches($rule, ['family_size' => 3, 'membership_type' => 'non_member']));
    }

    public function test_both_tiers_listed_is_the_same_as_no_filter(): void
    {
        $rule = $this->rule('sibling_order', ['min' => 2, 'membership_type' => ['member', 'non_member']]);

        $this->assertTrue($this->ruleMatches($rule, ['sibling_order' => 2, 'membership_type' => 'member']));
        $this->assertTrue($this->ruleMatches($rule, ['sibling_order' => 2, 'membership_type' => 'non_member']));
    }

    // ---- the schema -------------------------------------------------------

    public function test_normalize_keeps_the_tier_on_a_sibling_rule(): void
    {
        $out = ConditionSchema::normalize('sibling_order', [
            'min' => '2',
            'membership_type' => ['non_member'],
        ]);

        $this->assertSame(['min' => 2, 'membership_type' => ['non_member']], $out);
    }

    public function test_normalize_strips_the_tier_from_a_rule_type_that_cannot_use_it(): void
    {
        // The engine only consults the filter for sibling_order and family_size.
        // Storing it on an age rule would be a condition that reads as a promise
        // and is never evaluated.
        $out = ConditionSchema::normalize('age', [
            'min' => 6,
            'membership_type' => ['non_member'],
        ]);

        $this->assertSame(['min' => 6], $out);
    }

    public function test_normalize_drops_a_tier_value_that_is_not_a_tier(): void
    {
        $out = ConditionSchema::normalize('sibling_order', [
            'min' => 2,
            'membership_type' => ['non_member', 'platinum'],
        ]);

        $this->assertSame(['non_member'], $out['membership_type']);
    }

    public function test_normalize_accepts_a_bare_string_tier(): void
    {
        $out = ConditionSchema::normalize('sibling_order', [
            'min' => 2,
            'membership_type' => 'member',
        ]);

        $this->assertSame(['member'], $out['membership_type']);
    }

    public function test_the_builder_validates_the_tier_only_where_it_is_allowed(): void
    {
        $this->assertArrayHasKey('conditions.membership_type', ConditionSchema::validationRules('sibling_order'));
        $this->assertArrayHasKey('conditions.membership_type', ConditionSchema::validationRules('family_size'));
        $this->assertArrayNotHasKey('conditions.membership_type', ConditionSchema::validationRules('age'));
    }

    public function test_the_sentence_names_the_tier(): void
    {
        $with = ConditionSchema::describe('sibling_order', ['min' => 2, 'membership_type' => ['non_member']]);
        $without = ConditionSchema::describe('sibling_order', ['min' => 2]);

        // The rejection reason in the discount picker has to stay a sentence, so
        // the tier is appended to the existing clause rather than replacing it.
        $this->assertStringContainsString('للابن رقم 2', $with);
        $this->assertStringContainsString(ConditionSchema::membershipTypeLabel('non_member'), $with);
        $this->assertStringNotContainsString(ConditionSchema::membershipTypeLabel('non_member'), $without);
    }

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

    private function rule(string $type, array $conditions): PricingRule
    {
        return new PricingRule([
            'rule_type'  => $type,
            'conditions' => $conditions,
        ]);
    }

    /**
     * evaluateConditions() is private and stays that way — it is engine internals,
     * not an API. Reaching it directly keeps these tests free of a database, which
     * matters because none of the pricing migrations run on sqlite.
     */
    private function ruleMatches(PricingRule $rule, array $context): bool
    {
        $method = new ReflectionMethod(PricingService::class, 'evaluateConditions');
        $method->setAccessible(true);

        return $method->invoke(new PricingService(), $rule, $context);
    }
}
