<?php

declare( strict_types=1 );

namespace Simplyin\Simplyin_W_Plugin\Tests\Unit;

use Brain\Monkey;
use Brain\Monkey\Functions;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
use ReflectionMethod;
use ReflectionProperty;
use Simplyin\Simplyin_W_Plugin\Sync\Simplyin_Sync;

/**
 * Minimal wc_get_order()-returned fake, extending the global WC_Order stub (tests/bootstrap.php)
 * so the loose `$order` usage in Simplyin_Sync::sync_orders() works without WooCommerce loaded.
 */
class Sync_Orders_Fake_Order extends \WC_Order { // phpcs:ignore
	private $id;
	private $date_created;
	private $meta;

	public function __construct( int $id, ?DateTimeImmutable $date_created, array $meta = [] ) {
		$this->id           = $id;
		$this->date_created = $date_created;
		$this->meta         = $meta;
	}

	public function get_id() {
		return $this->id;
	}

	public function get_meta( $key, $single = false ) {
		return $this->meta[ $key ] ?? '';
	}

	public function update_meta_data( $key, $value ) {
		$this->meta[ $key ] = $value;
	}

	public function delete_meta_data( $key ) {
		unset( $this->meta[ $key ] );
	}

	public function save_meta_data() {}

	public function get_date_created() {
		return $this->date_created;
	}
}

/**
 * Global-namespace stub for the WC_Order_Query used by Simplyin_Sync::sync_orders() (WooCommerce
 * is not loaded in the unit-test runtime). Test-controlled via the static queue below instead of
 * reading real args, since sync_orders() always requests 'return' => 'ids'.
 */
class Sync_Orders_Fake_Order_Query_Impl { // phpcs:ignore
	/** @var int[] */
	public static array $next_result = [];

	public function __construct( array $args = [] ) {}

	public function get_orders() {
		$result             = self::$next_result;
		self::$next_result = [];

		return $result;
	}
}

if ( ! class_exists( 'WC_Order_Query' ) ) {
	class_alias( __NAMESPACE__ . '\\Sync_Orders_Fake_Order_Query_Impl', 'WC_Order_Query' );
}

/**
 * Fake replacement for SimplyIn_Background_Process (protected `process_all` property on
 * Simplyin_Sync) — records what gets queued without touching the real WP cron/queue machinery.
 */
class Sync_Orders_Fake_Queue { // phpcs:ignore
	public array $queued = [];

	public function push_to_queue( $data ) {
		$this->queued[] = $data;

		return $this;
	}

	public function save() {
		return $this;
	}

	public function dispatch() {}
}

/**
 * Regression: sync_orders() used to unconditionally advance SimplyInLastSyncTimestamp
 * to time() after every run, regardless of whether every order in the window was actually
 * confirmed synced (SimplyInOrderExported=1). Since push_to_queue() only hands the order to the
 * async background process — it does not confirm success synchronously — advancing the watermark
 * past a not-yet-confirmed order meant that if the background process later failed on it, the
 * order fell out of the date_created >= watermark window forever: permanently lost.
 *
 * These tests exercise sync_orders() directly (via reflection) and assert on the
 * SimplyInLastSyncTimestamp value passed to update_option().
 */
final class Sync_Orders_Watermark_Test extends TestCase {

	protected function setUp(): void {
		parent::setUp();
		Monkey\setUp();

		Functions\when( 'add_action' )->justReturn( true );
		Functions\when( 'add_filter' )->justReturn( true );
		Functions\when( 'wp_json_encode' )->alias( 'json_encode' );
		Functions\when( 'simplyin' )->justReturn(
			new class() {
				public function get_simplyin_logger() {
					return new class() {
						public function log_if_wpdebug_on( $message, $context = '' ) {}
					};
				}
			}
		);
	}

	protected function tearDown(): void {
		Monkey\tearDown();
		parent::tearDown();
		Sync_Orders_Fake_Order_Query_Impl::$next_result = [];
	}

	/**
	 * @param Sync_Orders_Fake_Order[] $orders_by_id
	 *
	 * @return array{0: Simplyin_Sync, 1: Sync_Orders_Fake_Queue}
	 */
	private function build_sync( int $last_sync_timestamp, array $orders_by_id ): array {
		Functions\when( 'get_option' )->alias(
			static function ( $name, $default = false ) use ( $last_sync_timestamp ) {
				return 'SimplyInLastSyncTimestamp' === $name ? $last_sync_timestamp : $default;
			}
		);
		Functions\when( 'wc_get_order' )->alias(
			static function ( $order_id ) use ( $orders_by_id ) {
				return $orders_by_id[ $order_id ] ?? false;
			}
		);
		Sync_Orders_Fake_Order_Query_Impl::$next_result = array_keys( $orders_by_id );

		$sync  = new Simplyin_Sync( 'simplyin', '7.2.17' );
		$queue = new Sync_Orders_Fake_Queue();

		$process_all_prop = new ReflectionProperty( Simplyin_Sync::class, 'process_all' );
		$process_all_prop->setAccessible( true );
		$process_all_prop->setValue( $sync, $queue );

		return [ $sync, $queue ];
	}

	private function invoke_sync_orders( Simplyin_Sync $sync ): void {
		$method = new ReflectionMethod( $sync, 'sync_orders' );
		$method->setAccessible( true );
		$method->invoke( $sync );
	}

