<?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 — same shape as Sync_Orders_Fake_Order (Sync_Orders_Watermark_Test.php),
 * plus get_billing_email() which createOrder() also reads.
 */
class Create_Order_Fake_Order extends \WC_Order { // phpcs:ignore
	private $id;
	private $meta;

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

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

	public function get_billing_email() {
		return 'nonmember@example.com';
	}

	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: when checkUserExist() returns false (order from an e-mail with no
 * Simply.IN account), createOrder() drops the item from the queue (returns true) WITHOUT ever
 * setting SimplyInOrderExported — that flag means "confirmed exported to SIN.CORE", which a
 * non-member order will never be. Simplyin_Sync::sync_orders() used SimplyInOrderExported as its
 * only "resolved" signal, so a non-member order was counted as unconfirmed forever: the
 * watermark got pinned at it and every future cron pass re-queued and re-checked it,
 * indefinitely (see Sync_Orders_Watermark_Test for the sync_orders() side of the fix).
 *
 * These tests exercise createOrder() directly and assert it now marks the order with a
 * dedicated "terminal, not applicable" flag distinct from SimplyInOrderExported, so
 * sync_orders() can tell the two apart.
 */
final class Background_Process_Create_Order_NonMember_Test extends TestCase {

	protected function setUp(): void {
		parent::setUp();
		Monkey\setUp();
		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 = '' ) {}
					};
				}

				public function get_woocommerce_logger( $context = null ) {
					return new class() {
						public function log_warning( $message ) {}
					};
				}
			}
		);
	}

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

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

		$mock->method( 'checkUserExist' )->willReturn( $result );

		return $mock;
	}

	private function invoke_create_order( SimplyIn_Background_Process $process, int $order_id, int $attempt = 1 ) {
		$method = new ReflectionMethod( $process, 'createOrder' );
		$method->setAccessible( true );

		return $method->invoke( $process, $order_id, $attempt );
	}

	public function test_non_member_order_is_marked_not_applicable(): void {
		$order = new Create_Order_Fake_Order( 42, [ 'SimplyInOrderInQueue' => 1 ] );
		Functions\when( 'wc_get_order' )->justReturn( $order );

		$mock   = $this->mock_process_with_check_user_exist_result( false );
		$result = $this->invoke_create_order( $mock, 42 );

		$this->assertTrue( $result, 'A non-member order must still drop from this queue pass — nothing to retry synchronously.' );
		$this->assertSame(
			1,
			$order->get_meta_value( 'SimplyInOrderNotApplicable' ),
			'createOrder() must mark a non-member order as not-applicable so sync_orders() stops treating it as unconfirmed.'
		);
		$this->assertNotEquals(
			1,
			$order->get_meta_value( 'SimplyInOrderExported' ),
			'A non-member order was never exported — it must not be marked as if it were.'
		);
		$this->assertNull(
			$order->get_meta_value( 'SimplyInOrderInQueue' ),
			'A confirmed terminal outcome must clear SimplyInOrderInQueue.'
		);
	}

	/**
	 * Regression: checkUserExist() returns false both when it CONFIRMS no account exists and
	 * when the check itself fails to get a confirmed answer (network error, malformed response —
	 * see checkUserExist()'s docblock). Before distinguishing the two (null vs false), a transient
	 * API blip on this specific check was permanently mistaken for "confirmed non-member" and the
	 * order was silently dropped from all future sync attempts forever.
	 */
	public function test_transient_check_user_exist_failure_is_retryable_not_terminal(): void {
		$order = new Create_Order_Fake_Order( 42, [ 'SimplyInOrderInQueue' => 1 ] );
		Functions\when( 'wc_get_order' )->justReturn( $order );

		$mock   = $this->mock_process_with_check_user_exist_result( null );
		$result = $this->invoke_create_order( $mock, 42 );

		$this->assertFalse(
			$result,
			'An unconfirmed checkUserExist() check must be retried, like any other unconfirmed sync — not dropped.'
		);
		$this->assertNull(
			$order->get_meta_value( 'SimplyInOrderNotApplicable' ),
			'A transient check failure is not a confirmed non-member — must not be marked not-applicable.'
		);
		$this->assertSame(
			1,
			$order->get_meta_value( 'SimplyInOrderInQueue' ),
			'A retryable outcome must keep SimplyInOrderInQueue set so sync_orders() does not push a duplicate while this is still being retried.'
		);
	}

	/**
	 * Regression: checkUserExist()'s "did the check itself fail?" guard only verified that the
	 * `ok` property was SET, not that it was truthy — an explicit API error response shaped like
	 * `{"ok": false}` passed that guard and fell through to be read as a confirmed "no matching
	 * account" (false), instead of the unconfirmed/retryable null it actually is. Every other
	 * `->ok` check in this codebase (Widget_Controller.php, Plugin_Rsa_Registrar.php) treats `ok`
	 * as a truthy success flag, not merely a present key.
	 */
	public function test_check_user_exist_returns_null_on_explicit_ok_false_response(): void {
		$process = $this->getMockBuilder( SimplyIn_Background_Process::class )
			->disableOriginalConstructor()
			->onlyMethods( [] )
			->getMock();

		$api = new class() {
			public function sendRequest( $endpoint, $method, $body ) {
				return (object) [ 'ok' => false ];
			}
		};
		$process->setSimplyInApi( $api );

		$method = new ReflectionMethod( $process, 'checkUserExist' );
		$method->setAccessible( true );

		$this->assertNull(
			$method->invoke( $process, 'someone@example.com' ),
			'An explicit ok=false API response is an unconfirmed/failed check, not a confirmed non-member — must return null, not false.'
		);
	}

	public function test_already_exported_order_is_not_touched(): void {
		$order = new Create_Order_Fake_Order( 42, [ 'SimplyInOrderExported' => 1 ] );
		Functions\when( 'wc_get_order' )->justReturn( $order );

		$mock   = $this->mock_process_with_check_user_exist_result( false );
		$result = $this->invoke_create_order( $mock, 42 );

		$this->assertTrue( $result );
		$this->assertNull(
			$order->get_meta_value( 'SimplyInOrderNotApplicable' ),
			'An already-exported order returns early and must not be touched by the non-member branch.'
		);
	}
}
