<?php

use MediaWiki\Deferred\DeferredUpdates;
use MediaWiki\Extension\AbuseFilter\AbuseFilterServices;
use MediaWiki\Extension\AbuseFilter\Parser\AFPData;
use MediaWiki\Extension\AbuseFilter\Variables\LazyLoadedVariable;
use MediaWiki\RecentChanges\RecentChange;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Tests\User\TempUser\TempUserTestTrait;
use MediaWiki\Title\Title;
use MediaWiki\User\UserIdentity;
use MediaWiki\User\UserIdentityValue;
use MediaWiki\Utils\MWTimestamp;

/**
 * @group Test
 * @group AbuseFilter
 * @group AbuseFilterGeneric
 * @group Database
 * @covers \MediaWiki\Extension\AbuseFilter\VariableGenerator\RCVariableGenerator
 * @todo Make this a unit test?
 */
class RCVariableGeneratorTest extends MediaWikiIntegrationTestCase {
	use AbuseFilterCreateAccountTestTrait;
	use AbuseFilterUploadTestTrait;
	use TempUserTestTrait;

	/**
	 * @inheritDoc
	 */
	protected function tearDown(): void {
		$this->clearUploads();
		parent::tearDown();
	}

	/**
	 * Check all methods used to retrieve variables from an RC row
	 *
	 * @param string $type Type of the action the row refers to
	 * @param string $action Same as the 'action' variable
	 * @param UserIdentity|null $userIdentity The user who performed the action, null to use an autogenerated test user.
	 * @covers \MediaWiki\Extension\AbuseFilter\VariableGenerator\RCVariableGenerator
	 * @dataProvider provideRCRowTypes
	 */
	public function testGetVarsFromRCRow( string $type, string $action, ?UserIdentity $userIdentity = null ) {
		if ( $userIdentity && !$userIdentity->isRegistered() ) {
			// If we are testing anonymous user, make sure we disable temp accounts.
			$this->disableAutoCreateTempUser();
		}
		$timestamp = '1514700000';
		MWTimestamp::setFakeTime( $timestamp );
		if ( $userIdentity !== null ) {
			$user = $this->getServiceContainer()->getUserFactory()->newFromUserIdentity( $userIdentity );
		} else {
			$user = $this->getMutableTestUser()->getUser();
		}
		$title = Title::makeTitle( NS_MAIN, 'AbuseFilter testing page' );
		$services = $this->getServiceContainer();
		$wikiPageFactory = $services->getWikiPageFactory();
		$page = $type === 'create' ? $wikiPageFactory->newFromTitle( $title ) : $this->getExistingTestPage( $title );
		$page->clear();

		$summary = 'Abuse Filter summary for RC tests';
		$expectedValues = [
			'user_name' => $user->getName(),
			'action' => $action,
			'summary' => $summary,
			'timestamp' => $timestamp
		];
		$rcConds = [];

		switch ( $type ) {
			case 'create':
				$expectedValues['page_id'] = 0;
				$expectedValues['old_wikitext'] = '';
				$expectedValues['old_content_model'] = '';
				$expectedValues['page_last_edit_age'] = null;
			// Fallthrough
			case 'edit':
				$status = $this->editPage( $title, 'Some new text for testing RC vars.', $summary, NS_MAIN, $user );
				$this->assertArrayHasKey( 'revision-record', $status->value, 'Edit succeeded' );
				/** @var RevisionRecord $revRecord */
				$revRecord = $status->value['revision-record'];
				$rcConds['rc_this_oldid'] = $revRecord->getId();

				$expectedValues += [
					'page_id' => $page->getId(),
					'page_namespace' => $title->getNamespace(),
					'page_title' => $title->getText(),
					'page_prefixedtitle' => $title->getPrefixedText()
				];
				break;
			case 'move':
				$newTitle = Title::makeTitle( NS_MAIN, 'Another AbuseFilter testing page' );
				$mpf = $services->getMovePageFactory();
				$mp = $mpf->newMovePage( $title, $newTitle );
				$status = $mp->move( $user, $summary, false );
				$this->assertArrayHasKey( 'nullRevision', $status->value, 'Move succeeded' );
				/** @var RevisionRecord $revRecord */
				$revRecord = $status->value['nullRevision'];
				$rcConds['rc_this_oldid'] = $revRecord->getId();

				$expectedValues += [
					'moved_from_id' => $page->getId(),
					'moved_from_namespace' => $title->getNamespace(),
					'moved_from_title' => $title->getText(),
					'moved_from_prefixedtitle' => $title->getPrefixedText(),
					'moved_to_id' => $revRecord->getPageId(),
					'moved_to_namespace' => $newTitle->getNamespace(),
					'moved_to_title' => $newTitle->getText(),
					'moved_to_prefixedtitle' => $newTitle->getPrefixedText()
				];
				break;
			case 'delete':
				$status = $page->doDeleteArticleReal( $summary, $user );
				$rcConds['rc_logid'] = $status->value;

				$expectedValues += [
					'page_id' => $page->getId(),
					'page_namespace' => $title->getNamespace(),
					'page_title' => $title->getText(),
					'page_prefixedtitle' => $title->getPrefixedText()
				];
				break;
			case 'newusers':
				$accountName = 'AbuseFilter dummy user';
				$status = $this->createAccount( $accountName, $action === 'autocreateaccount', $user );
				$rcConds['rc_logid'] = $status->value;

				$expectedValues = [
					'action' => $action,
					'accountname' => $accountName,
					'timestamp' => $timestamp
				];
				if ( $user->isRegistered() ) {
					$expectedValues['user_name'] = $user->getName();
				}
				break;
			case 'upload':
				$fileName = 'My File.svg';
				$destTitle = Title::makeTitle( NS_FILE, $fileName );
				$page = $wikiPageFactory->newFromTitle( $destTitle );
				[ $status, $this->clearPath ] = $this->doUpload( $user, $fileName, 'Some text', $summary );
				if ( !$status->isGood() ) {
					throw new LogicException( "Cannot upload file:\n$status" );
				}
				$rcConds['rc_namespace'] = $destTitle->getNamespace();
				$rcConds['rc_title'] = $destTitle->getDbKey();

				// Since the SVG is randomly generated, we need to read some properties live
				$file = $services->getRepoGroup()->getLocalRepo()->newFile( $destTitle );
				$expectedValues += [
					'page_id' => $page->getId(),
					'page_namespace' => $destTitle->getNamespace(),
					'page_title' => $destTitle->getText(),
					'page_prefixedtitle' => $destTitle->getPrefixedText(),
					'file_sha1' => \Wikimedia\base_convert( $file->getSha1(), 36, 16, 40 ),
					'file_size' => $file->getSize(),
					'file_mime' => 'image/svg+xml',
					'file_mediatype' => 'DRAWING',
					'file_width' => $file->getWidth(),
					'file_height' => $file->getHeight(),
					'file_bits_per_channel' => $file->getBitDepth(),
				];
				break;
			default:
				throw new LogicException( "Type $type not recognized!" );
		}

		DeferredUpdates::doUpdates();
		$rc = $services->getRecentChangeLookup()->getRecentChangeByConds( $rcConds, __METHOD__, true );
		$this->assertNotNull( $rc, 'RC item found' );

		$accountCreationHookCalled = false;
		if ( $type === 'newusers' ) {
			// Verify that the AbuseFilterGenerateAccountCreationVars hook is called and provides the expected
			// data to the handlers.
			$this->setTemporaryHook(
				'AbuseFilterGenerateAccountCreationVars',
				function (
					$actualVars, UserIdentity $actualCreator, UserIdentity $actualCreatedUser, bool $autocreated,
					?RecentChange $actualRc
				) use ( &$accountCreationHookCalled, $user, $action, $rc ) {
					$this->assertTrue( $user->equals( $actualCreator ) );
					$this->assertSame( 'AbuseFilter dummy user', $actualCreatedUser->getName() );
					$this->assertSame( $action === 'autocreateaccount', $autocreated );
					$this->assertSame( $rc, $actualRc );

					$accountCreationHookCalled = true;
				}
			);
		}

		$varGenerator = AbuseFilterServices::getVariableGeneratorFactory()->newRCGenerator(
			$rc,
			$this->getTestSysop()->getUser()
		);
		$actual = $varGenerator->getVars()->getVars();

		// Convert PHP variables to AFPData
		$expected = array_map( [ AFPData::class, 'newFromPHPVar' ], $expectedValues );

		// Remove lazy variables (covered in other tests) and variables coming
		// from other extensions (may not be generated, depending on the test environment)
		$coreVariables = AbuseFilterServices::getKeywordsManager()->getCoreVariables();
		foreach ( $actual as $var => $value ) {
			if ( !in_array( $var, $coreVariables, true ) || $value instanceof LazyLoadedVariable ) {
				unset( $actual[ $var ] );
			}
		}

		// Not assertSame because we're comparing different AFPData objects
		$this->assertEquals( $expected, $actual );

		$this->assertSame(
			$type === 'newusers',
			$accountCreationHookCalled,
			'AbuseFilterGenerateAccountCreationVars hook should be run only if the type is "newusers"'
		);
	}

