<?php

namespace MediaWiki\Tests\Block;

use MediaWiki\Block\BlockRestrictionStore;
use MediaWiki\Block\DatabaseBlock;
use MediaWiki\Block\Restriction\NamespaceRestriction;
use MediaWiki\Block\Restriction\PageRestriction;
use MediaWiki\Block\Restriction\Restriction;

/**
 * @group Database
 * @group Blocking
 * @coversDefaultClass \MediaWiki\Block\BlockRestrictionStore
 */
class BlockRestrictionStoreTest extends \MediaWikiLangTestCase {

	/** @var BlockRestrictionStore */
	protected $blockRestrictionStore;

	protected function setUp(): void {
		parent::setUp();

		$this->blockRestrictionStore = $this->getServiceContainer()->getBlockRestrictionStore();
	}

	protected function tearDown(): void {
		$this->resetTables();
		parent::tearDown();
	}

	/**
	 * @covers ::loadByBlockId
	 * @covers ::resultToRestrictions
	 * @covers ::rowToRestriction
	 */
	public function testLoadMultipleRestrictions() {
		$this->setMwGlobals( [
			'wgBlockDisablesLogin' => false,
		] );
		$block = $this->insertBlock();

		$pageFoo = $this->getExistingTestPage( 'Foo' );
		$pageBar = $this->getExistingTestPage( 'Bar' );

		$this->blockRestrictionStore->insert( [
			new PageRestriction( $block->getId(), $pageFoo->getId() ),
			new PageRestriction( $block->getId(), $pageBar->getId() ),
			new NamespaceRestriction( $block->getId(), NS_USER ),
		] );

		$restrictions = $this->blockRestrictionStore->loadByBlockId( $block->getId() );

		$this->assertCount( 3, $restrictions );
	}

	/**
	 * @covers ::loadByBlockId
	 * @covers ::resultToRestrictions
	 * @covers ::rowToRestriction
	 */
	public function testWithNoRestrictions() {
		$block = $this->insertBlock();
		$restrictions = $this->blockRestrictionStore->loadByBlockId( $block->getId() );
		$this->assertSame( [], $restrictions );
	}

	/**
	 * @covers ::loadByBlockId
	 * @covers ::resultToRestrictions
	 * @covers ::rowToRestriction
	 */
	public function testWithEmptyParam() {
		$restrictions = $this->blockRestrictionStore->loadByBlockId( [] );
		$this->assertSame( [], $restrictions );
	}

	/**
	 * @covers ::loadByBlockId
	 * @covers ::resultToRestrictions
	 * @covers ::rowToRestriction
	 */
	public function testIgnoreNotSupportedTypes() {
		$block = $this->insertBlock();

		$pageFoo = $this->getExistingTestPage( 'Foo' );
		$pageBar = $this->getExistingTestPage( 'Bar' );

		// valid type
		$this->insertRestriction( $block->getId(), PageRestriction::TYPE_ID, $pageFoo->getId() );
		$this->insertRestriction( $block->getId(), NamespaceRestriction::TYPE_ID, NS_USER );

		// invalid type
		$this->insertRestriction( $block->getId(), 9, $pageBar->getId() );
		$this->insertRestriction( $block->getId(), 10, NS_FILE );

		$restrictions = $this->blockRestrictionStore->loadByBlockId( $block->getId() );
		$this->assertCount( 2, $restrictions );
	}

	/**
	 * @covers ::loadByBlockId
	 * @covers ::resultToRestrictions
	 * @covers ::rowToRestriction
	 */
	public function testMappingPageRestrictionObject() {
		$block = $this->insertBlock();
		$title = 'Lady Macbeth';
		$page = $this->getExistingTestPage( $title );

		// Test Page Restrictions.
		$this->blockRestrictionStore->insert( [
			new PageRestriction( $block->getId(), $page->getId() ),
		] );

		$restrictions = $this->blockRestrictionStore->loadByBlockId( $block->getId() );

		list( $pageRestriction ) = $restrictions;
		$this->assertInstanceOf( PageRestriction::class, $pageRestriction );
		$this->assertEquals( $block->getId(), $pageRestriction->getBlockId() );
		$this->assertEquals( $page->getId(), $pageRestriction->getValue() );
		$this->assertEquals( $pageRestriction->getType(), PageRestriction::TYPE );
		$this->assertEquals( $pageRestriction->getTitle()->getText(), $title );
	}

