<?php

use MediaWiki\Config\ServiceOptions;
use MediaWiki\Revision\MutableRevisionRecord;
use MediaWiki\Revision\RevisionLookup;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Tests\Unit\DummyServicesTrait;
use MediaWiki\User\TalkPageNotificationManager;
use MediaWiki\User\UserIdentity;
use MediaWiki\User\UserIdentityValue;
use PHPUnit\Framework\AssertionFailedError;

/**
 * @covers \MediaWiki\User\TalkPageNotificationManager
 * @group Database
 */
class TalkPageNotificationManagerTest extends MediaWikiIntegrationTestCase {
	use DummyServicesTrait;

	protected function setUp(): void {
		parent::setUp();
		// tablesUsed don't clear up the database before the first test runs: T265033
		$this->truncateTable( 'user_newtalk' );
		$this->tablesUsed[] = 'user_newtalk';
	}

	private function editUserTalk( UserIdentity $user, string $text ): RevisionRecord {
		// UserIdentity doesn't have getUserPage/getTalkPage, but we can easily recreate
		// it, and its easier than needing to depend on a full user object
		$userTalk = Title::makeTitle( NS_USER_TALK, $user->getName() );
		$status = $this->editPage(
			$userTalk->getPrefixedText(),
			$text,
			'',
			NS_MAIN,
			$this->getTestSysop()->getUser()
		);
		$this->assertStatusGood( $status, 'create revision of user talk' );
		return $status->getValue()['revision-record'];
	}

	private function getManager(
		bool $disableAnonTalk = false,
		bool $isReadOnly = false,
		RevisionLookup $revisionLookup = null
	) {
		$services = $this->getServiceContainer();
		return new TalkPageNotificationManager(
			new ServiceOptions(
				TalkPageNotificationManager::CONSTRUCTOR_OPTIONS,
				new HashConfig( [
					'DisableAnonTalk' => $disableAnonTalk
				] )
			),
			$services->getDBLoadBalancer(),
			$this->getDummyReadOnlyMode( $isReadOnly ),
			$revisionLookup ?? $services->getRevisionLookup(),
			$this->createHookContainer(),
			$services->getUserFactory()
		);
	}

	public function provideUserHasNewMessages() {
		yield 'Registered user' => [ UserIdentityValue::newRegistered( 123, 'MyName' ) ];
		yield 'Anonymous user' => [ UserIdentityValue::newAnonymous( '1.2.3.4' ) ];
	}

	/**
	 * @dataProvider provideUserHasNewMessages
	 * @covers \MediaWiki\User\TalkPageNotificationManager::userHasNewMessages
	 * @covers \MediaWiki\User\TalkPageNotificationManager::setUserHasNewMessages
	 * @covers \MediaWiki\User\TalkPageNotificationManager::clearInstanceCache
	 * @covers \MediaWiki\User\TalkPageNotificationManager::removeUserHasNewMessages
	 */
	public function testUserHasNewMessages( UserIdentity $user ) {
		$manager = $this->getManager();
		$this->assertFalse( $manager->userHasNewMessages( $user ),
			'Should be false before updated' );
		$revRecord = $this->editUserTalk( $user, __METHOD__ );
		$manager->setUserHasNewMessages( $user, $revRecord );
		$this->assertTrue( $manager->userHasNewMessages( $user ),
			'Should be true after updated' );
		$manager->clearInstanceCache( $user );
		$this->assertTrue( $manager->userHasNewMessages( $user ),
			'Should be true after cache cleared' );
		$manager->removeUserHasNewMessages( $user );
		$this->assertFalse( $manager->userHasNewMessages( $user ),
			'Should be false after updated' );
		$manager->clearInstanceCache( $user );
		$this->assertFalse( $manager->userHasNewMessages( $user ),
			'Should be false after cache cleared' );
		$manager->setUserHasNewMessages( $user, null );
		$this->assertTrue( $manager->userHasNewMessages( $user ),
			'Should be true after updated' );
		$manager->removeUserHasNewMessages( $user );
		$this->assertFalse( $manager->userHasNewMessages( $user ),
			'Should be false after updated' );
	}

	/**
	 * @covers \MediaWiki\User\TalkPageNotificationManager::userHasNewMessages
	 * @covers \MediaWiki\User\TalkPageNotificationManager::setUserHasNewMessages
	 */
	public function testUserHasNewMessagesDisabledAnon() {
		$user = new UserIdentityValue( 0, '1.2.3.4' );
		$revRecord = $this->editUserTalk( $user, __METHOD__ );
		$manager = $this->getManager( true );
		$this->assertFalse( $manager->userHasNewMessages( $user ),
			'New anon should have no new messages' );
		$manager->setUserHasNewMessages( $user, $revRecord );
		$this->assertFalse( $manager->userHasNewMessages( $user ),
			'Must not set new messages for anon if disabled' );
		$manager->clearInstanceCache( $user );
		$this->assertFalse( $manager->userHasNewMessages( $user ),
			'Must not set to database if anon messages disabled' );
	}