	/**
	 * Data provider for testGetVarsFromRCRow
	 * @return array
	 */
	public static function provideRCRowTypes() {
		return [
			'edit' => [ 'edit', 'edit' ],
			'create' => [ 'create', 'edit' ],
			'move' => [ 'move', 'move' ],
			'delete' => [ 'delete', 'delete' ],
			'createaccount' => [ 'newusers', 'createaccount' ],
			'createaccount with IP performer' => [
				'newusers', 'createaccount', UserIdentityValue::newAnonymous( '127.0.0.1' ),
			],
			'autocreateaccount' => [ 'newusers', 'autocreateaccount' ],
			'upload' => [ 'upload', 'upload' ],
		];
	}

	/**
	 * @covers \MediaWiki\Extension\AbuseFilter\Variables\LazyVariableComputer
	 */
	public function testAddEditVarsForRow() {
		$timestamp = 1514700000;
		MWTimestamp::setFakeTime( $timestamp );

		$title = Title::makeTitle( NS_MAIN, 'AbuseFilter testing page' );

		$oldLink = "https://wikipedia.org";
		$newLink = "https://en.wikipedia.org";
		$oldText = "test $oldLink";
		$newText = "new test $newLink";

		$this->editPage( $title, $oldText, 'Creating the test page' );

		$timestamp += 10;
		MWTimestamp::setFakeTime( $timestamp );

		$status = $this->editPage( $title, $newText, 'Editing the test page' );
		$this->assertArrayHasKey( 'revision-record', $status->value, 'Edit succeeded' );
		/** @var RevisionRecord $revRecord */
		$revRecord = $status->value['revision-record'];

		$rc = $this->getServiceContainer()->getRecentChangeLookup()->getRecentChangeByConds(
			[ 'rc_this_oldid' => $revRecord->getId() ],
			__METHOD__,
			true
		);
		$this->assertNotNull( $rc, 'RC item found' );

		// one more tick to reliably test page_age etc.
		MWTimestamp::setFakeTime( $timestamp + 10 );

		$generator = AbuseFilterServices::getVariableGeneratorFactory()->newRCGenerator(
			$rc,
			$this->getMutableTestUser()->getUser()
		);
		$varHolder = $generator->getVars();
		$manager = AbuseFilterServices::getVariablesManager();

		$expected = [
			'page_age' => 10,
			'page_last_edit_age' => 10,
			'old_wikitext' => $oldText,
			'old_size' => strlen( $oldText ),
			'old_content_model' => 'wikitext',
			'old_links' => [ "https://wikipedia.org/" ],
			'new_wikitext' => $newText,
			'new_size' => strlen( $newText ),
			'new_content_model' => 'wikitext',
			'new_links' => [ "https://en.wikipedia.org/" ],
			'timestamp' => (string)$timestamp,
		];
		foreach ( $expected as $var => $value ) {
			$this->assertSame(
				$value,
				$manager->getVar( $varHolder, $var )->toNative()
			);
		}
	}