	/**
	 * @covers ::loadByBlockId
	 * @covers ::resultToRestrictions
	 * @covers ::rowToRestriction
	 */
	public function testMappingNamespaceRestrictionObject() {
		$block = $this->insertBlock();

		$this->blockRestrictionStore->insert( [
			new NamespaceRestriction( $block->getId(), NS_USER ),
		] );

		$restrictions = $this->blockRestrictionStore->loadByBlockId( $block->getId() );

		list( $namespaceRestriction ) = $restrictions;
		$this->assertInstanceOf( NamespaceRestriction::class, $namespaceRestriction );
		$this->assertEquals( $block->getId(), $namespaceRestriction->getBlockId() );
		$this->assertSame( NS_USER, $namespaceRestriction->getValue() );
		$this->assertEquals( $namespaceRestriction->getType(), NamespaceRestriction::TYPE );
	}

	/**
	 * @covers ::insert
	 */
	public function testInsert() {
		$block = $this->insertBlock();

		$pageFoo = $this->getExistingTestPage( 'Foo' );
		$pageBar = $this->getExistingTestPage( 'Bar' );

		$restrictions = [
			(object)[],
			new PageRestriction( $block->getId(), $pageFoo->getId() ),
			new PageRestriction( $block->getId(), $pageBar->getId() ),
			new NamespaceRestriction( $block->getId(), NS_USER )
		];

		$result = $this->blockRestrictionStore->insert( $restrictions );
		$this->assertTrue( $result );

		$restrictions = [
			(object)[],
		];

		$result = $this->blockRestrictionStore->insert( $restrictions );
		$this->assertFalse( $result );

		$result = $this->blockRestrictionStore->insert( [] );
		$this->assertFalse( $result );
	}

	/**
	 * @covers ::insert
	 */
	public function testInsertTypes() {
		$block = $this->insertBlock();

		$pageFoo = $this->getExistingTestPage( 'Foo' );
		$pageBar = $this->getExistingTestPage( 'Bar' );

		$invalid = $this->createMock( Restriction::class );
		$invalid->method( 'toRow' )
			->willReturn( [
				'ir_ipb_id' => $block->getId(),
				'ir_type' => 9,
				'ir_value' => 42,
			] );

		$restrictions = [
			(object)[],
			new PageRestriction( $block->getId(), $pageFoo->getId() ),
			new PageRestriction( $block->getId(), $pageBar->getId() ),
			new NamespaceRestriction( $block->getId(), NS_USER ),
			$invalid,
		];

		$result = $this->blockRestrictionStore->insert( $restrictions );
		$this->assertTrue( $result );

		$restrictions = $this->blockRestrictionStore->loadByBlockId( $block->getId() );
		$this->assertCount( 3, $restrictions );
	}

	/**
	 * @covers ::update
	 * @covers ::restrictionsByBlockId
	 * @covers ::restrictionsToRemove
	 */
	public function testUpdateInsert() {
		$block = $this->insertBlock();
		$pageFoo = $this->getExistingTestPage( 'Foo' );
		$pageBar = $this->getExistingTestPage( 'Bar' );
		$this->blockRestrictionStore->insert( [
				new PageRestriction( $block->getId(), $pageFoo->getId() ),
		] );

		$this->blockRestrictionStore->update( [
			(object)[],
			new PageRestriction( $block->getId(), $pageBar->getId() ),
			new NamespaceRestriction( $block->getId(), NS_USER ),
		] );

		$db = wfGetDB( DB_REPLICA );
		$result = $db->select(
			[ 'ipblocks_restrictions' ],
			[ '*' ],
			[ 'ir_ipb_id' => $block->getId() ]
		);

		$this->assertEquals( 2, $result->numRows() );
		$row = $result->fetchObject();
		$this->assertEquals( $block->getId(), $row->ir_ipb_id );
		$this->assertEquals( $pageBar->getId(), $row->ir_value );
	}

