<?php

namespace Unit\Accounting\Export\Exporter;

use Myvetshop\Module\Clinique\Accounting\Export\Exporter\DocumentExporterInterface;
use Myvetshop\Module\Clinique\Accounting\Export\Model\ExportLine;
use Myvetshop\Module\Clinique\Accounting\Export\Repository\CountryRepository;
use Myvetshop\Module\Clinique\Accounting\Export\Repository\InvoiceAddressRepository;
use Myvetshop\Module\Clinique\Accounting\Export\Repository\OrderDetailRepository;
use Myvetshop\Module\Clinique\Accounting\Export\Repository\OrderDetailTaxRepository;
use Myvetshop\Module\Clinique\Accounting\Export\Repository\OrderInvoiceTaxRepository;
use Myvetshop\Module\Clinique\Accounting\Export\Repository\OrderPaymentRepository;
use Myvetshop\Module\Clinique\Accounting\Export\Repository\OrderSlipDetailRepository;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

/**
 * @template DocType of \OrderInvoice|\OrderSlip
 */
abstract class AbstractDocumentExporterTest extends TestCase
{
    /**
     * @var CountryRepository&MockObject
     */
    protected CountryRepository $countryRepository;

    /**
     * @var InvoiceAddressRepository&MockObject
     */
    protected InvoiceAddressRepository $invoiceAddressRepository;

    /**
     * @var OrderDetailRepository&MockObject
     */
    protected OrderDetailRepository $orderDetailRepository;

    /**
     * @var OrderDetailTaxRepository&MockObject
     */
    protected OrderDetailTaxRepository $orderDetailTaxRepository;

    /**
     * @var OrderInvoiceTaxRepository&MockObject
     */
    protected OrderInvoiceTaxRepository $orderInvoiceTaxRepository;

    /**
     * @var OrderPaymentRepository&MockObject
     */
    protected OrderPaymentRepository $orderPaymentRepository;

    /**
     * @var OrderSlipDetailRepository&MockObject
     */
    protected OrderSlipDetailRepository $orderSlipDetailRepository;

    /**
     * @var DocumentExporterInterface<DocType>
     */
    protected DocumentExporterInterface $exporter;

    protected int $orderDetailIdBase;

    protected function setUp(): void
    {
        $this->countryRepository = $this->getMockBuilder(CountryRepository::class)
            ->disableOriginalConstructor()
            ->getMock();

        $this->invoiceAddressRepository = $this->getMockBuilder(InvoiceAddressRepository::class)
            ->disableOriginalConstructor()
            ->getMock();

        $this->orderDetailRepository = $this->getMockBuilder(OrderDetailRepository::class)
            ->disableOriginalConstructor()
            ->getMock();

        $this->orderDetailTaxRepository = $this->getMockBuilder(OrderDetailTaxRepository::class)
            ->disableOriginalConstructor()
            ->getMock();

        $this->orderInvoiceTaxRepository = $this->getMockBuilder(OrderInvoiceTaxRepository::class)
            ->disableOriginalConstructor()
            ->getMock();

        $this->orderPaymentRepository = $this->getMockBuilder(OrderPaymentRepository::class)
            ->disableOriginalConstructor()
            ->getMock();

        $this->orderSlipDetailRepository = $this->getMockBuilder(OrderSlipDetailRepository::class)
           ->disableOriginalConstructor()
           ->getMock();

        // Fixtures
        $invoiceAddress = $this->getMockBuilder(\Address::class)
            ->disableOriginalConstructor()
            ->getMock();
        $invoiceAddress->id_country = 8;

        $this->invoiceAddressRepository
            ->method('getByOrder')
            ->willReturn($invoiceAddress);

        $this->orderDetailIdBase = \rand(100, 300);
    }