	/**
	 * @covers \MediaWiki\Extension\AbuseFilter\Variables\LazyVariableComputer
	 */
	public function testAddUploadVars() {
		$timestamp = 1514700000;
		MWTimestamp::setFakeTime( $timestamp );

		$newText = 'Some text';
		$fileName = 'My File.svg';
		$destTitle = Title::makeTitle( NS_FILE, $fileName );
		$user = $this->getMutableTestUser()->getUser();
		[ $status, $this->clearPath ] = $this->doUpload( $user, $fileName, $newText, '' );
		if ( !$status->isGood() ) {
			throw new LogicException( "Cannot upload file:\n$status" );
		}

		$rc = $this->getServiceContainer()->getRecentChangeLookup()->getRecentChangeByConds(
			[
				'rc_namespace' => $destTitle->getNamespace(),
				'rc_title' => $destTitle->getDbKey(),
				'rc_source' => RecentChange::SRC_LOG,
				'rc_log_type' => 'upload',
				'rc_log_action' => 'upload',
			],
			__METHOD__,
			true
		);
		$this->assertNotNull( $rc, 'RC item found' );

		$generator = AbuseFilterServices::getVariableGeneratorFactory()->newRCGenerator(
			$rc,
			$this->getTestSysop()->getUser()
		);
		$varHolder = $generator->getVars();
		$manager = AbuseFilterServices::getVariablesManager();

		$expected = [
			'page_namespace' => $destTitle->getNamespace(),
			'page_title' => $destTitle->getText(),
			'page_prefixedtitle' => $destTitle->getPrefixedText(),
			'old_wikitext' => '',
			'old_size' => 0,
			'removed_lines' => [],
			'new_wikitext' => $newText,
			'new_size' => strlen( $newText ),
			'added_lines' => [ $newText ],
			'file_mime' => 'image/svg+xml',
			'file_mediatype' => 'DRAWING',
			'timestamp' => (string)$timestamp,
		];

		foreach ( $expected as $var => $value ) {
			$this->assertSame(
				$value,
				$manager->getVar( $varHolder, $var )->toNative()
			);
		}
	}

}
