<?php

declare(strict_types=1);

/*
 * This file is part of the Ferienpass package.
 *
 * (c) Richard Henkenjohann <richard@ferienpass.online>
 *
 * For more information visit the project website <https://ferienpass.online>
 * or the documentation under <https://docs.ferienpass.online>.
 */

namespace Ferienpass\AdminBundle\Tests\Components;

use Doctrine\ORM\EntityManagerInterface;
use Ferienpass\AdminBundle\Components\AdjustPayment;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\Offer\Base;
use Ferienpass\CoreBundle\Entity\Payment;
use Ferienpass\CoreBundle\Entity\PaymentItem;
use Ferienpass\CoreBundle\Entity\User;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Workflow\WorkflowInterface;

class AdjustPaymentTest extends TestCase
{
    public function testRemoveItem(): void
    {
        $component = $this->component();
        $component->items = [
            ['amount' => 10.0, 'offer' => 'Offer 1', 'attendanceId' => 1, 'type' => 'attendance'],
            ['amount' => 20.0, 'offer' => 'Offer 2', 'attendanceId' => 2, 'type' => 'attendance'],
        ];

        $component->removeItem(0);

        $this->assertCount(1, $component->items);
        $this->assertSame(2, $component->items[0]['attendanceId']);
        $this->assertSame(20.0, $component->getTotalAmount());
    }

    public function testGetTotalAmountSumsItemsAndToleratesStrings(): void
    {
        $component = $this->component();
        // Amounts arrive from the number inputs as strings; an emptied field is ''.
        $component->items = [
            ['amount' => '12.50', 'attendanceId' => 1, 'type' => 'attendance'],
            ['amount' => 7.5, 'attendanceId' => 2, 'type' => 'attendance'],
            ['amount' => '', 'attendanceId' => 3, 'type' => 'attendance'],
        ];

        $this->assertEqualsWithDelta(20.0, $component->getTotalAmount(), 0.0001);
    }

    public function testBuildReversalCancelsEveryPaidAttendanceInFull(): void
    {
        $user = new User();
        $paidA = $this->paidAttendance();
        $paidB = $this->paidAttendance();

        $original = new Payment(null, user: $user);
        $original->setBillingAddress('Musterstr. 1');
        $original->setBillingEmail('kunde@example.com');
        $original->addItem(new PaymentItem($original, 1000, $paidA));
        $original->addItem(new PaymentItem($original, 2500, $paidB));

        $reversal = $this->component()->buildReversal($original, null, $user);

        // The reversal negates the whole original — the "cancel in full" invariant.
        $this->assertCount(2, $reversal->getItems());
        $this->assertSame(-3500, $reversal->getTotalAmount());
        // Reuses the very same managed Attendance instances the original references.
        $this->assertSame(
            [$paidA, $paidB],
            array_map(static fn (PaymentItem $i) => $i->getAttendance(), $reversal->getItems()->toArray())
        );
        // Carries the original's billing details, not the edited ones.
        $this->assertSame('Musterstr. 1', $reversal->getBillingAddress());
        $this->assertSame('kunde@example.com', $reversal->getBillingEmail());
    }

    public function testBuildReversalSkipsUnpaidAndDiscountItems(): void
    {
        $user = new User();
        $paid = $this->paidAttendance();
        $unpaid = new Attendance(new Base(), null); // paid === false by default

        $original = new Payment(null, user: $user);
        $original->setBillingAddress('x');
        $original->addItem(new PaymentItem($original, 1000, $paid));
        $original->addItem(new PaymentItem($original, 800, $unpaid));
        $original->addItem(new PaymentItem($original, -300, null, 'discount'));

        $reversal = $this->component()->buildReversal($original, null, $user);

        // Only the paid attendance posten is reversed; the unpaid one and the
        // Guthaben/discount posten are left untouched.
        $this->assertCount(1, $reversal->getItems());
        $this->assertSame(-1000, $reversal->getTotalAmount());
        $this->assertSame($paid, $reversal->getItems()->first()->getAttendance());
    }

