<?php

declare( strict_types=1 );

namespace Simplyin\Simplyin_W_Plugin\Tests\Unit;

use Brain\Monkey;
use Brain\Monkey\Functions;
use PHPUnit\Framework\TestCase;
use Simplyin\Simplyin_W_Plugin\Dto\Order\Order_DTO_Factory;

/**
 * Minimal runtime stub of a WooCommerce line item exposing only the pre-discount
 * subtotal tax that compute_order_financials() reads. WooCommerce is not loaded in the
 * unit-test runtime (see tests/bootstrap.php).
 */
class Fake_Line_Item { // phpcs:ignore
	/** @var float */
	private $subtotal_tax;

	public function __construct( float $subtotal_tax ) {
		$this->subtotal_tax = $subtotal_tax;
	}

	public function get_subtotal_tax(): float {
		return $this->subtotal_tax;
	}
}

/**
 * Minimal fee line-item stub (WC_Order_Item_Fee-like): gross = get_total() + get_total_tax().
 */
class Fake_Fee_Item { // phpcs:ignore
	private $total;
	private $total_tax;

	public function __construct( float $total, float $total_tax ) {
		$this->total     = $total;
		$this->total_tax = $total_tax;
	}

	public function get_total(): float {
		return $this->total;
	}

	public function get_total_tax(): float {
		return $this->total_tax;
	}
}

// Global-namespace stub so the `WC_Order` type-hint on compute_order_financials() resolves.
if ( ! class_exists( 'WC_Order' ) ) {
	// phpcs:disable
	class WC_Order_Financials_Stub_Base {}
	// phpcs:enable
	\class_alias( __NAMESPACE__ . '\\WC_Order_Financials_Stub_Base', 'WC_Order' );
}

/**
 * Reconciliation contract for the gross (B2C) order financial breakdown:
 * subtotal_amount + discount_amount + shipping_amount == total_amount across the required
 * scenarios (no discount / coupon / free shipping / mixed tax / coupon+shipping).
 */
final class Order_Financials_Test extends TestCase {

	protected function setUp(): void {
		parent::setUp();
		Monkey\setUp();
		// wc_format_decimal returns a fixed-precision string; round_price casts it back to float.
		Functions\when( 'wc_format_decimal' )->alias(
			static function ( $value, $dp = 2 ) {
				return number_format( round( (float) $value, (int) $dp ), (int) $dp, '.', '' );
			}
		);
	}

	protected function tearDown(): void {
		Monkey\tearDown();
		parent::tearDown();
	}

	/**
	 * @param array<int, float> $subtotal_taxes Per-line pre-discount tax.
	 * @return \WC_Order
	 */
	private function makeOrder(
		array $subtotal_taxes,
		float $subtotal,
		float $shipping_total,
		float $shipping_tax,
		float $discount_gross,
		float $total_tax,
		float $total,
		array $fees = []
	) {
		$items = array_map(
			static function ( float $tax ) {
				return new Fake_Line_Item( $tax );
			},
			$subtotal_taxes
		);

		// Anonymous class extending the aliased WC_Order stub; provides the getters the
		// financial computation depends on.
		return new class( $items, $fees, $subtotal, $shipping_total, $shipping_tax, $discount_gross, $total_tax, $total ) extends \WC_Order {
			/** @var array<int, Fake_Line_Item> */
			private $items;
			/** @var array<int, Fake_Fee_Item> */
			private $fees;
			private $subtotal;
			private $shipping_total;
			private $shipping_tax;
			private $discount_gross;
			private $total_tax;
			private $total;

			public function __construct( $items, $fees, $subtotal, $shipping_total, $shipping_tax, $discount_gross, $total_tax, $total ) {
				$this->items          = $items;
				$this->fees           = $fees;
				$this->subtotal       = $subtotal;
				$this->shipping_total = $shipping_total;
				$this->shipping_tax   = $shipping_tax;
				$this->discount_gross = $discount_gross;
				$this->total_tax      = $total_tax;
				$this->total          = $total;
			}

			public function get_items( $types = 'line_item' ) { return 'fee' === $types ? $this->fees : $this->items; }
			public function get_subtotal() { return $this->subtotal; }
			public function get_shipping_total() { return $this->shipping_total; }
			public function get_shipping_tax() { return $this->shipping_tax; }
			public function get_total_discount( $ex_tax = true ) { return $ex_tax ? 0.0 : $this->discount_gross; }
			public function get_total_tax() { return $this->total_tax; }
			public function get_total() { return $this->total; }
		};
	}

