<?php

declare(strict_types=1);

/*
 * This file is part of SolidInvoice project.
 *
 * (c) Pierre du Plessis <open-source@solidworx.co>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace SolidInvoice\CoreBundle\Tests\Billing;

use Brick\Math\BigDecimal;
use Brick\Math\Exception\MathException;
use Doctrine\ORM\Exception\NotSupported;
use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\OptimisticLockException;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use SolidInvoice\ClientBundle\Test\Factory\ClientFactory;
use SolidInvoice\CoreBundle\Billing\TotalCalculator;
use SolidInvoice\CoreBundle\Entity\Discount;
use SolidInvoice\CoreBundle\Exception\UnexpectedTypeException;
use SolidInvoice\CoreBundle\Test\Traits\DoctrineTestTrait;
use SolidInvoice\InvoiceBundle\Entity\Invoice;
use SolidInvoice\InvoiceBundle\Entity\Line;
use SolidInvoice\InvoiceBundle\Model\Graph;
use SolidInvoice\MoneyBundle\Calculator;
use SolidInvoice\PaymentBundle\Entity\Payment;
use SolidInvoice\PaymentBundle\Model\Status;
use SolidInvoice\TaxBundle\Entity\Tax;
use stdClass;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Zenstruck\Foundry\Test\Factories;

class TotalCalculatorTest extends KernelTestCase
{
    use DoctrineTestTrait;
    use MockeryPHPUnitIntegration;
    use Factories;

    public function testOnlyAcceptsQuotesOrInvoices(): void
    {
        $updater = new TotalCalculator($this->em->getRepository(Payment::class), new Calculator());

        $this->expectException(UnexpectedTypeException::class);
        $this->expectExceptionMessage('Expected argument of type "Invoice or Quote", "stdClass" given');
        $updater->calculateTotals(new stdClass());
    }

    /**
     * @throws MathException
     * @throws NotSupported
     */
    public function testUpdateWithSingleItem(): void
    {
        $updater = new TotalCalculator($this->em->getRepository(Payment::class), new Calculator());

        $invoice = new Invoice();
        $invoice->setClient(ClientFactory::createOne(['currencyCode' => 'USD'])->_real());
        $item = new Line();
        $item->setQty(1)
            ->setPrice(15000);
        $invoice->addLine($item);

        $updater->calculateTotals($invoice);

        self::assertEquals(BigDecimal::of(15000), $invoice->getTotal());
        self::assertEquals(BigDecimal::of(15000), $invoice->getBalance());
        self::assertEquals(BigDecimal::of(15000), $invoice->getBaseTotal());
    }

    /**
     * @throws MathException
     * @throws NotSupported
     */
    public function testUpdateWithSingleItemAndMultipleQtys(): void
    {
        $updater = new TotalCalculator($this->em->getRepository(Payment::class), new Calculator());

        $invoice = new Invoice();
        $invoice->setClient(ClientFactory::createOne(['currencyCode' => 'USD'])->_real());
        $item = new Line();
        $item->setQty(2)
            ->setPrice(15000);
        $invoice->addLine($item);

        $updater->calculateTotals($invoice);

        self::assertEquals(BigDecimal::of(30000), $invoice->getTotal());
        self::assertEquals(BigDecimal::of(30000), $invoice->getBalance());
        self::assertEquals(BigDecimal::of(30000), $invoice->getBaseTotal());
    }

    /**
     * @throws MathException
     * @throws NotSupported
     */
    public function testUpdateWithPercentageDiscount(): void
    {
        $updater = new TotalCalculator($this->em->getRepository(Payment::class), new Calculator());

        $invoice = new Invoice();
        $invoice->setClient(ClientFactory::createOne(['currencyCode' => 'USD'])->_real());
        $item = new Line();
        $item->setQty(2)
            ->setPrice(15000);
        $invoice->addLine($item);
        $discount = new Discount();
        $discount->setType(Discount::TYPE_PERCENTAGE);
        $discount->setValue(15);
        $invoice->setDiscount($discount);

        $updater->calculateTotals($invoice);

        self::assertEquals(BigDecimal::of(25500), $invoice->getTotal());
        self::assertEquals(BigDecimal::of(25500), $invoice->getBalance());
        self::assertEquals(BigDecimal::of(30000), $invoice->getBaseTotal());
    }

    /**
     * @throws MathException
     * @throws NotSupported
     */
    public function testUpdateWithMonetaryDiscount(): void
    {
        $updater = new TotalCalculator($this->em->getRepository(Payment::class), new Calculator());

        $invoice = new Invoice();
        $invoice->setClient(ClientFactory::createOne()->_real());
        $item = new Line();
        $item->setQty(2)
            ->setPrice(15000);
        $invoice->addLine($item);
        $discount = new Discount();
        $discount->setType(Discount::TYPE_MONEY);
        $discount->setValue(80);
        $invoice->setDiscount($discount);

        $updater->calculateTotals($invoice);

        self::assertEquals(BigDecimal::of(29920), $invoice->getTotal());
        self::assertEquals(BigDecimal::of(29920), $invoice->getBalance());
        self::assertEquals(BigDecimal::of(30000), $invoice->getBaseTotal());
    }

    /**
     * @throws MathException
     * @throws NotSupported
     */
    public function testUpdateWithTaxIncl(): void
    {
        $updater = new TotalCalculator($this->em->getRepository(Payment::class), new Calculator());

        $tax = new Tax();
        $tax->setType(Tax::TYPE_INCLUSIVE)
            ->setRate(20);

        $invoice = new Invoice();
        $invoice->setClient(ClientFactory::createOne(['currencyCode' => 'USD'])->_real());
        $item = new Line();
        $item->setQty(2)
            ->setPrice(15000)
            ->setTax($tax);

        $invoice->addLine($item);

        $updater->calculateTotals($invoice);

        self::assertEquals(BigDecimal::of(30000), $invoice->getTotal());
        self::assertEquals(BigDecimal::of(30000), $invoice->getBalance());
        self::assertEquals(BigDecimal::of('25000.00'), $invoice->getBaseTotal());
        self::assertEquals(BigDecimal::of('5000.00'), $invoice->getTax());
    }

    /**
     * @throws MathException
     * @throws NotSupported
     */
    public function testUpdateWithTaxExcl(): void
    {
        $updater = new TotalCalculator($this->em->getRepository(Payment::class), new Calculator());

        $tax = new Tax();
        $tax->setType(Tax::TYPE_EXCLUSIVE)
            ->setRate(20);

        $invoice = new Invoice();
        $invoice->setClient(ClientFactory::createOne(['currencyCode' => 'USD'])->_real());
        $item = new Line();
        $item->setQty(2)
            ->setPrice(15000)
            ->setTax($tax);

        $invoice->addLine($item);

        $updater->calculateTotals($invoice);

        self::assertEquals(BigDecimal::of('36000'), $invoice->getTotal());
        self::assertEquals(BigDecimal::of('36000'), $invoice->getBalance());
        self::assertEquals(BigDecimal::of(30000), $invoice->getBaseTotal());
        self::assertEquals(BigDecimal::of('6000'), $invoice->getTax());
    }

    /**
     * @throws MathException
     * @throws NotSupported
     */
    public function testUpdateWithTaxInclAndPercentageDiscount(): void
    {
        $updater = new TotalCalculator($this->em->getRepository(Payment::class), new Calculator());

        $tax = new Tax();
        $tax->setType(Tax::TYPE_INCLUSIVE)
            ->setRate(20);

        $invoice = new Invoice();
        $invoice->setClient(ClientFactory::createOne(['currencyCode' => 'USD'])->_real());
        $item = new Line();
        $item->setQty(2)
            ->setPrice(15000)
            ->setTax($tax);
        $invoice->addLine($item);
        $discount = new Discount();
        $discount->setType(Discount::TYPE_PERCENTAGE);
        $discount->setValue(1500);
        $invoice->setDiscount($discount);

        $updater->calculateTotals($invoice);

        self::assertEquals(BigDecimal::of(26250), $invoice->getTotal());
        self::assertEquals(BigDecimal::of(26250), $invoice->getBalance());
        self::assertEquals(BigDecimal::of('25000.00'), $invoice->getBaseTotal());
        self::assertEquals(BigDecimal::of('5000.00'), $invoice->getTax());
    }

    /**
     * @throws MathException
     * @throws NotSupported
     */
    public function testUpdateWithTaxExclAndMonetaryDiscount(): void
    {
        $updater = new TotalCalculator($this->em->getRepository(Payment::class), new Calculator());

        $tax = new Tax();
        $tax->setType(Tax::TYPE_EXCLUSIVE)
            ->setRate(20);

        $invoice = new Invoice();
        $invoice->setClient(ClientFactory::createOne(['currencyCode' => 'USD'])->_real());
        $item = new Line();
        $item->setQty(2)
            ->setPrice(15000)
            ->setTax($tax);
        $invoice->addLine($item);
        $discount = new Discount();
        $discount->setType(Discount::TYPE_MONEY);
        $discount->setValue(80);
        $invoice->setDiscount($discount);

        $updater->calculateTotals($invoice);

        self::assertEquals(BigDecimal::of('35920'), $invoice->getTotal());
        self::assertEquals(BigDecimal::of('35920'), $invoice->getBalance());
        self::assertEquals(BigDecimal::of(30000), $invoice->getBaseTotal());
        self::assertEquals(BigDecimal::of('6000'), $invoice->getTax());
    }

    /**
     * @throws ORMException
     * @throws OptimisticLockException
     * @throws MathException
     * @throws NotSupported
     */
    public function testUpdateTotalsWithPayments(): void
    {
        $invoice = new Invoice();
        $invoice->setClient(ClientFactory::createOne(['currencyCode' => 'USD'])->_real());
        $invoice->setTotal(30000);
        $invoice->setBaseTotal(30000);
        $invoice->setBalance(30000);
        $invoice->setStatus(Graph::STATUS_PENDING);
        $item = new Line();
        $item->setQty(2)
            ->setPrice(15000)
            ->setDescription('foobar');
        $invoice->addLine($item);

        $payment = new Payment();
        $payment->setTotalAmount(1000);
        $payment->setStatus(Status::STATUS_CAPTURED);

        $invoice->addPayment($payment);
        $this->em->persist($invoice);
        $this->em->flush();

        $updater = new TotalCalculator($this->em->getRepository(Payment::class), new Calculator());

        $updater->calculateTotals($invoice);

        self::assertEquals(BigDecimal::of(30000), $invoice->getTotal());
        self::assertEquals(BigDecimal::of(29000), $invoice->getBalance());
        self::assertEquals(BigDecimal::of(30000), $invoice->getBaseTotal());
    }

    /**
     * Test for the rounding issue that causes RoundingNecessaryException
     * with specific price and tax combinations (issue #1824)
     *
     * @throws MathException
     */
    public function testUpdateWithTaxExclRoundingIssue(): void
    {
        $updater = new TotalCalculator($this->em->getRepository(Payment::class), new Calculator());

        $tax = new Tax();
        $tax->setType(Tax::TYPE_EXCLUSIVE)
            ->setRate(21);

        // Test case 1: 3.32 EUR with 21% tax (problematic case from issue)
        $invoice = new Invoice();
        $invoice->setClient(ClientFactory::createOne(['currencyCode' => 'EUR']));
        $item = new Line();
        $item->setQty(1)
            ->setPrice(332) // 3.32 EUR (stored as cents)
            ->setTax($tax);

        $invoice->addLine($item);

        $updater->calculateTotals($invoice);

        // Verify that the calculation completes without RoundingNecessaryException
        self::assertEquals(BigDecimal::of(332), $invoice->getBaseTotal());
        self::assertEquals(BigDecimal::of('70'), $invoice->getTax()); // 3.32 * 0.21 = 0.6972, rounded to 70 cents
        self::assertEquals(BigDecimal::of('402'), $invoice->getTotal()); // 332 + 69.72 = 402 cents

        // Test case 2: 3.33 EUR with 21% tax (another problematic case)
        $invoice2 = new Invoice();
        $invoice2->setClient(ClientFactory::createOne(['currencyCode' => 'EUR']));
        $item2 = new Line();
        $item2->setQty(1)
            ->setPrice(333) // 3.33 EUR (stored as cents)
            ->setTax($tax);

        $invoice2->addLine($item2);

        $updater->calculateTotals($invoice2);

        // Verify that the calculation completes without RoundingNecessaryException
        self::assertEquals(BigDecimal::of(333), $invoice2->getBaseTotal());
        self::assertEquals(BigDecimal::of('70'), $invoice2->getTax()); // 3.33 * 0.21 = 0.6993, rounded to 70 cents
        self::assertEquals(BigDecimal::of('403'), $invoice2->getTotal()); // 333 + 69.93 = 403 cents
    }
}
