<?php

declare( strict_types=1 );

namespace Simplyin\Simplyin_W_Plugin\Tests\Unit;

use Brain\Monkey;
use Brain\Monkey\Functions;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase;
use Simplyin\Simplyin_W_Plugin\Lib\Remote\Sin_Core_Api\Plugin_Rsa_Registrar;
use Simplyin\Simplyin_W_Plugin\Lib\Remote\Sin_Core_Api\Plugin_Rsa_Signer;

/**
 * PEM -> base64 DER conversion used to build the completion-route body. The backend's
 * `publicKey` field expects raw base64 DER (not PEM armor) — this is the exact byte
 * transform `register()` applies to the generated key pair before sending it.
 *
 * Also covers response_indicates_already_registered()/was_already_registered(): the flag
 * that lets the admin surface tell a merchant apart from an invalid API key when
 * auto_register() fails because the backend already has a public key on file for this
 * merchant.
 */
final class Plugin_Rsa_Registrar_Test extends TestCase {

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

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

	public function test_pem_to_base64_der_round_trips_through_openssl(): void {
		if ( ! function_exists( 'openssl_pkey_new' ) ) {
			$this->markTestSkipped( 'OpenSSL extension is required to exercise RSA key handling.' );
		}

		$key = openssl_pkey_new(
			[
				'private_key_bits' => 2048,
				'private_key_type' => OPENSSL_KEYTYPE_RSA,
			]
		);
		if ( false === $key ) {
			$this->markTestSkipped( 'openssl_pkey_new() failed on this runtime (config/entropy) — not a conversion regression.' );
		}
		$original_details = openssl_pkey_get_details( $key );
		$this->assertNotFalse( $original_details, 'openssl_pkey_get_details() must succeed on the freshly generated key.' );
		$public_pem = $original_details['key'];

		$result = ( new Plugin_Rsa_Registrar() )->pem_to_base64_der( $public_pem );

		$this->assertNotSame( '', $result );
		$this->assertStringNotContainsString( '-----', $result );

		$der = base64_decode( $result, true );
		$this->assertNotFalse( $der, 'Conversion must yield valid base64.' );

		// openssl_pkey_get_public() requires PEM armor, so re-wrap the recovered DER bytes
		// to prove they parse back into the exact same key — this is the round-trip check.
		$rearmored = "-----BEGIN PUBLIC KEY-----\n" . chunk_split( base64_encode( $der ), 64, "\n" ) . "-----END PUBLIC KEY-----\n";
		$parsed    = openssl_pkey_get_public( $rearmored );
		$this->assertNotFalse( $parsed, 'Recovered DER must parse back into a valid public key.' );

		$parsed_details = openssl_pkey_get_details( $parsed );
		$this->assertNotFalse( $parsed_details, 'openssl_pkey_get_details() must succeed on the re-armored key.' );
		$this->assertSame( $original_details['rsa']['n'], $parsed_details['rsa']['n'] );
		$this->assertSame( $original_details['rsa']['e'], $parsed_details['rsa']['e'] );
	}

	public function test_pem_to_base64_der_returns_empty_for_empty_input(): void {
		$this->assertSame( '', ( new Plugin_Rsa_Registrar() )->pem_to_base64_der( '' ) );
	}

	public function test_response_indicates_already_registered_matches_flat_message(): void {
		$response = json_decode( '{"status":"failed","message":"Merchant api key already exists"}' );

		$this->assertTrue( ( new Plugin_Rsa_Registrar() )->response_indicates_already_registered( $response ) );
	}

	public function test_response_indicates_already_registered_is_case_insensitive(): void {
		$response = json_decode( '{"message":"MERCHANT API KEY ALREADY EXISTS"}' );

		$this->assertTrue( ( new Plugin_Rsa_Registrar() )->response_indicates_already_registered( $response ) );
	}

	public function test_response_indicates_already_registered_matches_wrapped_message(): void {
		$response = json_decode( '{"result":{"data":{"message":"already exists"}}}' );

		$this->assertTrue( ( new Plugin_Rsa_Registrar() )->response_indicates_already_registered( $response ) );
	}

	public function test_response_indicates_already_registered_false_for_unrelated_failure(): void {
		$response = json_decode( '{"status":"failed","message":"Invalid API key"}' );

		$this->assertFalse( ( new Plugin_Rsa_Registrar() )->response_indicates_already_registered( $response ) );
	}

	public function test_response_indicates_already_registered_false_for_null_response(): void {
		$this->assertFalse( ( new Plugin_Rsa_Registrar() )->response_indicates_already_registered( null ) );
	}

	public function test_was_already_registered_reads_persisted_flag(): void {
		Functions\when( 'get_option' )->alias(
			static function ( $name, $default = '' ) {
				return Plugin_Rsa_Registrar::OPTION_ALREADY_REGISTERED === $name ? '1' : $default;
			}
		);

		$this->assertTrue( ( new Plugin_Rsa_Registrar() )->was_already_registered() );
	}

	public function test_was_already_registered_false_when_flag_not_set(): void {
		Functions\when( 'get_option' )->alias(
			static function ( $name, $default = '' ) {
				return $default;
			}
		);

		$this->assertFalse( ( new Plugin_Rsa_Registrar() )->was_already_registered() );
	}