	/**
	 * @covers ::update
	 * @covers ::restrictionsByBlockId
	 * @covers ::restrictionsToRemove
	 */
	public function testUpdateChange() {
		$block = $this->insertBlock();
		$page = $this->getExistingTestPage( 'Foo' );

		$this->blockRestrictionStore->update( [
			new PageRestriction( $block->getId(), $page->getId() ),
		] );

		$db = wfGetDB( DB_REPLICA );
		$result = $db->select(
			[ 'ipblocks_restrictions' ],
			[ '*' ],
			[ 'ir_ipb_id' => $block->getId() ]
		);

		$this->assertSame( 1, $result->numRows() );
		$row = $result->fetchObject();
		$this->assertEquals( $block->getId(), $row->ir_ipb_id );
		$this->assertEquals( $page->getId(), $row->ir_value );
	}

	/**
	 * @covers ::update
	 * @covers ::restrictionsByBlockId
	 * @covers ::restrictionsToRemove
	 */
	public function testUpdateNoRestrictions() {
		$block = $this->insertBlock();

		$this->blockRestrictionStore->update( [] );

		$db = wfGetDB( DB_REPLICA );
		$result = $db->select(
			[ 'ipblocks_restrictions' ],
			[ '*' ],
			[ 'ir_ipb_id' => $block->getId() ]
		);

		$this->assertSame( 0, $result->numRows() );
	}

	/**
	 * @covers ::update
	 * @covers ::restrictionsByBlockId
	 * @covers ::restrictionsToRemove
	 */
	public function testUpdateSame() {
		$block = $this->insertBlock();
		$page = $this->getExistingTestPage( 'Foo' );
		$this->blockRestrictionStore->insert( [
			new PageRestriction( $block->getId(), $page->getId() ),
		] );

		$this->blockRestrictionStore->update( [
			new PageRestriction( $block->getId(), $page->getId() ),
		] );

		$db = wfGetDB( DB_REPLICA );
		$result = $db->select(
			[ 'ipblocks_restrictions' ],
			[ '*' ],
			[ 'ir_ipb_id' => $block->getId() ]
		);

		$this->assertSame( 1, $result->numRows() );
		$row = $result->fetchObject();
		$this->assertEquals( $block->getId(), $row->ir_ipb_id );
		$this->assertEquals( $page->getId(), $row->ir_value );
	}

	/**
	 * @covers ::updateByParentBlockId
	 */
	public function testDeleteAllUpdateByParentBlockId() {
		// Create a block and an autoblock (a child block)
		$block = $this->insertBlock();
		$pageFoo = $this->getExistingTestPage( 'Foo' );
		$pageBar = $this->getExistingTestPage( 'Bar' );
		$this->blockRestrictionStore->insert( [
			new PageRestriction( $block->getId(), $pageFoo->getId() ),
		] );
		$autoblockId = $block->doAutoblock( '127.0.0.1' );

		// Ensure that the restrictions on the block have not changed.
		$restrictions = $this->blockRestrictionStore->loadByBlockId( $block->getId() );
		$this->assertCount( 1, $restrictions );
		$this->assertEquals( $pageFoo->getId(), $restrictions[0]->getValue() );

		// Ensure that the restrictions on the autoblock are the same as the block.
		$restrictions = $this->blockRestrictionStore->loadByBlockId( $autoblockId );
		$this->assertCount( 1, $restrictions );
		$this->assertEquals( $pageFoo->getId(), $restrictions[0]->getValue() );

		// Update the restrictions on the autoblock (but leave the block unchanged)
		$this->blockRestrictionStore->updateByParentBlockId( $block->getId(), [
			new PageRestriction( $block->getId(), $pageBar->getId() ),
		] );

		// Ensure that the restrictions on the block have not changed.
		$restrictions = $this->blockRestrictionStore->loadByBlockId( $block->getId() );
		$this->assertCount( 1, $restrictions );
		$this->assertEquals( $pageFoo->getId(), $restrictions[0]->getValue() );

		// Ensure that the restrictions on the autoblock have been updated.
		$restrictions = $this->blockRestrictionStore->loadByBlockId( $autoblockId );
		$this->assertCount( 1, $restrictions );
		$this->assertEquals( $pageBar->getId(), $restrictions[0]->getValue() );
	}