	private function assertReconciles( array $f ): void {
		$sum = round( $f['subtotal_amount'] + $f['discount_amount'] + $f['shipping_amount'], 2 );
		$this->assertSame( $f['total_amount'], $sum, 'subtotal + discount + shipping must equal total' );
	}

	public function test_no_discount(): void {
		// 1 item net 100 @ 23% tax, no shipping, no discount.
		$order = $this->makeOrder( [ 23.0 ], 100.0, 0.0, 0.0, 0.0, 23.0, 123.0 );
		$f     = Order_DTO_Factory::compute_order_financials( $order );

		$this->assertSame( 123.0, $f['subtotal_amount'] );
		$this->assertSame( 0.0, $f['discount_amount'] );
		$this->assertSame( 0.0, $f['shipping_amount'] );
		$this->assertSame( 123.0, $f['total_amount'] );
		$this->assertReconciles( $f );
	}

	public function test_coupon(): void {
		// net 100 (@23%), 10% coupon -> item total 90 tax 20.7; gross discount 12.30.
		$order = $this->makeOrder( [ 23.0 ], 100.0, 0.0, 0.0, 12.30, 20.70, 110.70 );
		$f     = Order_DTO_Factory::compute_order_financials( $order );

		$this->assertSame( 123.0, $f['subtotal_amount'] );
		$this->assertSame( -12.30, $f['discount_amount'] );
		$this->assertSame( 110.70, $f['total_amount'] );
		$this->assertReconciles( $f );
	}

	public function test_free_shipping(): void {
		// net 200 @23% tax 46, free shipping, no discount.
		$order = $this->makeOrder( [ 46.0 ], 200.0, 0.0, 0.0, 0.0, 46.0, 246.0 );
		$f     = Order_DTO_Factory::compute_order_financials( $order );

		$this->assertSame( 246.0, $f['subtotal_amount'] );
		$this->assertSame( 0.0, $f['shipping_amount'] );
		$this->assertSame( 246.0, $f['total_amount'] );
		$this->assertReconciles( $f );
	}

	public function test_mixed_tax(): void {
		// item1 net 100 @23% (tax 23), item2 net 50 @10% (tax 5), no discount, no shipping.
		$order = $this->makeOrder( [ 23.0, 5.0 ], 150.0, 0.0, 0.0, 0.0, 28.0, 178.0 );
		$f     = Order_DTO_Factory::compute_order_financials( $order );

		$this->assertSame( 178.0, $f['subtotal_amount'] );
		$this->assertSame( 178.0, $f['total_amount'] );
		$this->assertReconciles( $f );
	}

	public function test_fee_line_item(): void {
		// net 100 @23% (subtotal gross 123), no discount, no shipping, plus a COD fee 10 + 2.30 tax.
		// total = 123 + 12.30 = 135.30; total_tax = 23 + 2.30 = 25.30.
		$order = $this->makeOrder( [ 23.0 ], 100.0, 0.0, 0.0, 0.0, 25.30, 135.30, [ new Fake_Fee_Item( 10.0, 2.30 ) ] );
		$f     = Order_DTO_Factory::compute_order_financials( $order );

		// Fee gross folded into subtotal so the reconciliation stays exact.
		$this->assertSame( 135.30, $f['subtotal_amount'] );
		$this->assertSame( 135.30, $f['total_amount'] );
		$this->assertReconciles( $f );
	}

	public function test_coupon_plus_taxed_shipping(): void {
		// net 100 @23%, 10% coupon (gross disc 12.30), shipping net 10 tax 2.30.
		// total = 90 + 20.70 + 10 + 2.30 = 123.00; total_tax = 20.70 + 2.30 = 23.00.
		$order = $this->makeOrder( [ 23.0 ], 100.0, 10.0, 2.30, 12.30, 23.0, 123.0 );
		$f     = Order_DTO_Factory::compute_order_financials( $order );

		$this->assertSame( 123.0, $f['subtotal_amount'] );
		$this->assertSame( -12.30, $f['discount_amount'] );
		$this->assertSame( 12.30, $f['shipping_amount'] );
		$this->assertSame( 123.0, $f['total_amount'] );
		$this->assertReconciles( $f );
	}
}
