<?php

namespace SMW\Tests\Utils\JSONScript;

use Article;
use MediaWiki\Context\RequestContext;
use MediaWiki\User\User;
use MediaWikiIntegrationTestCase;
use RuntimeException;
use SMW\DIWikiPage;
use SMW\SerializerFactory;
use SMW\Services\ServicesFactory;
use SMW\Services\ServicesFactory as ApplicationFactory;
use SMW\Store;
use SMW\Tests\Utils\PageReader;
use SMW\Tests\Utils\UtilityFactory;
use SMW\Tests\Utils\Validators\IncomingSemanticDataValidator;
use SMW\Tests\Utils\Validators\SemanticDataValidator;
use SMW\Tests\Utils\Validators\StringValidator;

/**
 * @group semantic-mediawiki
 * @group Database
 * @group medium
 *
 * @license GPL-2.0-or-later
 * @since 2.3
 *
 * @author mwjames
 */
class ParserTestCaseProcessor extends MediaWikiIntegrationTestCase {

	/**
	 * @var Store
	 */
	private $store;

	/**
	 * @var SemanticDataValidator
	 */
	private $semanticDataValidator;

	/**
	 * @var IncomingSemanticDataValidator
	 */
	private $incomingSemanticDataValidator;

	/**
	 * @var StringValidator
	 */
	private $stringValidator;

	/**
	 * @var PageReader
	 */
	private $pageReader;

	/**
	 * @var User
	 */
	private $superUser;

	/**
	 * @var SerializerFactory
	 */
	private $serializerFactory;

	/**
	 * @var bool
	 */
	private $debug = false;

	/**
	 * @param Store
	 * @param SemanticDataValidator
	 * @param IncomingSemanticDataValidator
	 * @param StringValidator
	 */
	public function __construct( $store, $semanticDataValidator, $incomingSemanticDataValidator, $stringValidator ) {
		$this->store = $store;
		$this->semanticDataValidator = $semanticDataValidator;
		$this->incomingSemanticDataValidator = $incomingSemanticDataValidator;
		$this->stringValidator = $stringValidator;
		$this->pageReader = UtilityFactory::getInstance()->newPageReader();
		$this->superUser = UtilityFactory::getInstance()->newMockSuperUser();
		$this->serializerFactory = ApplicationFactory::getInstance()->newSerializerFactory();
	}

	/**
	 * @since 2.2
	 *
	 * @param bool $debugMode
	 */
	public function setDebugMode( $debugMode ) {
		$this->debug = $debugMode;
	}

	/**
	 * @since 2.2
	 *
	 * @param array $case
	 */
	public function process( array $case ) {
		if ( !isset( $case['subject'] ) ) {
			return;
		}

		$this->assertSemanticDataForCase(
			$case
		);

		$this->assertTextFromParserOutputForCase(
			$case
		);

		$this->assertTextFromParsedMsgForCase(
			$case
		);
	}

	private function assertSemanticDataForCase( $case ) {
		// Allows for data to be re-read from the DB instead of being fetched
		// from the store-id-cache
		if ( isset( $case['store']['clear-cache'] ) && $case['store']['clear-cache'] ) {
			$this->store->clear();
		}

		if ( !isset( $case['assert-store'] ) || !isset( $case['assert-store']['semantic-data'] ) ) {
			return;
		}

		$subject = $this->getSubjectFrom( $case, false );
		$semanticData = $this->store->getSemanticData( $subject );

		if ( $this->debug ) {
			print_r(
				$this->serializerFactory->newSemanticDataSerializer()->serialize( $semanticData )
			);
		}

		if ( isset( $case['errors'] ) && $case['errors'] !== [] ) {
			$this->assertNotEmpty(
				$semanticData->getErrors()
			);
		}

		$this->semanticDataValidator->assertThatPropertiesAreSet(
			$case['assert-store']['semantic-data'],
			$semanticData,
			$case['about']
		);

		if ( !isset( $case['assert-store']['semantic-data']['incoming'] ) ) {
			return;
		}

		$this->incomingSemanticDataValidator->assertThatIncomingDataAreSet(
			$case['assert-store']['semantic-data']['incoming'],
			$subject,
			$case['about']
		);
	}