    /**
     * @param array{
     *     orderPayments: list<\OrderPayment>,
     *     orderDetails: list<\OrderDetail>,
     *     orderDetailTaxes: list<array{id_order_detail: int, rate: float, amount: float}>,
     *     orderShipping?: array{total_tax_excl: float, total_tax_incl: float, vat_rate: numeric-string},
     *     orderDiscount?: array{total_tax_excl: float, total_tax_incl: float, vat_rate: numeric-string},
     *     orderInvoice?: \OrderInvoice,
     *     orderInvoiceTaxes?: array<int, array{id_order_invoice: int, type: string, rate: float, amount: float}>,
     *     orderSlip?: \OrderSlip,
     *     order?: array{invoice_number: int},
     *     orderSlipDetails?: list<array{product_quantity: int, amount_tax_excl: float, amount_tax_incl: float}>,
     * } $fixture
     *
     * @return array{document: DocType, order: \Order}
     */
    protected function loadFixtures(array $fixture): array
    {
        $order = $this->getMockBuilder(\Order::class)
            ->disableOriginalConstructor()
            ->getMock();

        $order->id = \rand();
        $order->reference = \bin2hex(\random_bytes(4));

        if (isset($fixture['orderShipping'])) {
            $order->total_shipping_tax_excl = $fixture['orderShipping']['total_tax_excl'];
            $order->total_shipping_tax_incl = $fixture['orderShipping']['total_tax_incl'];
            $order->carrier_tax_rate = \floatval($fixture['orderShipping']['vat_rate']);
        }

        if (isset($fixture['orderDiscount'])) {
            $order->total_discounts_tax_excl = $fixture['orderDiscount']['total_tax_excl'];
            $order->total_discounts_tax_incl = $fixture['orderDiscount']['total_tax_incl'];
        }

        // Prepare fixtures
        foreach ($fixture['orderPayments'] as $orderPayment) {
            $orderPayment->order_reference = $order->reference;
        }

        foreach ($fixture['orderDetails'] as $orderDetail) {
            $orderDetail->id_order = $order->id;
            if (isset($fixture['orderInvoice'])) {
                $orderDetail->id_order_invoice = (int) $fixture['orderInvoice']->id;
            }
        }

        if (isset($fixture['orderInvoice'])) {
            $fixture['orderInvoice']->id_order = $order->id;
            $order->total_discounts_tax_excl = $fixture['orderInvoice']->total_discount_tax_incl;
            $order->total_discounts_tax_incl = $fixture['orderInvoice']->total_discount_tax_incl;
        }

        // Map fixtures to mocks
        $this->orderDetailRepository
            ->method('getByOrder')
            ->with($order)
            ->willReturn($fixture['orderDetails']);

        $this->orderDetailTaxRepository
            ->method('getByOrder')
            ->with($order)
            ->willReturn($fixture['orderDetailTaxes']);

        $this->orderPaymentRepository
            ->method('getByOrder')
            ->with($order)
            ->willReturn($fixture['orderPayments']);

        if (isset($fixture['orderInvoice'])) {
            $this->orderInvoiceTaxRepository
                ->method('getByOrderInvoice')
                ->with($fixture['orderInvoice'])
                ->willReturn($fixture['orderInvoiceTaxes'] ?? []);

            $this->orderPaymentRepository
                ->method('getByOrderInvoice')
                ->with($fixture['orderInvoice'])
                ->willReturn($fixture['orderPayments']);
        }

        if (isset($fixture['orderSlip'])) {
            $this->orderSlipDetailRepository
                ->method('getByOrderSlip')
                ->with($fixture['orderSlip'])
                ->willReturn($fixture['orderSlipDetails'] ?? []);

            if (isset($fixture['order'])) {
                $order->invoice_number = $fixture['order']['invoice_number'];
            }

            $fixture['orderSlip']->id_order = $order->id;
        }

        /** @var DocType|null $document */
        $document = $fixture['orderInvoice'] ?? $fixture['orderSlip'] ?? null;

        if (!$document) {
            throw new \Exception('Fixture must include an invoice or a slip');
        }

        return [
            'document' => $document,
            'order' => $order,
        ];
    }