	public function test_watermark_held_at_earliest_unconfirmed_order_not_advanced_to_now(): void {
		$last_sync = 1000;

		[ $sync, $queue ] = $this->build_sync(
			$last_sync,
			[
				// Already confirmed synced — must be skipped, not counted as unconfirmed.
				10 => new Sync_Orders_Fake_Order( 10, new DateTimeImmutable( '@1500' ), [ 'SimplyInOrderExported' => 1 ] ),
				// Two unconfirmed orders; 2000 is the earlier of the two.
				11 => new Sync_Orders_Fake_Order( 11, new DateTimeImmutable( '@3000' ) ),
				12 => new Sync_Orders_Fake_Order( 12, new DateTimeImmutable( '@2000' ) ),
			]
		);

		$captured = null;
		Functions\when( 'update_option' )->alias(
			static function ( $name, $value ) use ( &$captured ) {
				$captured = [ $name, $value ];

				return true;
			}
		);

		$this->invoke_sync_orders( $sync );

		$this->assertSame( 'SimplyInLastSyncTimestamp', $captured[0] );
		$this->assertSame(
			1999,
			$captured[1],
			'The watermark must be held just before the earliest unconfirmed order (2000 - 1), not advanced to now() — otherwise a later background-process failure on it would permanently drop it from every future sync window.'
		);

		// The already-exported order must not be re-queued; the two unconfirmed ones must be.
		$queued_order_ids = array_column( $queue->queued, 'order_id' );
		$this->assertSame( [ 11, 12 ], $queued_order_ids );
	}

	public function test_watermark_does_not_drift_backwards_on_order_with_null_date_created(): void {
		// Copilot review: WooCommerce can return a null get_date_created(); the fallback to
		// $lastSyncTimestamp must not, by itself, regress the watermark by 1 second on every run
		// forever (unbounded backward drift widening the sync window indefinitely).
		$last_sync = 1000;

		[ $sync ] = $this->build_sync(
			$last_sync,
			[
				11 => new Sync_Orders_Fake_Order( 11, null ),
			]
		);

		$captured = null;
		Functions\when( 'update_option' )->alias(
			static function ( $name, $value ) use ( &$captured ) {
				$captured = [ $name, $value ];

				return true;
			}
		);

		$this->invoke_sync_orders( $sync );

		$this->assertSame(
			[ 'SimplyInLastSyncTimestamp', $last_sync ],
			$captured,
			'A null date_created must not regress the watermark below where this run started.'
		);
	}

	public function test_watermark_advances_to_now_when_nothing_unconfirmed(): void {
		[ $sync, $queue ] = $this->build_sync(
			1000,
			[
				10 => new Sync_Orders_Fake_Order( 10, new DateTimeImmutable( '@1500' ), [ 'SimplyInOrderExported' => 1 ] ),
			]
		);

		$captured = null;
		Functions\when( 'update_option' )->alias(
			static function ( $name, $value ) use ( &$captured ) {
				$captured = [ $name, $value ];

				return true;
			}
		);

		$before = time();
		$this->invoke_sync_orders( $sync );
		$after  = time();

		$this->assertSame( 'SimplyInLastSyncTimestamp', $captured[0] );
		$this->assertGreaterThanOrEqual( $before, $captured[1] );
		$this->assertLessThanOrEqual( $after, $captured[1] );
		$this->assertSame( [], $queue->queued, 'An already-exported order must not be re-queued.' );
	}

	/**
	 * Regression: a non-member order (checkUserExist() came back false in
	 * SimplyIn_Background_Process::createOrder()) will NEVER get SimplyInOrderExported=1 — it is
	 * terminal, not a transient failure. Before this fix, sync_orders() had no way to tell that
	 * apart from a genuinely-unconfirmed order, so it held the watermark back on it FOREVER and
	 * re-queued it on every single cron pass, indefinitely.
	 */
	public function test_not_applicable_order_is_skipped_and_does_not_hold_back_watermark(): void {
		[ $sync, $queue ] = $this->build_sync(
			1000,
			[
				10 => new Sync_Orders_Fake_Order( 10, new DateTimeImmutable( '@1500' ), [ 'SimplyInOrderNotApplicable' => 1 ] ),
			]
		);

		$captured = null;
		Functions\when( 'update_option' )->alias(
			static function ( $name, $value ) use ( &$captured ) {
				$captured = [ $name, $value ];

				return true;
			}
		);

		$before = time();
		$this->invoke_sync_orders( $sync );
		$after  = time();

		$this->assertGreaterThanOrEqual( $before, $captured[1], 'A not-applicable order must not pin the watermark in the past.' );
		$this->assertLessThanOrEqual( $after, $captured[1] );
		$this->assertSame( [], $queue->queued, 'A not-applicable order must not be re-queued.' );
	}

	/**
	 * Regression: sync_orders() had no guard for an order still sitting in the
	 * background-process queue (SimplyInOrderInQueue=1, not yet resolved). If a cron pass ran
	 * again before the async dispatch had processed it, the order was pushed to the queue a
	 * second time. It must still count toward the watermark hold-back (it is genuinely
	 * unconfirmed), just not be queued twice.
	 */
	public function test_in_queue_order_is_not_requeued_but_still_holds_back_watermark(): void {
		[ $sync, $queue ] = $this->build_sync(
			1000,
			[
				11 => new Sync_Orders_Fake_Order( 11, new DateTimeImmutable( '@2000' ), [ 'SimplyInOrderInQueue' => 1 ] ),
			]
		);

		$captured = null;
		Functions\when( 'update_option' )->alias(
			static function ( $name, $value ) use ( &$captured ) {
				$captured = [ $name, $value ];

				return true;
			}
		);

		$this->invoke_sync_orders( $sync );

		$this->assertSame(
			[ 'SimplyInLastSyncTimestamp', 1999 ],
			$captured,
			'An order still in flight is genuinely unconfirmed and must keep holding back the watermark.'
		);
		$this->assertSame( [], $queue->queued, 'An order already in the queue must not be pushed a second time.' );
	}
}