	private function assertTextFromParserOutputForCase( $case ) {
		if ( !isset( $case['assert-output'] ) ) {
			return;
		}

		$title = $this->getSubjectFrom( $case )->getTitle();

		$parserOutput = $this->pageReader->getParserOutputFromEdit(
			$title
		);

		if ( isset( $case['assert-output']['onOutputPage'] ) && $case['assert-output']['onOutputPage'] ) {
			$context = new RequestContext();
			$context->setTitle( $title );
			// Ensures the OutputPageBeforeHTML hook is run
			$context->getOutput()->addParserOutput( $parserOutput );
			$output = $context->getOutput()->getHtml();
		} elseif ( isset( $case['assert-output']['onPageView'] ) ) {
			$parameters = isset( $case['assert-output']['onPageView']['parameters'] ) ? $case['assert-output']['onPageView']['parameters'] : [];
			$context = RequestContext::newExtraneousContext(
				$title,
				$parameters
			);

			// Avoid "... PermissionsError: The action you have requested is
			// limited to users in the group ..."
			$context->setUser( $this->superUser );

			Article::newFromTitle( $title, $context )->view();
			$output = $context->getOutput()->getHtml();
		} else {
			$output = $parserOutput->getText();
		}

		if ( isset( $case['assert-output']['include-head-items'] ) && $case['assert-output']['include-head-items'] ) {
			$output .= '<include-head-items>' . implode( '', $parserOutput->getHeadItems() ) . '</include-head-items>';
		}

		// Strip HTML comments
		$output = preg_replace( '/<!--(.*)-->/Uis', '', $output );

		if ( isset( $case['assert-output']['to-contain'] ) ) {
			$contains = $case['assert-output']['to-contain'];

			// Concatenate to a string to ensure we keep the sequence as entered
			// while adding .* to signal that anything goes between them except
			// for the string expected to be asserted
			if ( isset( $case['assert-output']['in-sequence' ] ) && is_array( $contains ) ) {
				$contains = implode( '.*', $contains );
			}

			$this->stringValidator->assertThatStringContains(
				$contains,
				$output,
				$case['about']
			);
		}

		if ( isset( $case['assert-output']['not-contain'] ) ) {
			$this->stringValidator->assertThatStringNotContains(
				$case['assert-output']['not-contain'],
				$output,
				$case['about']
			);
		}
	}

	private function assertTextFromParsedMsgForCase( $case ) {
		if ( !isset( $case['assert-msgoutput'] ) ) {
			return;
		}

		$mediaWikiNsContentReader = ServicesFactory::getInstance()->create( 'MediaWikiNsContentReader' );
		$mediaWikiNsContentReader->skipMessageCache();

		$text = $mediaWikiNsContentReader->read( $case['subject'] );
		$text = wfMessage( 'smw-parse', $text )->parse();

		if ( isset( $case['assert-msgoutput']['to-contain'] ) ) {
			$this->stringValidator->assertThatStringContains(
				$case['assert-msgoutput']['to-contain'],
				$text,
				$case['about']
			);
		}

		if ( isset( $case['assert-msgoutput']['not-contain'] ) ) {
			$this->stringValidator->assertThatStringNotContains(
				$case['assert-msgoutput']['not-contain'],
				$text,
				$case['about']
			);
		}
	}

	private function getSubjectFrom( $case, $checkExists = true ) {
		$subject = DIWikiPage::newFromText(
			$case['subject'],
			isset( $case['namespace'] ) ? constant( $case['namespace'] ) : NS_MAIN
		);

		$title = $subject->getTitle();

		if ( $subject->getNamespace() === SMW_NS_PROPERTY ) {
			$checkExists = false;
		}

		if ( $title === null ) {
			throw new RuntimeException( 'Could not create Title object for subject page "' . $case['subject'] . '".' );
		}

		if ( $checkExists && !$title->exists() ) {
			throw new RuntimeException( 'Subject page "' . $case['subject'] . '" does not exist.' );
		}

		return $subject;
	}

}
