<?php

namespace MediaWiki\Tests\Api\Validator;

use Generator;
use MediaWiki\Api\ApiBase;
use MediaWiki\Api\ApiMain;
use MediaWiki\Api\ApiMessage;
use MediaWiki\Api\ApiQueryBase;
use MediaWiki\Api\Validator\ApiParamValidatorCallbacks;
use MediaWiki\Request\FauxRequest;
use MediaWiki\Tests\Api\ApiUploadTestCase;
use MediaWiki\Tests\Unit\Permissions\MockAuthorityTrait;
use Psr\Http\Message\UploadedFileInterface;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\TestingAccessWrapper;

/**
 * @covers \MediaWiki\Api\Validator\ApiParamValidatorCallbacks
 * @group API
 * @group medium
 */
class ApiParamValidatorCallbacksTest extends ApiUploadTestCase {
	use MockAuthorityTrait;

	private function getCallbacks( FauxRequest $request ): array {
		$context = $this->apiContext->newTestContext( $request, $this->mockRegisteredUltimateAuthority() );
		$main = new ApiMain( $context );
		return [ new ApiParamValidatorCallbacks( $main ), $main ];
	}

	private function filePath( $fileName ) {
		return __DIR__ . '/../../../data/media/' . $fileName;
	}

	public function testHasParam(): void {
		[ $callbacks, $main ] = $this->getCallbacks( new FauxRequest( [
			'foo' => '1',
			'bar' => '',
		] ) );

		$this->assertTrue( $callbacks->hasParam( 'foo', [] ) );
		$this->assertTrue( $callbacks->hasParam( 'bar', [] ) );
		$this->assertFalse( $callbacks->hasParam( 'baz', [] ) );

		$this->assertSame(
			[ 'foo', 'bar', 'baz' ],
			TestingAccessWrapper::newFromObject( $main )->getParamsUsed()
		);
	}

	/**
	 * @dataProvider provideGetValue
	 * @param string|null $data Value from request
	 * @param mixed $default For getValue()
	 * @param mixed $expect Expected return value
	 * @param bool $normalized Whether handleParamNormalization is called
	 */
	public function testGetValue( ?string $data, $default, $expect, bool $normalized = false ): void {
		[ $callbacks, $main ] = $this->getCallbacks( new FauxRequest( [ 'test' => $data ] ) );

		$module = $this->getMockBuilder( ApiBase::class )
			->setConstructorArgs( [ $main, 'testmodule' ] )
			->onlyMethods( [ 'handleParamNormalization' ] )
			->getMockForAbstractClass();
		$options = [ 'module' => $module ];
		if ( $normalized ) {
			$module->expects( $this->once() )->method( 'handleParamNormalization' )
				->with(
					$this->identicalTo( 'test' ),
					$this->identicalTo( $expect ),
					$this->identicalTo( $data ?? $default )
				);
		} else {
			$module->expects( $this->never() )->method( 'handleParamNormalization' );
		}

		$this->assertSame( $expect, $callbacks->getValue( 'test', $default, $options ) );
		$this->assertSame( [ 'test' ], TestingAccessWrapper::newFromObject( $main )->getParamsUsed() );
	}

	public static function provideGetValue() {
		$obj = (object)[];
		return [
			'Basic test' => [ 'foo', 'bar', 'foo', false ],
			'Default value' => [ null, 1234, 1234, false ],
			'Default value (2)' => [ null, $obj, $obj, false ],
			'No default value' => [ null, null, null, false ],
			'Multi separator' => [ "\x1ffoo\x1fbar", 1234, "\x1ffoo\x1fbar", false ],
			'Normalized' => [ "\x1ffoo\x1fba\u{0301}r", 1234, "\x1ffoo\x1fbár", true ],
		];
	}

	private function setupUploads(): void {
		$fileName = 'TestUploadStash.jpg';
		$mimeType = 'image/jpeg';
		$filePath = $this->filePath( 'yuv420.jpg' );
		$this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath );

