<?php

namespace MediaWiki\Extension\CheckUser\Tests\Integration\Maintenance;

use MediaWiki\Extension\CheckUser\CheckUserQueryInterface;
use MediaWiki\Extension\CheckUser\Maintenance\PopulateCentralCheckUserIndexTables;
use MediaWiki\Extension\CheckUser\Tests\Integration\CheckUserCommonTestTrait;
use MediaWiki\Extension\CheckUser\Tests\Integration\CheckUserTempUserTestTrait;
use MediaWiki\MainConfigNames;
use MediaWiki\Request\FauxRequest;
use MediaWiki\Tests\Maintenance\MaintenanceBaseTestCase;
use MediaWiki\User\UserIdentity;
use MediaWiki\User\UserIdentityValue;
use Wikimedia\IPUtils;

/**
 * @covers \MediaWiki\Extension\CheckUser\Maintenance\PopulateCentralCheckUserIndexTables
 * @group CheckUser
 * @group Database
 */
class PopulateCentralCheckUserIndexTablesTest extends MaintenanceBaseTestCase implements CheckUserQueryInterface {

	use CheckUserTempUserTestTrait;
	use CheckUserCommonTestTrait;

	protected function setUp(): void {
		parent::setUp();
		// We don't want to test specifically the CentralAuth implementation of the CentralIdLookup. As such, force it
		// to be the local provider.
		$this->overrideConfigValue( MainConfigNames::CentralIdLookupProvider, 'local' );
	}

	/** @inheritDoc */
	protected function getMaintenanceClass() {
		return PopulateCentralCheckUserIndexTables::class;
	}

	public function testNoPopulationOnEmptyLocalCheckUserTables() {
		$this->assertTrue( $this->maintenance->execute() );
		$expectedOutputRegex = '/';
		foreach ( CheckUserQueryInterface::RESULT_TABLES as $table ) {
			$expectedOutputRegex .= "Skipping importing data from $table to central index " .
				"tables as the table is empty\n";
		}
		$this->expectOutputRegex( $expectedOutputRegex . '/' );
		$this->assertRowCount( 0, 'cuci_temp_edit', '*', 'cuci_temp_edit should be empty' );
		$this->assertRowCount( 0, 'cuci_wiki_map', '*', 'cuci_wiki_map should be empty' );
		$this->assertRowCount( 0, 'cuci_user', '*', 'cuci_user should be empty' );
	}

	private function getTestingRowForTable(
		string $table,
		UserIdentity $performer,
		string $ip,
		string $timestamp,
		int $oldId = 0
	): array {
		if ( $table !== self::CHANGES_TABLE && $oldId !== 0 ) {
			$this->fail( 'No table other than cu_changes can contain an revision ID' );
		}

		$columnAlias = self::RESULT_TABLE_TO_PREFIX[$table];
		$actorStore = $this->getServiceContainer()->getActorStore();

		$row = [
			$columnAlias . 'actor' => $actorStore->acquireActorId( $performer, $this->getDb() ),
			$columnAlias . 'timestamp' => $this->getDb()->timestamp( $timestamp ),
			$columnAlias . 'ip_hex' => IPUtils::toHex( $ip ),
		];

		if ( $oldId !== 0 ) {
			$row['cuc_this_oldid'] = $oldId;
		}

		return $row;
	}

