<?php

use Psr\Container\ContainerInterface;
use Wikimedia\ObjectFactory\ObjectFactory;

/**
 * @covers SkinFactory
 */
class SkinFactoryTest extends \MediaWikiUnitTestCase {

	private function createSkinFactory( $service = null, $options = [] ): SkinFactory {
		$objectFactory = $service
			? new ObjectFactory( $service )
			: new ObjectFactory( $this->createMock( ContainerInterface::class ) );

		return new SkinFactory( $objectFactory, $options );
	}

	public function testRegisterWithInvalidCallable() {
		$factory = $this->createSkinFactory();

		$this->expectException( InvalidArgumentException::class );
		$factory->register( 'invalid', 'Invalid', 'Invalid callback' );
	}

	public function testRegisterWithCallable() {
		$factory = $this->createSkinFactory();
		$instance = new SkinFallback();

		$factory->register( 'fallback', 'Fallback', static function () use ( $instance ) {
			return $instance;
		}, true );

		$this->assertSame( $instance, $factory->makeSkin( 'fallback' ) );
	}

	public function testRegisterWithSpec() {
		$factory = $this->createSkinFactory();
		$factory->register( 'fallback', 'Fallback', [
			'class' => SkinFallback::class
		], true );

		$this->assertInstanceOf( SkinFallback::class, $factory->makeSkin( 'fallback' ) );
	}

	public function testMakeSkinWithNoBuilders() {
		$factory = $this->createSkinFactory();
		$this->expectException( SkinException::class );
		$factory->makeSkin( 'nobuilderregistered' );
	}

	public function testMakeSkinWithInvalidCallback() {
		$factory = $this->createSkinFactory();
		$factory->register( 'unittest', 'Unittest', static function () {
			// Not a Skin object
			return true;
		} );
		$this->expectException( UnexpectedValueException::class );
		$factory->makeSkin( 'unittest' );
	}

	public function testMakeSkinWithValidCallback() {
		$factory = $this->createSkinFactory();
		$factory->register( 'testfallback', 'TestFallback', static function () {
			return new SkinFallback();
		} );

		$skin = $factory->makeSkin( 'testfallback' );
		$this->assertInstanceOf( SkinFallback::class, $skin );
		$this->assertEquals( 'fallback', $skin->getSkinName() );
	}

	public function testMakeSkinWithValidSpec() {
		$serviceInstance = (object)[];

		$serviceContainer = $this->createMock( ContainerInterface::class );
		$serviceContainer->method( 'has' )->willReturn( true );
		$serviceContainer->method( 'get' )->willReturn( $serviceInstance );

		$args = [];
		$factory = $this->createSkinFactory( $serviceContainer );
		$factory->register( 'testfallback', 'TestFallback', [
			'factory' => static function ( $service, $options ) use ( &$args ) {
				$args = [ $service, $options ];
				return new SkinFallback();
			},
			'services' => [
				'testservice'
			]
		] );

		$skin = $factory->makeSkin( 'testfallback' );
		$this->assertInstanceOf( SkinFallback::class, $skin );
		$this->assertEquals( 'fallback', $skin->getSkinName() );
		$this->assertSame( 'testfallback', $args[1]['name'] );
		$this->assertSame( $serviceInstance, $args[0] );
	}

	public function testRegisterReplaces() {
		$factory = $this->createSkinFactory();

		$s1 = $this->createMock( Skin::class );
		$factory->register( 'foo', 'Skin 1',
			static function () use ( $s1 ) {
				return $s1;
			},
			true
		);
		$this->assertEquals( [ 'foo'  => 'Skin 1' ], $factory->getSkinNames() );
		$this->assertSame( $s1, $factory->makeSkin( 'foo' ) );
		$this->assertSame( [], $factory->getAllowedSkins(), 'skipped' );

		// Skippable state from previous register() call must not leak to replacement
		$s2 = $this->createMock( Skin::class );
		$factory->register( 'foo', 'Skin 2',
			static function () use ( $s2 ) {
				return $s2;
			}
		);
		$this->assertEquals( [ 'foo'  => 'Skin 2' ], $factory->getSkinNames() );
		$this->assertSame( $s2, $factory->makeSkin( 'foo' ) );
		$this->assertSame( [ 'foo'  => 'Skin 2' ], $factory->getAllowedSkins(), 'not skipped' );
	}

	public function testGetSkinNames() {
		$factory = $this->createSkinFactory();
		$factory->register( 'skin1', 'Skin1', [] );
		$factory->register( 'skin2', 'Skin2', [] );

		$names = $factory->getSkinNames();
		$this->assertEquals( 'Skin1', $names['skin1'] );
		$this->assertEquals( 'Skin2', $names['skin2'] );
	}

	public function testGetAllowedSkins() {
		$sf = $this->createSkinFactory( null, [ 'quux' ] );
		$sf->register( 'foo', 'Foo', [] );
		$sf->register( 'apioutput', 'ApiOutput', [], true );

		// Skippable state is unspecified here and must inherit from site config,
		// which we seeded with 'quux', and thus skipped from allowed skins.
		$sf->register( 'quux', 'Quux', [] );

		$sf->register( 'fallback', 'Fallback', [], true );
		$sf->register( 'bar', 'Barbar', [] );

		$this->assertEquals(
			[ 'foo' => 'Foo', 'bar' => 'Barbar' ],
			$sf->getAllowedSkins()
		);
	}

	public function testGetAllowedSkinsEmpty() {
		$sf = $this->createSkinFactory();
		$sf->register( 'apioutput', 'ApiOutput', [], true );
		$sf->register( 'fallback', 'Fallback', [], true );

		$this->assertEquals( [], $sf->getAllowedSkins() );
	}
}