		$this->requestDataFiles['file2'] = [
			'name' => '',
			'type' => '',
			'tmp_name' => '',
			'size' => 0,
			'error' => UPLOAD_ERR_NO_FILE,
		];

		$this->requestDataFiles['file3'] = [
			'name' => 'xxx.png',
			'type' => '',
			'tmp_name' => '',
			'size' => 0,
			'error' => UPLOAD_ERR_INI_SIZE,
		];
	}

	public function testHasUpload(): void {
		$this->setupUploads();

		$request = new FauxRequest( [
			'foo' => '1',
			'bar' => '',
		] );
		$request->setUploadData( $this->requestDataFiles );
		[ $callbacks, $main ] = $this->getCallbacks( $request );

		$this->assertFalse( $callbacks->hasUpload( 'foo', [] ) );
		$this->assertFalse( $callbacks->hasUpload( 'bar', [] ) );
		$this->assertFalse( $callbacks->hasUpload( 'baz', [] ) );
		$this->assertTrue( $callbacks->hasUpload( 'file', [] ) );
		$this->assertTrue( $callbacks->hasUpload( 'file2', [] ) );
		$this->assertTrue( $callbacks->hasUpload( 'file3', [] ) );

		$this->assertSame(
			[ 'foo', 'bar', 'baz', 'file', 'file2', 'file3' ],
			TestingAccessWrapper::newFromObject( $main )->getParamsUsed()
		);
	}

	public function testGetUploadedFile(): void {
		$this->setupUploads();

		$request = new FauxRequest( [
			'foo' => '1',
			'bar' => '',
		] );
		$request->setUploadData( $this->requestDataFiles );
		[ $callbacks, $main ] = $this->getCallbacks( $request );

		$this->assertNull( $callbacks->getUploadedFile( 'foo', [] ) );
		$this->assertNull( $callbacks->getUploadedFile( 'bar', [] ) );
		$this->assertNull( $callbacks->getUploadedFile( 'baz', [] ) );

		$file = $callbacks->getUploadedFile( 'file', [] );
		$this->assertInstanceOf( UploadedFileInterface::class, $file );
		$this->assertSame( UPLOAD_ERR_OK, $file->getError() );
		$this->assertSame( 'TestUploadStash.jpg', $file->getClientFilename() );

		$file = $callbacks->getUploadedFile( 'file2', [] );
		$this->assertInstanceOf( UploadedFileInterface::class, $file );
		$this->assertSame( UPLOAD_ERR_NO_FILE, $file->getError() );

		$file = $callbacks->getUploadedFile( 'file3', [] );
		$this->assertInstanceOf( UploadedFileInterface::class, $file );
		$this->assertSame( UPLOAD_ERR_INI_SIZE, $file->getError() );
	}

	/**
	 * @dataProvider provideRecordCondition
	 * @param DataMessageValue $message
	 * @param ApiMessage|null $expect
	 * @param bool $sensitive
	 */
	public function testRecordCondition(
		DataMessageValue $message, ?ApiMessage $expect, bool $sensitive = false
	): void {
		[ $callbacks, $main ] = $this->getCallbacks( new FauxRequest( [ 'testparam' => 'testvalue' ] ) );
		$query = $main->getModuleFromPath( 'query' );
		$warnings = [];

		$module = $this->getMockBuilder( ApiQueryBase::class )
			->setConstructorArgs( [ $query, 'test' ] )
			->onlyMethods( [ 'addWarning' ] )
			->getMockForAbstractClass();
		$module->method( 'addWarning' )->willReturnCallback(
			static function ( $msg, $code, $data ) use ( &$warnings ) {
				$warnings[] = [ $msg, $code, $data ];
			}
		);
		$query->getModuleManager()->addModule( 'test', 'meta', [
			'class' => get_class( $module ),
			'factory' => static function () use ( $module ) {
				return $module;
			}
		] );

		$callbacks->recordCondition( $message, 'testparam', 'testvalue', [], [ 'module' => $module ] );

		if ( $expect ) {
			$this->assertNotCount( 0, $warnings );
			$this->assertSame(
				$expect->inLanguage( 'qqx' )->plain(),
				wfMessage( $warnings[0][0] )->inLanguage( 'qqx' )->plain()
			);
			$this->assertSame( $expect->getApiCode(), $warnings[0][1] );
			$this->assertSame( $expect->getApiData(), $warnings[0][2] );
		} else {
			$this->assertSame( [], $warnings );
		}

		$this->assertSame(
			$sensitive ? [ 'testparam' ] : [],
			TestingAccessWrapper::newFromObject( $main )->getSensitiveParams()
		);
	}

	public static function provideRecordCondition(): Generator {
		yield 'Deprecated param' => [
			DataMessageValue::new(
				'paramvalidator-param-deprecated', [],
				'param-deprecated',
				[ 'data' => true ]
			)->plaintextParams( 'XXtestparam', 'XXtestvalue' ),
			ApiMessage::create(
				'paramvalidator-param-deprecated',
				'deprecation',
				[ 'data' => true, 'feature' => 'action=query&meta=test&testparam' ]
			)->plaintextParams( 'XXtestparam', 'XXtestvalue' )
		];

		yield 'Deprecated value' => [
			DataMessageValue::new(
				'paramvalidator-deprecated-value', [],
				'deprecated-value'
			)->plaintextParams( 'XXtestparam', 'XXtestvalue' ),
			ApiMessage::create(
				'paramvalidator-deprecated-value',
				'deprecation',
				[ 'feature' => 'action=query&meta=test&testparam=testvalue' ]
			)->plaintextParams( 'XXtestparam', 'XXtestvalue' )
		];

		yield 'Deprecated value with custom MessageValue' => [
			DataMessageValue::new(
				'some-custom-message-value', [],
				'deprecated-value',
				[ 'xyz' => 123 ]
			)->plaintextParams( 'XXtestparam', 'XXtestvalue', 'foobar' ),
			ApiMessage::create(
				'some-custom-message-value',
				'deprecation',
				[ 'xyz' => 123, 'feature' => 'action=query&meta=test&testparam=testvalue' ]
			)->plaintextParams( 'XXtestparam', 'XXtestvalue', 'foobar' )
		];

		// See ApiParamValidator::normalizeSettings()
		yield 'Deprecated value with custom Message' => [
			DataMessageValue::new(
				'some-custom-message', [],
				'deprecated-value',
				[ '💩' => 'back-compat' ]
			)->plaintextParams( 'XXtestparam', 'XXtestvalue', 'foobar' ),
			ApiMessage::create(
				'some-custom-message',
				'deprecation',
				[ 'feature' => 'action=query&meta=test&testparam=testvalue' ]
			)->plaintextParams( 'foobar' )
		];

		yield 'Sensitive param' => [
			DataMessageValue::new( 'paramvalidator-param-sensitive', [], 'param-sensitive' )
				->plaintextParams( 'XXtestparam', 'XXtestvalue' ),
			null,
			true
		];

		yield 'Arbitrary warning' => [
			DataMessageValue::new( 'some-warning', [], 'some-code', [ 'some-data' ] )
				->plaintextParams( 'XXtestparam', 'XXtestvalue', 'foobar' ),
			ApiMessage::create( 'some-warning', 'some-code', [ 'some-data' ] )
				->plaintextParams( 'XXtestparam', 'XXtestvalue', 'foobar' ),
		];
	}

	public function testUseHighLimits(): void {
		$context = $this->apiContext->newTestContext( new FauxRequest, $this->mockRegisteredUltimateAuthority() );
		$main = $this->getMockBuilder( ApiMain::class )
			->setConstructorArgs( [ $context ] )
			->onlyMethods( [ 'canApiHighLimits' ] )
			->getMock();

		$main->method( 'canApiHighLimits' )->willReturnOnConsecutiveCalls( true, false );

		$callbacks = new ApiParamValidatorCallbacks( $main );
		$this->assertTrue( $callbacks->useHighLimits( [] ) );
		$this->assertFalse( $callbacks->useHighLimits( [] ) );
	}
}
