<?php

namespace MediaWiki\Tests\Auth;

use MediaWiki\Auth\AuthenticationRequest;
use MediaWiki\Auth\PasswordAuthenticationRequest;
use MediaWiki\Message\Message;
use MediaWikiIntegrationTestCase;
use UnexpectedValueException;

/**
 * @group AuthManager
 * @covers \MediaWiki\Auth\AuthenticationRequest
 */
class AuthenticationRequestTest extends MediaWikiIntegrationTestCase {
	public function testBasics() {
		$mock = $this->getMockForAbstractClass( AuthenticationRequest::class );

		$this->assertSame( get_class( $mock ), $mock->getUniqueId() );

		$this->assertIsArray( $mock->getMetadata() );

		$ret = $mock->describeCredentials();
		$this->assertIsArray( $ret );
		$this->assertArrayHasKey( 'provider', $ret );
		$this->assertInstanceOf( Message::class, $ret['provider'] );
		$this->assertArrayHasKey( 'account', $ret );
		$this->assertInstanceOf( Message::class, $ret['account'] );
	}

	public function testLoadRequestsFromSubmission() {
		$mb = $this->getMockBuilder( AuthenticationRequest::class )
			->onlyMethods( [ 'loadFromSubmission' ] );

		$data = [ 'foo', 'bar' ];

		$req1 = $mb->getMockForAbstractClass();
		$req1->expects( $this->once() )->method( 'loadFromSubmission' )
			->with( $this->identicalTo( $data ) )
			->willReturn( false );

		$req2 = $mb->getMockForAbstractClass();
		$req2->expects( $this->once() )->method( 'loadFromSubmission' )
			->with( $this->identicalTo( $data ) )
			->willReturn( true );

		$this->assertSame(
			[ $req2 ],
			AuthenticationRequest::loadRequestsFromSubmission( [ $req1, $req2 ], $data )
		);
	}

	public function testGetRequestByClass() {
		$mb = $this->getMockBuilder(
			AuthenticationRequest::class, 'AuthenticationRequestTest_AuthenticationRequest2'
		);

		$reqs = [
			$this->getMockForAbstractClass(
				AuthenticationRequest::class, [], 'AuthenticationRequestTest_AuthenticationRequest1'
			),
			$mb->getMockForAbstractClass(),
			$mb->getMockForAbstractClass(),
			$this->getMockForAbstractClass(
				PasswordAuthenticationRequest::class, [],
				'AuthenticationRequestTest_PasswordAuthenticationRequest'
			),
		];

		$this->assertNull( AuthenticationRequest::getRequestByClass(
			$reqs, 'AuthenticationRequestTest_AuthenticationRequest0'
		) );
		$this->assertSame( $reqs[0], AuthenticationRequest::getRequestByClass(
			$reqs, 'AuthenticationRequestTest_AuthenticationRequest1'
		) );
		$this->assertNull( AuthenticationRequest::getRequestByClass(
			$reqs, 'AuthenticationRequestTest_AuthenticationRequest2'
		) );
		$this->assertNull( AuthenticationRequest::getRequestByClass(
			$reqs, PasswordAuthenticationRequest::class
		) );
		$this->assertNull( AuthenticationRequest::getRequestByClass(
			$reqs, 'ClassThatDoesNotExist'
		) );

		$this->assertNull( AuthenticationRequest::getRequestByClass(
			$reqs, 'AuthenticationRequestTest_AuthenticationRequest0', true
		) );
		$this->assertSame( $reqs[0], AuthenticationRequest::getRequestByClass(
			$reqs, 'AuthenticationRequestTest_AuthenticationRequest1', true
		) );
		$this->assertNull( AuthenticationRequest::getRequestByClass(
			$reqs, 'AuthenticationRequestTest_AuthenticationRequest2', true
		) );
		$this->assertSame( $reqs[3], AuthenticationRequest::getRequestByClass(
			$reqs, PasswordAuthenticationRequest::class, true
		) );
		$this->assertNull( AuthenticationRequest::getRequestByClass(
			$reqs, 'ClassThatDoesNotExist', true
		) );
	}