	/**
	 * @covers ::updateByParentBlockId
	 */
	public function testUpdateByParentBlockId() {
		// Create a block and an autoblock (a child block)
		$block = $this->insertBlock();
		$page = $this->getExistingTestPage( 'Foo' );
		$this->blockRestrictionStore->insert( [
			new PageRestriction( $block->getId(), $page->getId() ),
		] );
		$autoblockId = $block->doAutoblock( '127.0.0.1' );

		// Ensure that the restrictions on the block have not changed.
		$restrictions = $this->blockRestrictionStore->loadByBlockId( $block->getId() );
		$this->assertCount( 1, $restrictions );

		// Ensure that the restrictions on the autoblock have not changed.
		$restrictions = $this->blockRestrictionStore->loadByBlockId( $autoblockId );
		$this->assertCount( 1, $restrictions );

		// Remove the restrictions on the autoblock (but leave the block unchanged)
		$this->blockRestrictionStore->updateByParentBlockId( $block->getId(), [] );

		// Ensure that the restrictions on the block have not changed.
		$restrictions = $this->blockRestrictionStore->loadByBlockId( $block->getId() );
		$this->assertCount( 1, $restrictions );

		// Ensure that the restrictions on the autoblock have been updated.
		$restrictions = $this->blockRestrictionStore->loadByBlockId( $autoblockId );
		$this->assertSame( [], $restrictions );
	}

	/**
	 * @covers ::updateByParentBlockId
	 */
	public function testNoAutoblocksUpdateByParentBlockId() {
		// Create a block with no autoblock.
		$block = $this->insertBlock();
		$page = $this->getExistingTestPage( 'Foo' );
		$this->blockRestrictionStore->insert( [
			new PageRestriction( $block->getId(), $page->getId() ),
		] );

		// Ensure that the restrictions on the block have not changed.
		$restrictions = $this->blockRestrictionStore->loadByBlockId( $block->getId() );
		$this->assertCount( 1, $restrictions );

		// Update the restrictions on any autoblocks (there are none).
		$this->blockRestrictionStore->updateByParentBlockId( $block->getId(), $restrictions );

		// Ensure that the restrictions on the block have not changed.
		$restrictions = $this->blockRestrictionStore->loadByBlockId( $block->getId() );
		$this->assertCount( 1, $restrictions );
	}

	/**
	 * @covers ::delete
	 */
	public function testDelete() {
		$block = $this->insertBlock();
		$page = $this->getExistingTestPage( 'Foo' );
		$this->blockRestrictionStore->insert( [
			new PageRestriction( $block->getId(), $page->getId() ),
		] );

		$restrictions = $this->blockRestrictionStore->loadByBlockId( $block->getId() );
		$this->assertCount( 1, $restrictions );

		$result = $this->blockRestrictionStore->delete(
			array_merge( $restrictions, [ (object)[] ] )
		);
		$this->assertTrue( $result );

		$restrictions = $this->blockRestrictionStore->loadByBlockId( $block->getId() );
		$this->assertSame( [], $restrictions );
	}

	/**
	 * @covers ::deleteByBlockId
	 */
	public function testDeleteByBlockId() {
		$block = $this->insertBlock();
		$page = $this->getExistingTestPage( 'Foo' );
		$this->blockRestrictionStore->insert( [
			new PageRestriction( $block->getId(), $page->getId() ),
		] );

		$restrictions = $this->blockRestrictionStore->loadByBlockId( $block->getId() );
		$this->assertCount( 1, $restrictions );

		$result = $this->blockRestrictionStore->deleteByBlockId( $block->getId() );
		$this->assertNotFalse( $result );

		$restrictions = $this->blockRestrictionStore->loadByBlockId( $block->getId() );
		$this->assertSame( [], $restrictions );
	}