    /**
     * @param array{total_price_tax_excl: float, total_price_tax_incl: float, vat_rate: numeric-string} $definition
     *
     * @return \OrderDetail
     */
    protected function orderDetailFromDefinition(array $definition): \OrderDetail
    {
        $od = $this->getMockBuilder(\OrderDetail::class)
            ->disableOriginalConstructor()
            ->getMock();

        $od->id = $this->orderDetailIdBase;
        $od->total_price_tax_excl = $definition['total_price_tax_excl'];
        $od->total_price_tax_incl = $definition['total_price_tax_incl'];

        ++$this->orderDetailIdBase;

        return $od;
    }

    /**
     * @param array{total_price_tax_excl: float, total_price_tax_incl: float, vat_amount: float, vat_rate: numeric-string} $definition
     *
     * @return array{id_order_detail: int, rate: float, amount: float}
     */
    protected function orderDetailTaxFromDefinition(\OrderDetail $od, array $definition): array
    {
        return [
            'id_order_detail' => (int) $od->id,
            'rate' => (float) $definition['vat_rate'],
            'amount' => \round($definition['vat_amount'], 2),
        ];
    }

    /**
     * @param array{product_quantity: int, amount_tax_excl: float, amount_tax_incl: float} $definition
     *
     * @return array{id_order_detail: int, product_quantity: int, amount_tax_excl: float, amount_tax_incl: float}
     */
    protected function orderSlipDetailTaxFromDefinition(\OrderDetail $od, array $definition): array
    {
        $ret = $definition;
        $ret['id_order_detail'] = (int) $od->id;

        return $ret;
    }