	private function populateLocalCheckUserResultTablesForTest(): array {
		// Get two test users, a test temporary account, a test user with the bot group, and an anon user to use for
		// testing.
		$testUser1 = $this->getMutableTestUser()->getUserIdentity();
		$testUser2 = $this->getMutableTestUser()->getUserIdentity();
		$this->enableAutoCreateTempUser();
		$temporaryUser = $this->getServiceContainer()->getTempUserCreator()
			->create( null, new FauxRequest() )->getUser();
		$this->disableAutoCreateTempUser();
		$botUser = $this->getTestUser( [ 'bot' ] )->getUserIdentity();
		$anonUser = UserIdentityValue::newAnonymous( '4.3.2.1' );
		// Run jobs and then truncate all CheckUser result tables as CheckUserInsert will have created rows for the
		// creation of the temporary account.
		$this->runJobs();
		$this->truncateTables( [ 'cuci_user', 'cu_log_event', 'cu_private_event', 'cu_changes' ] );

		// Add test data for cu_changes
		$testData = [
			// Add rows which should be imported to only the cuci_user central index
			$this->getTestingRowForTable( self::CHANGES_TABLE, $testUser1, '1.2.3.1', '20200101000000', 1 ),
			$this->getTestingRowForTable( self::CHANGES_TABLE, $testUser1, '1.2.3.2', '20200101000001', 10 ),
			$this->getTestingRowForTable( self::CHANGES_TABLE, $testUser1, '1.2.3.3', '20200101000002', 100 ),
			$this->getTestingRowForTable( self::CHANGES_TABLE, $testUser1, '1.2.3.3', '20200101001000', 101 ),
			$this->getTestingRowForTable( self::CHANGES_TABLE, $testUser2, '1.2.3.3', '20200101000002', 103 ),
			$this->getTestingRowForTable( self::CHANGES_TABLE, $testUser2, '1.2.3.3', '20200101001000' ),
			$this->getTestingRowForTable(
				self::CHANGES_TABLE,
				$testUser2,
				'2001:0db8:85a3:0000:0000:8a2e:0370:7334',
				'20200101001000'
			),
			$this->getTestingRowForTable( self::CHANGES_TABLE, $temporaryUser, '1.2.3.5', '20200101001100' ),
			// Add rows which should be imported to both central indexes
			$this->getTestingRowForTable( self::CHANGES_TABLE, $temporaryUser, '1.2.3.4', '20200101001100', 123 ),
			$this->getTestingRowForTable( self::CHANGES_TABLE, $temporaryUser, '1.2.3.5', '20200101001200', 124 ),
			$this->getTestingRowForTable(
				self::CHANGES_TABLE,
				$temporaryUser,
				'2001:0db8:85a3:0000:0000:8a2e:0370:7334',
				'20200101001300',
				187
			),
			// Add rows which should not be imported to the central index (because they are anon users or bots)
			$this->getTestingRowForTable( self::CHANGES_TABLE, $botUser, '2.3.4.5', '20240506070809', 106 ),
			$this->getTestingRowForTable( self::CHANGES_TABLE, $anonUser, $anonUser->getName(), '20240506070810', 107 ),
		];

		$rows = array_map( static fn ( $row ) => [
			'cuc_type'       => RC_EDIT,
			'cuc_agent_id'   => 0,
			'cuc_namespace'  => NS_MAIN,
			'cuc_title'      => 'Foo_Page',
			'cuc_minor'      => 0,
			'cuc_page_id'    => 1,
			'cuc_xff'        => 0,
			'cuc_xff_hex'    => null,
			'cuc_comment_id' => 0,
			'cuc_last_oldid' => 0,
			'cuc_this_oldid' => 0,
			...$row,
		], $testData );

		$this->getDb()->newInsertQueryBuilder()
			->insertInto( 'cu_changes' )
			->rows( $rows )
			->caller( __METHOD__ )
			->execute();

		// Add test data for cu_log_event
		$testData = [
			// Add rows which should be imported to only the cuci_user central index
			$this->getTestingRowForTable( self::LOG_EVENT_TABLE, $testUser1, '1.2.3.4', '20200104000000' ),
			$this->getTestingRowForTable( self::LOG_EVENT_TABLE, $testUser1, '1.2.3.5', '20220101000000' ),
			$this->getTestingRowForTable( self::LOG_EVENT_TABLE, $testUser2, '1.2.3.6', '20220109000000' ),
			$this->getTestingRowForTable(
				self::LOG_EVENT_TABLE,
				$testUser2,
				'2001:0db8:85a3:0000:0000:8a2e:0370:7334',
				'20200101001000'
			),
			$this->getTestingRowForTable( self::LOG_EVENT_TABLE, $temporaryUser, '1.2.3.7', '20240109000000' ),
			// Add rows which should not be imported to the central index (because they are anon users or bots)
			$this->getTestingRowForTable( self::LOG_EVENT_TABLE, $botUser, '2.3.4.5', '20240506070809' ),
			$this->getTestingRowForTable( self::LOG_EVENT_TABLE, $anonUser, $anonUser->getName(), '20240506070810' ),
		];

		$rows = array_map( static fn ( $row ) => [
			'cule_xff' => 0,
			'cule_xff_hex' => null,
			'cule_agent_id' => 0,
			...$row,
		], $testData );

		$this->getDb()->newInsertQueryBuilder()
			->insertInto( 'cu_log_event' )
			->rows( $rows )
			->caller( __METHOD__ )
			->execute();

		// Add test data for cu_private_event
		$testData = [
			// Add rows which should be imported to only the cuci_user central index
			$this->getTestingRowForTable( self::PRIVATE_LOG_EVENT_TABLE, $testUser1, '1.2.3.4', '20200104000000' ),
			$this->getTestingRowForTable( self::PRIVATE_LOG_EVENT_TABLE, $testUser1, '1.2.3.5', '20220101000000' ),
			$this->getTestingRowForTable(
				self::PRIVATE_LOG_EVENT_TABLE,
				$testUser1,
				'2001:0db8:85a3:0000:0000:8a2e:0370:7334',
				'20200101001000'
			),
			$this->getTestingRowForTable( self::PRIVATE_LOG_EVENT_TABLE, $testUser2, '1.2.3.6', '20220109000000' ),
			$this->getTestingRowForTable( self::PRIVATE_LOG_EVENT_TABLE, $temporaryUser, '1.2.3.7', '20230109000002' ),
			// Add rows which should not be imported to the central index (because they are anon users or bots)
			$this->getTestingRowForTable( self::PRIVATE_LOG_EVENT_TABLE, $botUser, '2.3.4.5', '20230506070809' ),
			$this->getTestingRowForTable(
				self::PRIVATE_LOG_EVENT_TABLE,
				$anonUser,
				$anonUser->getName(),
				'20250506070810'
			),
		];

		$rows = array_map( static fn ( $row ) => [
			'cupe_agent_id' => 0,
			'cupe_xff' => 0,
			'cupe_xff_hex' => null,
			'cupe_params' => '',
			...$row,
		], $testData );

		$this->getDb()->newInsertQueryBuilder()
			->insertInto( 'cu_private_event' )
			->rows( $rows )
			->caller( __METHOD__ )
			->execute();

		return [
			'testUser1' => $testUser1,
			'testUser2' => $testUser2,
			'temporaryUser' => $temporaryUser,
			'botUser' => $botUser,
			'anonUser' => $anonUser,
		];
	}