	/**
	 * @covers ::deleteByParentBlockId
	 */
	public function testDeleteByParentBlockId() {
		// Create a block with no autoblock.
		$block = $this->insertBlock();
		$page = $this->getExistingTestPage( 'Foo' );
		$this->blockRestrictionStore->insert( [
			new PageRestriction( $block->getId(), $page->getId() ),
		] );
		$autoblockId = $block->doAutoblock( '127.0.0.1' );

		// Ensure that the restrictions on the block have not changed.
		$restrictions = $this->blockRestrictionStore->loadByBlockId( $block->getId() );
		$this->assertCount( 1, $restrictions );

		// Ensure that the restrictions on the autoblock are the same as the block.
		$restrictions = $this->blockRestrictionStore->loadByBlockId( $autoblockId );
		$this->assertCount( 1, $restrictions );

		// Remove all of the restrictions on the autoblock (but leave the block unchanged).
		$result = $this->blockRestrictionStore->deleteByParentBlockId( $block->getId() );
		// NOTE: commented out until https://gerrit.wikimedia.org/r/c/mediawiki/core/+/469324 is merged
		//$this->assertTrue( $result );

		// Ensure that the restrictions on the block have not changed.
		$restrictions = $this->blockRestrictionStore->loadByBlockId( $block->getId() );
		$this->assertCount( 1, $restrictions );

		// Ensure that the restrictions on the autoblock have been removed.
		$restrictions = $this->blockRestrictionStore->loadByBlockId( $autoblockId );
		$this->assertSame( [], $restrictions );
	}

	/**
	 * @covers ::equals
	 * @dataProvider equalsDataProvider
	 *
	 * @param array $a
	 * @param array $b
	 * @param bool $expected
	 */
	public function testEquals( array $a, array $b, $expected ) {
		$this->assertSame( $expected, $this->blockRestrictionStore->equals( $a, $b ) );
	}

	public function equalsDataProvider() {
		return [
			[
				[
					(object)[],
					new PageRestriction( 1, 1 ),
				],
				[
					(object)[],
					new PageRestriction( 1, 2 )
				],
				false,
			],
			[
				[
					new PageRestriction( 1, 1 ),
				],
				[
					new PageRestriction( 1, 1 ),
					new PageRestriction( 1, 2 )
				],
				false,
			],
			[
				[],
				[],
				true,
			],
			[
				[
					new PageRestriction( 1, 1 ),
					new PageRestriction( 1, 2 ),
					new PageRestriction( 2, 3 ),
				],
				[
					new PageRestriction( 2, 3 ),
					new PageRestriction( 1, 2 ),
					new PageRestriction( 1, 1 ),
				],
				true
			],
			[
				[
					new NamespaceRestriction( 1, NS_USER ),
				],
				[
					new NamespaceRestriction( 1, NS_USER ),
				],
				true
			],
			[
				[
					new NamespaceRestriction( 1, NS_USER ),
				],
				[
					new NamespaceRestriction( 1, NS_TALK ),
				],
				false
			],
		];
	}

	/**
	 * @covers ::setBlockId
	 */
	public function testSetBlockId() {
		$restrictions = [
			(object)[],
			new PageRestriction( 1, 1 ),
			new PageRestriction( 1, 2 ),
			new NamespaceRestriction( 1, NS_USER ),
		];

		$this->assertSame( 1, $restrictions[1]->getBlockId() );
		$this->assertSame( 1, $restrictions[2]->getBlockId() );
		$this->assertSame( 1, $restrictions[3]->getBlockId() );

		$result = $this->blockRestrictionStore->setBlockId( 2, $restrictions );

		foreach ( $result as $restriction ) {
			$this->assertSame( 2, $restriction->getBlockId() );
		}
	}

	protected function insertBlock() {
		$badActor = $this->getTestUser()->getUser();
		$sysop = $this->getTestSysop()->getUser();

		$block = new DatabaseBlock( [
			'address' => $badActor->getName(),
			'user' => $badActor->getId(),
			'by' => $sysop,
			'expiry' => 'infinity',
			'sitewide' => 0,
			'enableAutoblock' => true,
		] );

		$this->getServiceContainer()->getDatabaseBlockStore()->insertBlock( $block );

		return $block;
	}

	protected function insertRestriction( $blockId, $type, $value ) {
		$this->db->insert( 'ipblocks_restrictions', [
			'ir_ipb_id' => $blockId,
			'ir_type' => $type,
			'ir_value' => $value,
		] );
	}

	protected function resetTables() {
		$this->db->delete( 'ipblocks', '*', __METHOD__ );
		$this->db->delete( 'ipblocks_restrictions', '*', __METHOD__ );
	}
}