    /**
     * @param array{
     *     orderPayment: list<array{'amount': float, 'payment_method': string}>,
     *     orderDiscount?: array{total_tax_excl: float, total_tax_incl: float, vat_rate: numeric-string},
     *     orderDetails: list<array{total_price_tax_excl: float, total_price_tax_incl: float, vat_amount: float, vat_rate: numeric-string}>,
     *     orderInvoice?: array{date: string, number: int, paid_tax_excl: float, paid_tax_incl: float, shipping_tax_excl: float, shipping_vat_rat: numeric-string},
     *     orderSlip?: array{date: string, invoice_number: int, number: int, type: int, products_tax_incl: float, shipping_tax_excl: float, shipping_tax_incl: float, shipping_vat_rat: numeric-string},
     *     orderSlipDetails?: list<array{product_quantity: int, amount_tax_excl: float, amount_tax_incl: float}>,
     * } $definition
     *
     * @return array{
     *     country?: string,
     *     orderPayments: list<\OrderPayment>,
     *     orderDetails: list<\OrderDetail>,
     *     orderShipping?: array{total_tax_excl: float, total_tax_incl: float, vat_rate: numeric-string},
     *     orderDiscount?: array{total_tax_excl: float, total_tax_incl: float, vat_rate: numeric-string},
     *     orderDetailTaxes: list<array{id_order_detail: int, rate: float, amount: float}>,
     *     orderInvoice?: \OrderInvoice,
     *     orderInvoiceTaxes?: array<int, array{id_order_invoice: int, type: string, rate: float, amount: float}>,
     *     orderSlip?: \OrderSlip,
     *     order?: array{invoice_number: int},
     *     orderSlipDetails?: list<array{product_quantity: int, amount_tax_excl: float, amount_tax_incl: float}>,
     * }
     */
    protected function fixtureFromDefinition(array $definition): array
    {
        $country = $this->getMockBuilder(\Country::class)
            ->disableOriginalConstructor()
            ->getMock();
        $country->iso_code = $definition['country'] ?? 'FR';

        $this->countryRepository
            ->method('getById')
            ->with(8)
            ->willReturn($country);

        $orderPayments = [];
        foreach ($definition['orderPayment'] as $orderPaymentDefinition) {
            $orderPayment = $this->getMockBuilder(\OrderPayment::class)
                ->disableOriginalConstructor()
                ->getMock();
            $orderPayment->amount = $orderPaymentDefinition['amount'];
            $orderPayment->payment_method = $orderPaymentDefinition['payment_method'];

            $orderPayments[] = $orderPayment;
        }

        $orderDetails = [];
        $orderDetailTaxes = [];
        $orderSlipDetails = [];
        foreach ($definition['orderDetails'] as $i => $orderDetail) {
            $od = $this->orderDetailFromDefinition($orderDetail);
            $odTax = $this->orderDetailTaxFromDefinition($od, $orderDetail);

            $orderDetails[] = $od;
            $orderDetailTaxes[(int) $od->id] = $odTax;

            if (isset($definition['orderSlipDetails'][$i])) {
                $orderSlipDetails[] = $this->orderSlipDetailTaxFromDefinition($od, $definition['orderSlipDetails'][$i]);
            }
        }

        $ret = [
            'orderPayments' => $orderPayments,
            'orderDetails' => $orderDetails,
            'orderDetailTaxes' => $orderDetailTaxes,
        ];

        if (isset($definition['orderShipping'])) {
            $ret['orderShipping'] = $definition['orderShipping'];
        }

        if (isset($definition['orderDiscount'])) {
            $ret['orderDiscount'] = $definition['orderDiscount'];
        }

        if (isset($definition['orderSlipDetails'])) {
            $ret['orderSlipDetails'] = $orderSlipDetails;
        }

        if (isset($definition['orderInvoice'])) {
            $orderInvoice = $this->getMockBuilder(\OrderInvoice::class)
                ->disableOriginalConstructor()
                ->getMock();

            $orderInvoice->id = \rand(500, 600);
            $orderInvoice->number = $definition['orderInvoice']['number'];
            $orderInvoice->total_paid_tax_excl = $definition['orderInvoice']['paid_tax_excl'];
            $orderInvoice->total_paid_tax_incl = $definition['orderInvoice']['paid_tax_incl'];
            $orderInvoice->total_shipping_tax_excl = $definition['orderInvoice']['shipping_tax_excl'];
            $orderInvoice->total_shipping_tax_incl = $definition['orderInvoice']['shipping_tax_excl']
                * (1 + \floatval($definition['orderInvoice']['shipping_vat_rat']) / 100);
            // @phpstan-ignore-next-line
            $orderInvoice->date_add = $definition['orderInvoice']['date'];

            if (isset($definition['orderDiscount'])) {
                $orderInvoice->total_discount_tax_excl = $definition['orderDiscount']['total_tax_excl'];
                $orderInvoice->total_discount_tax_incl = $definition['orderDiscount']['total_tax_incl'];
            }

            $ret['orderInvoice'] = $orderInvoice;

            if ($orderInvoice->total_shipping_tax_excl) {
                $ret['orderShipping'] = [
                    'total_tax_excl' => $orderInvoice->total_shipping_tax_excl,
                    'total_tax_incl' => $orderInvoice->total_shipping_tax_incl,
                    'vat_rate' => \floatval($definition['orderInvoice']['shipping_vat_rat']),
                ];

                $ret['orderInvoiceTaxes'] = [
                    0 => [
                        'id_order_invoice' => $orderInvoice->id,
                        'type' => 'shipping',
                        'rate' => (float) $definition['orderInvoice']['shipping_vat_rat'],
                        'amount' => \round(
                            $orderInvoice->total_shipping_tax_incl - $orderInvoice->total_shipping_tax_excl,
                            2
                        ),
                    ],
                ];
            } else {
                $ret['orderInvoiceTaxes'] = [];
            }
        }

        if (isset($definition['orderSlip'])) {
            $orderSlip = $this->getMockBuilder(\OrderSlip::class)
                ->disableOriginalConstructor()
                ->getMock();

            $orderSlip->id = $definition['orderSlip']['number'];
            $orderSlip->total_products_tax_incl = $definition['orderSlip']['products_tax_incl'];
            $orderSlip->total_shipping_tax_excl = $definition['orderSlip']['shipping_tax_excl'];
            $orderSlip->total_shipping_tax_incl = $definition['orderSlip']['shipping_tax_incl'];
            $orderSlip->order_slip_type = $definition['orderSlip']['type'];
            $orderSlip->date_add = $definition['orderSlip']['date'];

            $ret['orderSlip'] = $orderSlip;
            $ret['order'] = ['invoice_number' => $definition['orderSlip']['invoice_number']];
        }

        return $ret;
    }

