<?php

declare( strict_types=1 );

namespace Simplyin\Simplyin_W_Plugin\Tests\Unit;

use Brain\Monkey;
use Brain\Monkey\Functions;
use PHPUnit\Framework\TestCase;
use ReflectionMethod;
use Simplyin\Simplyin_W_Plugin\Sync\SimplyIn_Background_Process;

/**
 * Minimal wc_get_order()-returned fake for the give-up test below — self-contained in this file
 * (not shared with Background_Process_Create_Order_NonMember_Test.php's Create_Order_Fake_Order)
 * so this test file has no load-order dependency on another test file when run in isolation.
 */
class Give_Up_Fake_Order extends \WC_Order { // phpcs:ignore
	private $meta;

	public function __construct( array $meta = [] ) {
		$this->meta = $meta;
	}

	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_meta_value( string $key ) {
		return $this->meta[ $key ] ?? null;
	}
}

/**
 * Regression: task() used to unconditionally `return false` for every action, so
 * WP_Background_Process removed a 'create_order' item from the queue after a single attempt
 * even when the sync call to SIN.CORE never confirmed success — a transient failure (rate
 * limit, brief backend outage) meant the order was silently dropped and never retried.
 *
 * These tests exercise the queue-retry contract directly: task() must return the (modified)
 * item to keep it queued while createOrder() has not confirmed success, up to
 * MAX_CREATE_ORDER_SYNC_ATTEMPTS, and must return false (drop) once that cap is hit or once
 * createOrder() confirms success. createOrder() itself (the real API/DB pipeline) is mocked
 * out here — it is covered separately by exercising the queue/retry contract in isolation.
 */
final class Background_Process_Create_Order_Retry_Test extends TestCase {

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

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

	/**
	 * @return SimplyIn_Background_Process&\PHPUnit\Framework\MockObject\MockObject
	 */
	private function mock_process_with_create_order_result( bool $result, int $expected_attempt ) {
		$mock = $this->getMockBuilder( SimplyIn_Background_Process::class )
			->disableOriginalConstructor()
			->onlyMethods( [ 'createOrder' ] )
			->getMock();

		$mock->expects( $this->once() )
			->method( 'createOrder' )
			->with( 42, $expected_attempt )
			->willReturn( $result );

		return $mock;
	}

	/**
	 * @return mixed task()'s return value.
	 */
	private function invoke_task( SimplyIn_Background_Process $process, array $item ) {
		$task = new ReflectionMethod( $process, 'task' );
		$task->setAccessible( true );

		return $task->invoke( $process, $item );
	}

	public function test_first_failed_attempt_is_requeued_with_attempt_count(): void {
		$mock = $this->mock_process_with_create_order_result( false, 1 );

		$result = $this->invoke_task( $mock, [ 'action' => 'create_order', 'order_id' => 42 ] );

		$this->assertSame(
			[ 'action' => 'create_order', 'order_id' => 42, 'sync_attempts' => 1 ],
			$result,
			'A failed sync must be requeued (task() must not return false) so WP_Background_Process keeps retrying it.'
		);
	}

	public function test_subsequent_failed_attempt_increments_from_existing_count(): void {
		$mock = $this->mock_process_with_create_order_result( false, 4 );

		$result = $this->invoke_task(
			$mock,
			[ 'action' => 'create_order', 'order_id' => 42, 'sync_attempts' => 3 ]
		);

		$this->assertSame(
			[ 'action' => 'create_order', 'order_id' => 42, 'sync_attempts' => 4 ],
			$result
		);
	}

	public function test_gives_up_and_logs_warning_after_max_attempts(): void {
		Functions\when( 'wp_json_encode' )->alias( 'json_encode' );

		$logged = new class() {
			public array $messages = [];
		};
		Functions\when( 'simplyin' )->justReturn(
			new class( $logged ) {
				private $logged;

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

				public function get_woocommerce_logger( $context = null ) {
					$logged = $this->logged;

					return new class( $logged ) {
						private $logged;

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

						public function log_warning( $message ) {
							$this->logged->messages[] = $message;
						}
					};
				}
			}
		);

		$order = new Give_Up_Fake_Order( [ 'SimplyInOrderInQueue' => 1 ] );
		Functions\when( 'wc_get_order' )->justReturn( $order );

		$max  = SimplyIn_Background_Process::MAX_CREATE_ORDER_SYNC_ATTEMPTS;
		$mock = $this->mock_process_with_create_order_result( false, $max );

		$result = $this->invoke_task(
			$mock,
			[ 'action' => 'create_order', 'order_id' => 42, 'sync_attempts' => $max - 1 ]
		);

		$this->assertFalse( $result, 'Item must be dropped once MAX_CREATE_ORDER_SYNC_ATTEMPTS is reached.' );
		$this->assertCount( 1, $logged->messages, 'Giving up must log exactly one WARNING so operators can see the stuck order.' );
		$this->assertStringContainsString( '42', $logged->messages[0] );
		$this->assertStringContainsString( (string) $max, $logged->messages[0] );
		$this->assertNull(
			$order->get_meta_value( 'SimplyInOrderInQueue' ),
			'Giving up must clear SimplyInOrderInQueue so the next sync_orders() cron pass is free to push a fresh attempt sequence.'
		);
	}

	public function test_confirmed_success_drops_item_regardless_of_prior_attempts(): void {
		$mock = $this->mock_process_with_create_order_result( true, 3 );

		$result = $this->invoke_task(
			$mock,
			[ 'action' => 'create_order', 'order_id' => 42, 'sync_attempts' => 2 ]
		);

		$this->assertFalse( $result );
	}
}