    public function testBuildReversalReversesAPaidLineWhoseAttendanceWasDeleted(): void
    {
        // The offer was deleted after the receipt was issued, so the paid line's
        // attendance is now null (the exact corruption behind the customer's
        // un-refundable third participant). The fee must still be reversible.
        $user = new User();
        $original = new Payment(null, user: $user);
        $original->setBillingAddress('x');
        $orphaned = new PaymentItem($original, 200, $this->paidAttendance());
        $orphaned->removeAttendanceAssociation();
        $original->addItem($orphaned);

        $reversal = $this->component()->buildReversal($original, null, $user);

        $this->assertCount(1, $reversal->getItems());
        $this->assertSame(-200, $reversal->getTotalAmount());
        $this->assertNull($reversal->getItems()->first()->getAttendance());
    }

    public function testBuildReversalHasNoItemsWhenNothingIsPaid(): void
    {
        $user = new User();
        $original = new Payment(null, user: $user);
        $original->setBillingAddress('x');
        $original->addItem(new PaymentItem($original, 1000, new Attendance(new Base(), null)));

        $reversal = $this->component()->buildReversal($original, null, $user);

        // submit() treats an empty reversal as "nothing left to cancel".
        $this->assertTrue($reversal->getItems()->isEmpty());
    }

    public function testBuildNewReceiptBillsOnlyTheEditedRowsSoRemovingOneDropsIt(): void
    {
        $user = new User();
        $attA = new Attendance(new Base(), null);
        $attB = new Attendance(new Base(), null);
        $attC = new Attendance(new Base(), null);
        // The original had A, B and C, but the admin removed C from $items.
        $attendances = [1 => $attA, 2 => $attB, 3 => $attC];

        $component = $this->component();
        $component->address = 'Neue Adresse';
        $component->email = 'neu@example.com';
        $component->items = [
            ['attendanceId' => 1, 'amount' => 10.0, 'type' => 'attendance'],
            ['attendanceId' => 2, 'amount' => 25.0, 'type' => 'attendance'],
        ];

        $new = $component->buildNewReceipt($attendances, null, $user);

        // The removed row (C) is simply absent from the corrected receipt.
        $this->assertCount(2, $new->getItems());
        $this->assertSame(3500, $new->getTotalAmount());
        $this->assertSame(
            [$attA, $attB],
            array_map(static fn (PaymentItem $i) => $i->getAttendance(), $new->getItems()->toArray())
        );
        $this->assertSame('Neue Adresse', $new->getBillingAddress());
        $this->assertSame('neu@example.com', $new->getBillingEmail());
    }

    public function testBuildNewReceiptConvertsEditedEuroAmountsToCents(): void
    {
        $user = new User();
        $attA = new Attendance(new Base(), null);

        $component = $this->component();
        // Amounts arrive from the number inputs as strings.
        $component->items = [
            ['attendanceId' => 1, 'amount' => '12.34', 'type' => 'attendance'],
        ];

        $new = $component->buildNewReceipt([1 => $attA], null, $user);

        $this->assertCount(1, $new->getItems());
        $this->assertSame(1234, $new->getItems()->first()->getAmount());
    }

    public function testBuildNewReceiptSkipsRowsWhoseAttendanceIsUnknown(): void
    {
        $user = new User();
        $attA = new Attendance(new Base(), null);

        $component = $this->component();
        $component->items = [
            ['attendanceId' => 1, 'amount' => 10.0, 'type' => 'attendance'],
            ['attendanceId' => 99, 'amount' => 5.0, 'type' => 'attendance'], // not in the map
            ['amount' => 7.0, 'type' => 'attendance'],                        // no attendanceId key
        ];

        $new = $component->buildNewReceipt([1 => $attA], null, $user);

        $this->assertCount(1, $new->getItems());
        $this->assertSame(1000, $new->getTotalAmount());
    }

    private function paidAttendance(): Attendance
    {
        $attendance = new Attendance(new Base(), null);
        $attendance->setPaid(true);

        return $attendance;
    }

    private function component(): AdjustPayment
    {
        return new AdjustPayment(
            $this->createStub(EntityManagerInterface::class),
            $this->createStub(WorkflowInterface::class),
        );
    }
}