	public function test_revoke_skips_the_call_when_no_key_is_registered(): void {
		Functions\when( 'get_option' )->alias(
			static function ( $name, $default = '' ) {
				return $default;
			}
		);

		$history = [];
		$client  = $this->mock_http_client( [], $history );

		$this->assertFalse( ( new Plugin_Rsa_Registrar( null, $client ) )->revoke( 'https://api.example/api' ) );
		$this->assertCount( 0, $history, 'No HTTP call must be attempted for an unregistered credential.' );
	}

	public function test_revoke_skips_the_call_when_backend_url_is_empty(): void {
		$this->stub_registered_credential();

		$history = [];
		$client  = $this->mock_http_client( [], $history );

		$this->assertFalse( ( new Plugin_Rsa_Registrar( null, $client ) )->revoke( '   ' ) );
		$this->assertCount( 0, $history );
	}

	public function test_revoke_sends_an_rsa_signed_post_and_returns_true_on_ok(): void {
		$this->stub_registered_credential();
		Functions\when( 'home_url' )->justReturn( 'https://shop.example' );
		Functions\when( 'wp_parse_url' )->alias( 'parse_url' );

		$history = [];
		$client  = $this->mock_http_client( [ new Response( 200, [], '{"ok":true}' ) ], $history );

		$result = ( new Plugin_Rsa_Registrar( null, $client ) )->revoke( 'https://api.example/api' );

		$this->assertTrue( $result );
		$this->assertCount( 1, $history );

		$request = $history[0]['request'];
		$this->assertSame( 'POST', $request->getMethod() );
		$this->assertSame(
			'https://api.example/api/plugin-sync/revokePluginPublicKeyRegistration',
			(string) $request->getUri()
		);
		$this->assertSame( 'cred_1', $request->getHeaderLine( Plugin_Rsa_Signer::HEADER_CREDENTIAL_ID ) );
		$this->assertNotSame( '', $request->getHeaderLine( Plugin_Rsa_Signer::HEADER_SIGNATURE ) );
		$this->assertNotSame( '', $request->getHeaderLine( Plugin_Rsa_Signer::HEADER_TIMESTAMP ) );
		$this->assertNotSame( '', $request->getHeaderLine( Plugin_Rsa_Signer::HEADER_NONCE ) );
		$this->assertSame( '', (string) $request->getBody() );
	}

	public function test_revoke_returns_false_when_backend_does_not_confirm(): void {
		$this->stub_registered_credential();
		Functions\when( 'home_url' )->justReturn( 'https://shop.example' );
		Functions\when( 'wp_parse_url' )->alias( 'parse_url' );

		$history = [];
		$client  = $this->mock_http_client( [ new Response( 401, [], '{"ok":false}' ) ], $history );

		$this->assertFalse( ( new Plugin_Rsa_Registrar( null, $client ) )->revoke( 'https://api.example/api' ) );
		$this->assertCount( 1, $history, 'The call is still attempted; only the confirmation is negative.' );
	}

	public function test_revoke_returns_false_instead_of_throwing_on_a_transport_failure(): void {
		$this->stub_registered_credential();
		Functions\when( 'home_url' )->justReturn( 'https://shop.example' );
		Functions\when( 'wp_parse_url' )->alias( 'parse_url' );

		// http_errors => false only suppresses exceptions for non-2xx/3xx HTTP responses; a
		// transport-level failure (host unreachable, timeout, TLS error) still throws even with
		// that option set. revoke()'s own `bool` contract must hold regardless — the caller
		// (uninstall.php) must never need its own exception handling to stay best-effort.
		$stack = HandlerStack::create(
			new MockHandler(
				[
					static function ( Request $request ) {
						throw new ConnectException( 'Could not resolve host', $request );
					},
				]
			)
		);
		$client = new Client( [ 'handler' => $stack ] );

		$this->assertFalse( ( new Plugin_Rsa_Registrar( null, $client ) )->revoke( 'https://api.example/api' ) );
	}

	/**
	 * Stub get_option() so Plugin_Rsa_Signer::is_registered() is true with a fresh, real key
	 * pair — @see Plugin_Rsa_Signer_Test for the same pattern used to exercise build_headers().
	 */
	private function stub_registered_credential(): void {
		if ( ! function_exists( 'openssl_pkey_new' ) ) {
			$this->markTestSkipped( 'OpenSSL extension is required to exercise RSA signing.' );
		}

		$key = openssl_pkey_new(
			[
				'private_key_bits' => 2048,
				'private_key_type' => OPENSSL_KEYTYPE_RSA,
			]
		);
		openssl_pkey_export( $key, $private_pem );

		Functions\when( 'get_option' )->alias(
			static function ( $name, $default = '' ) use ( $private_pem ) {
				switch ( $name ) {
					case Plugin_Rsa_Signer::OPTION_CREDENTIAL_ID:
						return 'cred_1';
					case Plugin_Rsa_Signer::OPTION_PRIVATE_KEY:
						return $private_pem;
					default:
						return $default;
				}
			}
		);
	}

	/**
	 * Guzzle client wired to a MockHandler queue; $history is populated (by reference) with
	 * every request the client sends, in order.
	 *
	 * @param array<int, Response>            $responses Queued responses, consumed in order.
	 * @param array<int, array<string, mixed>> $history   Out param: recorded request/response pairs.
	 */
	private function mock_http_client( array $responses, &$history ): Client {
		$history = [];
		$stack   = HandlerStack::create( new MockHandler( $responses ) );
		$stack->push( Middleware::history( $history ) );

		return new Client( [ 'handler' => $stack ] );
	}
}