	public function testPopulation() {
		$this->overrideConfigValue( 'CheckUserCentralIndexGroupsToExclude', [ 'bot' ] );
		$testUsers = $this->populateLocalCheckUserResultTablesForTest();
		$this->maintenance->loadWithArgv( [ '--batch-size', 2 ] );
		$this->maintenance->execute();

		// Run all jobs to make the inserts to cuci_user run
		$this->runJobs();

		$actorIds = array_map( function ( $userIdentity ) {
			return $this->getServiceContainer()->getActorStore()->findActorId( $userIdentity, $this->getDb() );
		}, $testUsers );
		sort( $actorIds );

		$actorIdGroups = array_chunk( $actorIds, 2 );

		// Generate the expected output regex
		$expectedOutputRegex = '/';
		foreach ( CheckUserQueryInterface::RESULT_TABLES as $table ) {
			$expectedOutputRegex .= "Now importing data from $table to the central index tables\n";
			foreach ( $actorIdGroups as $group ) {
				$expectedFirstActorId = reset( $group );
				$expectedLastActorId = end( $group );
				$expectedOutputRegex .= "...Processing users with actor IDs $expectedFirstActorId to " .
					"$expectedLastActorId\n";
			}
			$expectedOutputRegex .= "Finished importing data from $table to the central index tables\n";
		}
		$this->expectOutputRegex( $expectedOutputRegex . '/' );

		// Check that the cuci_wiki_map table contains one row for the current wiki domain.
		$this->newSelectQueryBuilder()
			->select( 'ciwm_wiki' )
			->from( 'cuci_wiki_map' )
			->assertFieldValue( $this->getDb()->getDomainID() );
		$wikiMapId = $this->newSelectQueryBuilder()
			->select( 'ciwm_id' )
			->from( 'cuci_wiki_map' )
			->fetchField();

		// Check that the expected rows exist in cuci_temp_edit
		$this->newSelectQueryBuilder()
			->select( [ 'cite_timestamp', 'cite_ip_hex', 'cite_ciwm_id' ] )
			->from( 'cuci_temp_edit' )
			->assertResultSet( [
				[ $this->getDb()->timestamp( '20200101001100' ), IPUtils::toHex( '1.2.3.4' ), $wikiMapId ],
				[ $this->getDb()->timestamp( '20200101001200' ), IPUtils::toHex( '1.2.3.5' ), $wikiMapId ],
				[
					$this->getDb()->timestamp( '20200101001300' ),
					IPUtils::toHex( '2001:0db8:85a3:0000:0000:8a2e:0370:7334' ),
					$wikiMapId,
				],
			] );

		// Check that the expected rows exist in cuci_user
		$this->newSelectQueryBuilder()
			->select( [ 'ciu_timestamp', 'ciu_central_id', 'ciu_ciwm_id' ] )
			->from( 'cuci_user' )
			->assertResultSet( [
				[ $this->getDb()->timestamp( '20220101000000' ), $testUsers['testUser1']->getId(), $wikiMapId ],
				[ $this->getDb()->timestamp( '20220109000000' ), $testUsers['testUser2']->getId(), $wikiMapId ],
				[ $this->getDb()->timestamp( '20240109000000' ), $testUsers['temporaryUser']->getId(), $wikiMapId ],
			] );
	}
}