	public function testGetUsernameFromRequests() {
		$mb = $this->getMockBuilder( AuthenticationRequest::class );

		for ( $i = 0; $i < 3; $i++ ) {
			$req = $mb->getMockForAbstractClass();
			$req->method( 'getFieldInfo' )->willReturn( [
				'username' => [
					'type' => 'string',
				],
			] );
			$reqs[] = $req;
		}

		$req = $mb->getMockForAbstractClass();
		$req->method( 'getFieldInfo' )->willReturn( [] );
		$req->username = 'baz';
		$reqs[] = $req;

		$this->assertNull( AuthenticationRequest::getUsernameFromRequests( $reqs ) );

		$reqs[1]->username = 'foo';
		$this->assertSame( 'foo', AuthenticationRequest::getUsernameFromRequests( $reqs ) );

		$reqs[0]->username = 'foo';
		$reqs[2]->username = 'foo';
		$this->assertSame( 'foo', AuthenticationRequest::getUsernameFromRequests( $reqs ) );

		$reqs[1]->username = 'bar';
		try {
			AuthenticationRequest::getUsernameFromRequests( $reqs );
			$this->fail( 'Expected exception not thrown' );
		} catch ( UnexpectedValueException $ex ) {
			$this->assertSame(
				'Conflicting username fields: "bar" from ' .
					get_class( $reqs[1] ) . '::$username vs. "foo" from ' .
					get_class( $reqs[0] ) . '::$username',
				$ex->getMessage()
			);
		}
	}

	public function testMergeFieldInfo() {
		$msg = wfMessage( 'foo' );

		$req1 = $this->createMock( AuthenticationRequest::class );
		$req1->required = AuthenticationRequest::REQUIRED;
		$req1->method( 'getFieldInfo' )->willReturn( [
			'string1' => [
				'type' => 'string',
				'label' => $msg,
				'help' => $msg,
			],
			'string2' => [
				'type' => 'string',
				'label' => $msg,
				'help' => $msg,
			],
			'optional' => [
				'type' => 'string',
				'label' => $msg,
				'help' => $msg,
				'optional' => true,
			],
			'select' => [
				'type' => 'select',
				'options' => [ 'foo' => $msg, 'baz' => $msg ],
				'label' => $msg,
				'help' => $msg,
			],
		] );

		$req2 = $this->createMock( AuthenticationRequest::class );
		$req2->required = AuthenticationRequest::REQUIRED;
		$req2->method( 'getFieldInfo' )->willReturn( [
			'string1' => [
				'type' => 'string',
				'label' => $msg,
				'help' => $msg,
				'sensitive' => true,
			],
			'string3' => [
				'type' => 'string',
				'label' => $msg,
				'help' => $msg,
			],
			'select' => [
				'type' => 'select',
				'options' => [ 'bar' => $msg, 'baz' => $msg ],
				'label' => $msg,
				'help' => $msg,
			],
		] );

		$req3 = $this->createMock( AuthenticationRequest::class );
		$req3->required = AuthenticationRequest::REQUIRED;
		$req3->method( 'getFieldInfo' )->willReturn( [
			'string1' => [
				'type' => 'checkbox',
				'label' => $msg,
				'help' => $msg,
			],
		] );

		$req4 = $this->createMock( AuthenticationRequest::class );
		$req4->required = AuthenticationRequest::REQUIRED;
		$req4->method( 'getFieldInfo' )->willReturn( [] );

		// Basic combining

		$this->assertEquals( [], AuthenticationRequest::mergeFieldInfo( [] ) );

		$fields = AuthenticationRequest::mergeFieldInfo( [ $req1 ] );
		$expect = $req1->getFieldInfo();
		foreach ( $expect as &$options ) {
			$options['optional'] = !empty( $options['optional'] );
			$options['sensitive'] = !empty( $options['sensitive'] );
		}
		unset( $options );
		$this->assertEquals( $expect, $fields );

		$fields = AuthenticationRequest::mergeFieldInfo( [ $req1, $req4 ] );
		$this->assertEquals( $expect, $fields );

		try {
			AuthenticationRequest::mergeFieldInfo( [ $req1, $req3 ] );
			$this->fail( 'Expected exception not thrown' );
		} catch ( UnexpectedValueException $ex ) {
			$this->assertSame(
				'Field type conflict for "string1", "string" vs "checkbox"',
				$ex->getMessage()
			);
		}

		$fields = AuthenticationRequest::mergeFieldInfo( [ $req1, $req2 ] );
		$expect += $req2->getFieldInfo();
		$expect['string1']['sensitive'] = true;
		$expect['string2']['optional'] = false;
		$expect['string3']['optional'] = false;
		$expect['string3']['sensitive'] = false;
		$expect['select']['options']['bar'] = $msg;
		$this->assertEquals( $expect, $fields );

		// Combining with something not required

		$req1->required = AuthenticationRequest::PRIMARY_REQUIRED;

		$fields = AuthenticationRequest::mergeFieldInfo( [ $req1, $req2 ] );
		$expect += $req2->getFieldInfo();
		$expect['string1']['optional'] = false;
		$expect['string1']['sensitive'] = true;
		$expect['string3']['optional'] = false;
		$expect['select']['optional'] = false;
		$expect['select']['options']['bar'] = $msg;
		$this->assertEquals( $expect, $fields );

		$req2->required = AuthenticationRequest::PRIMARY_REQUIRED;

		$fields = AuthenticationRequest::mergeFieldInfo( [ $req1, $req2 ] );
		$expect = $req1->getFieldInfo() + $req2->getFieldInfo();
		foreach ( $expect as &$options ) {
			$options['sensitive'] = !empty( $options['sensitive'] );
		}
		$expect['string1']['optional'] = false;
		$expect['string1']['sensitive'] = true;
		$expect['string2']['optional'] = true;
		$expect['string3']['optional'] = true;
		$expect['select']['optional'] = false;
		$expect['select']['options']['bar'] = $msg;
		$this->assertEquals( $expect, $fields );
	}