    /**
     * @param array{date: string, account: string, entitled: string, credit: float, debit: float} $definition
     */
    public static function assertExportLineEquals(array $definition, ExportLine $line): void
    {
        $values = [
            'date' => $line->getDate()->format('d/m/Y'),
            'account' => $line->getAccount(),
            'entitled' => $line->getEntitled(),
            'credit' => $line->getCredit(),
            'debit' => $line->getDebit(),
        ];

        self::assertEquals($definition, $values);
        self::assertEquals('VE', $line->getJournal());
    }

    /**
     * @param array{
     *     orderPayment: list<array{'amount': float, 'payment_method': string}>,
     *     orderDiscount?: array{total_tax_excl: float, total_tax_incl: float, vat_rate: numeric-string},
     *     orderDetails: list<array{total_price_tax_excl: float, total_price_tax_incl: float, vat_amount: float, vat_rate: numeric-string}>,
     *     orderInvoice?: array{date: string, number: int, paid_tax_excl: float, paid_tax_incl: float, shipping_tax_excl: float, shipping_vat_rat: numeric-string},
     *     orderSlip?: array{date: string, invoice_number: int, number: int, type: int, products_tax_incl: float, shipping_tax_excl: float, shipping_tax_incl: float, shipping_vat_rat: numeric-string},
     *     orderSlipDetails?: list<array{product_quantity: int, float, amount_tax_excl: float, amount_tax_incl: float}>,
     * } $fixtures
     * @param list<array{date: string, account: string, entitled: string, credit: float, debit: float}> $results
     *
     * @dataProvider fixtureProvider
     */
    public function testDocuments(array $fixtures, array $results): void
    {
        // Load fixtures before test
        $testData = $this->loadFixtures(
            $this->fixtureFromDefinition($fixtures)
        );

        $lines = $this->exporter->export($testData['document'], $testData['order']);

        self::assertCount(\count($results), $lines);

        foreach ($lines as $i => $line) {
            self::assertExportLineEquals($results[$i], $line);
        }

        // Check totals
        $total = \round(
            \array_reduce(
                $lines,
                function (float $carry, ExportLine $line): float {
                    return $carry - $line->getDebit() + $line->getCredit();
                },
                0.0
            ),
            2);
        self::assertEquals(0, $total);
    }

    /**
     * @return list<array{
     *   0: array{
     *     country?: string,
     *     orderPayment: list<array{amount: float, payment_method: string}>,
     *     orderDiscount?: array{total_tax_excl: float, total_tax_incl: float, vat_rate: numeric-string},
     *     orderDetails: list<array{total_price_tax_excl: float, vat_amount: float, vat_rate: numeric-string}>,
     *     orderInvoice?: array{date: string, number: int, paid_tax_excl: float, paid_tax_incl: float, shipping_tax_excl: float, shipping_vat_rat: numeric-string},
     *     orderSlip?: array{date: string, invoice_number: int, number: int, type: int, products_tax_incl: float, shipping_tax_excl: float, shipping_tax_incl: float, shipping_vat_rat: numeric-string},
     *     orderSlipDetails?: list<array{product_quantity: int, amount_tax_excl: float, amount_tax_incl: float}>,
     *   },
     *   1: list<array{date: string, account: string, entitled: string, credit: float, debit: float}>
     * }>
     */
    abstract public function fixtureProvider(): array;
}