	/**
	 * @covers \MediaWiki\User\TalkPageNotificationManager::getLatestSeenMessageTimestamp
	 */
	public function testGetLatestSeenMessageTimestamp() {
		$user = $this->getTestUser()->getUser();
		$firstRev = $this->editUserTalk( $user, __METHOD__ . ' 1' );
		$secondRev = $this->editUserTalk( $user, __METHOD__ . ' 2' );
		$manager = $this->getManager();
		$manager->setUserHasNewMessages( $user, $secondRev );
		$this->assertSame( $firstRev->getTimestamp(), $manager->getLatestSeenMessageTimestamp( $user ) );
	}

	/**
	 * @covers \MediaWiki\User\TalkPageNotificationManager::getLatestSeenMessageTimestamp
	 */
	public function testGetLatestSeenMessageTimestampOutOfOrderRevision() {
		$user = $this->getTestUser()->getUser();
		$firstRev = $this->editUserTalk( $user, __METHOD__ . ' 1' );
		$secondRev = $this->editUserTalk( $user, __METHOD__ . ' 2' );
		$thirdRev = $this->editUserTalk( $user, __METHOD__ . ' 3' );
		$veryOldTimestamp = MWTimestamp::convert( TS_MW, 1 );
		$mockOldRev = $this->createMock( RevisionRecord::class );
		$mockOldRev->method( 'getTimestamp' )
			->willReturn( $veryOldTimestamp );
		$mockRevLookup = $this->getMockForAbstractClass( RevisionLookup::class );
		$mockRevLookup->method( 'getPreviousRevision' )
			->willReturnCallback( static function ( RevisionRecord $rev )
				use ( $firstRev, $secondRev, $thirdRev, $mockOldRev )
			{
				if ( $rev === $secondRev ) {
					return $firstRev;
				}
				if ( $rev === $thirdRev ) {
					return $mockOldRev;
				}
				throw new AssertionFailedError(
					'RevisionLookup::getPreviousRevision called with wrong rev ' . $rev->getId()
				);
			} );
		$manager = $this->getManager( false, false, $mockRevLookup );
		$manager->setUserHasNewMessages( $user, $thirdRev );
		$this->assertSame( $veryOldTimestamp, $manager->getLatestSeenMessageTimestamp( $user ) );
		$manager->setUserHasNewMessages( $user, $secondRev );
		$this->assertSame( $veryOldTimestamp, $manager->getLatestSeenMessageTimestamp( $user ) );
	}

	/**
	 * @covers \MediaWiki\User\TalkPageNotificationManager::getLatestSeenMessageTimestamp
	 */
	public function testGetLatestSeenMessageTimestampNoNewMessages() {
		$user = $this->getTestUser()->getUser();
		$manager = $this->getManager();
		$this->assertNull( $manager->getLatestSeenMessageTimestamp( $user ),
			'Must be null if no new messages' );
	}

	/**
	 * @covers \MediaWiki\User\TalkPageNotificationManager::userHasNewMessages
	 * @covers \MediaWiki\User\TalkPageNotificationManager::setUserHasNewMessages
	 * @covers \MediaWiki\User\TalkPageNotificationManager::removeUserHasNewMessages
	 */
	public function testDoesNotCrashOnReadOnly() {
		$user = $this->getTestUser()->getUser();
		$this->editUserTalk( $user, __METHOD__ );

		$manager = $this->getManager( false, true );
		$this->assertTrue( $manager->userHasNewMessages( $user ) );
		$manager->removeUserHasNewMessages( $user );
		$this->assertFalse( $manager->userHasNewMessages( $user ) );
	}

	/**
	 * @covers \MediaWiki\User\TalkPageNotificationManager::clearForPageView
	 */
	public function testClearForPageView() {
		$user = $this->getTestUser()->getUser();
		$title = $user->getTalkPage();
		$revision = new MutableRevisionRecord( $title );
		$revision->setPageId( 100 );
		$revision->setId( 101 );
		$manager = $this->getManager();
		$manager->setUserHasNewMessages( $user );
		$this->assertTrue( $manager->userHasNewMessages( $user ) );

		// DB should have the notification
		$this->assertSelect(
			'user_newtalk',
			'user_id',
			[ 'user_id' => $user->getId() ],
			[ [ $user->getId() ] ]
		);

		$this->db->startAtomic( __METHOD__ ); // let deferred updates queue up

		$updateCountBefore = DeferredUpdates::pendingUpdatesCount();
		$manager->clearForPageView( $user, $revision );
		// Cache should already be updated
		$this->assertFalse( $manager->userHasNewMessages( $user ) );

		$updateCountAfter = DeferredUpdates::pendingUpdatesCount();
		$this->assertGreaterThan( $updateCountBefore, $updateCountAfter, 'An update should have been queued' );

		$this->db->endAtomic( __METHOD__ ); // run deferred updates
		$this->assertSame( 0, DeferredUpdates::pendingUpdatesCount(), 'No pending updates' );

		// Notification should have been deleted from the DB
		$this->assertSelect(
			'user_newtalk',
			'user_id',
			[ 'user_id' => $user->getId() ],
			[]
		);
	}

}