	/**
	 * @dataProvider provideLoadFromSubmission
	 * @param array $fieldInfo
	 * @param array $data
	 * @param array|bool $expectState
	 */
	public function testLoadFromSubmission( $fieldInfo, $data, $expectState ) {
		$mock = $this->getMockForAbstractClass( AuthenticationRequestForLoadFromSubmission::class );
		$mock->method( 'getFieldInfo' )
			->willReturn( $fieldInfo );

		$ret = $mock->loadFromSubmission( $data );
		if ( is_array( $expectState ) ) {
			$this->assertTrue( $ret );
			$expect = $mock::__set_state( $expectState );
			$this->assertEquals( $expect, $mock );
		} else {
			$this->assertFalse( $ret );
		}
	}

	public static function provideLoadFromSubmission() {
		return [
			'No fields' => [
				[],
				$data = [ 'foo' => 'bar' ],
				false
			],

			'Simple field' => [
				[
					'field' => [
						'type' => 'string',
					],
				],
				$data = [ 'field' => 'string!' ],
				$data
			],
			'Simple field, not supplied' => [
				[
					'field' => [
						'type' => 'string',
					],
				],
				[],
				false
			],
			'Simple field, empty' => [
				[
					'field' => [
						'type' => 'string',
					],
				],
				[ 'field' => '' ],
				false
			],
			'Simple field, optional, not supplied' => [
				[
					'field' => [
						'type' => 'string',
						'optional' => true,
					],
				],
				[],
				false
			],
			'Simple field, optional, empty' => [
				[
					'field' => [
						'type' => 'string',
						'optional' => true,
					],
				],
				$data = [ 'field' => '' ],
				$data
			],

			'Checkbox, checked' => [
				[
					'check' => [
						'type' => 'checkbox',
					],
				],
				[ 'check' => '' ],
				[ 'check' => true ]
			],
			'Checkbox, unchecked' => [
				[
					'check' => [
						'type' => 'checkbox',
					],
				],
				[],
				false
			],
			'Checkbox, optional, unchecked' => [
				[
					'check' => [
						'type' => 'checkbox',
						'optional' => true,
					],
				],
				[],
				[ 'check' => false ]
			],

			'Button, used' => [
				[
					'push' => [
						'type' => 'button',
					],
				],
				[ 'push' => '' ],
				[ 'push' => true ]
			],
			'Button, unused' => [
				[
					'push' => [
						'type' => 'button',
					],
				],
				[],
				false
			],
			'Button, optional, unused' => [
				[
					'push' => [
						'type' => 'button',
						'optional' => true,
					],
				],
				[],
				[ 'push' => false ]
			],
			'Button, image-style' => [
				[
					'push' => [
						'type' => 'button',
					],
				],
				[ 'push_x' => 0, 'push_y' => 0 ],
				[ 'push' => true ]
			],

			'Select' => [
				[
					'choose' => [
						'type' => 'select',
						'options' => [
							'foo' => wfMessage( 'mainpage' ),
							'bar' => wfMessage( 'mainpage' ),
						],
					],
				],
				$data = [ 'choose' => 'foo' ],
				$data
			],
			'Select, invalid choice' => [
				[
					'choose' => [
						'type' => 'select',
						'options' => [
							'foo' => wfMessage( 'mainpage' ),
							'bar' => wfMessage( 'mainpage' ),
						],
					],
				],
				$data = [ 'choose' => 'baz' ],
				false
			],
			'Multiselect (2)' => [
				[
					'choose' => [
						'type' => 'multiselect',
						'options' => [
							'foo' => wfMessage( 'mainpage' ),
							'bar' => wfMessage( 'mainpage' ),
						],
					],
				],
				$data = [ 'choose' => [ 'foo', 'bar' ] ],
				$data
			],
			'Multiselect (1)' => [
				[
					'choose' => [
						'type' => 'multiselect',
						'options' => [
							'foo' => wfMessage( 'mainpage' ),
							'bar' => wfMessage( 'mainpage' ),
						],
					],
				],
				$data = [ 'choose' => [ 'bar' ] ],
				$data
			],
			'Multiselect, string for some reason' => [
				[
					'choose' => [
						'type' => 'multiselect',
						'options' => [
							'foo' => wfMessage( 'mainpage' ),
							'bar' => wfMessage( 'mainpage' ),
						],
					],
				],
				[ 'choose' => 'foo' ],
				[ 'choose' => [ 'foo' ] ]
			],
			'Multiselect, invalid choice' => [
				[
					'choose' => [
						'type' => 'multiselect',
						'options' => [
							'foo' => wfMessage( 'mainpage' ),
							'bar' => wfMessage( 'mainpage' ),
						],
					],
				],
				[ 'choose' => [ 'foo', 'baz' ] ],
				false
			],
			'Multiselect, empty' => [
				[
					'choose' => [
						'type' => 'multiselect',
						'options' => [
							'foo' => wfMessage( 'mainpage' ),
							'bar' => wfMessage( 'mainpage' ),
						],
					],
				],
				[ 'choose' => [] ],
				false
			],
			'Multiselect, optional, nothing submitted' => [
				[
					'choose' => [
						'type' => 'multiselect',
						'options' => [
							'foo' => wfMessage( 'mainpage' ),
							'bar' => wfMessage( 'mainpage' ),
						],
						'optional' => true,
					],
				],
				[],
				[ 'choose' => [] ]
			],
		];
	}
}

// Dynamic properties from the testLoadFromSubmission not working in php8.2
abstract class AuthenticationRequestForLoadFromSubmission extends AuthenticationRequest {
	/** @var array */
	public $choose;
	/** @var bool */
	public $push;
	/** @var bool */
	public $check;
	/** @var string */
	public $field;
}
