<?php

namespace Drupal\Tests\simpletest\Unit;

use Drupal\Tests\UnitTestCase;

/**
 * @requires extension curl
 * @coversDefaultClass \Drupal\simpletest\TestBase
 * @group simpletest
 * @group TestBase
 */
class TestBaseTest extends UnitTestCase {

  /**
   * Helper method for constructing a mock TestBase object.
   *
   * TestBase is abstract, so we have to mock it. We'll also
   * mock the storeAssertion() method so we don't need the database.
   *
   * @param string $test_id
   *   An identifying name for the mocked test.
   *
   * @return object
   *   Mock of Drupal\simpletest\TestBase.
   */
  public function getTestBaseForAssertionTests($test_id) {
    $mock_test_base = $this->getMockBuilder('Drupal\simpletest\TestBase')
      ->setConstructorArgs([$test_id])
      ->setMethods(['storeAssertion'])
      ->getMockForAbstractClass();
    // Override storeAssertion() so we don't need a database.
    $mock_test_base->expects($this->any())
      ->method('storeAssertion')
      ->will($this->returnValue(NULL));
    return $mock_test_base;
  }

  /**
   * Invoke methods that are protected or private.
   *
   * @param object $object
   *   Object on which to invoke the method.
   * @param string $method_name
   *   Name of the method to invoke.
   * @param array $arguments
   *   Array of arguments to be passed to the method.
   *
   * @return mixed
   *   Value returned by the invoked method.
   */
  public function invokeProtectedMethod($object, $method_name, array $arguments) {
    $ref_method = new \ReflectionMethod($object, $method_name);
    $ref_method->setAccessible(TRUE);
    return $ref_method->invokeArgs($object, $arguments);
  }

  /**
   * Provides data for the random string validation test.
   *
   * @return array
   *   - The expected result of the validation.
   *   - The string to validate.
   */
  public function providerRandomStringValidate() {
    return [
      [FALSE, ' curry paste'],
      [FALSE, 'curry paste '],
      [FALSE, 'curry  paste'],
      [FALSE, 'curry   paste'],
      [TRUE, 'curry paste'],
      [TRUE, 'thai green curry paste'],
      [TRUE, '@startswithat'],
      [TRUE, 'contains@at'],
    ];
  }

  /**
   * @covers ::randomStringValidate
   * @dataProvider providerRandomStringValidate
   */
  public function testRandomStringValidate($expected, $string) {
    $mock_test_base = $this->getMockForAbstractClass('Drupal\simpletest\TestBase');
    $actual = $mock_test_base->randomStringValidate($string);
    $this->assertEquals($expected, $actual);
  }

  /**
   * Provides data for testRandomString() and others.
   *
   * @return array
   *   - The number of items (characters, object properties) we expect any of
   *     the random functions to give us.
   */
  public function providerRandomItems() {
    return [
      [NULL],
      [0],
      [1],
      [2],
      [3],
      [4],
      [7],
    ];
  }

  /**
   * @covers ::randomString
   * @dataProvider providerRandomItems
   */
  public function testRandomString($length) {
    $mock_test_base = $this->getMockForAbstractClass('Drupal\simpletest\TestBase');
    $string = $mock_test_base->randomString($length);
    $this->assertEquals($length, strlen($string));
    // randomString() should always include an ampersand ('&')  and a
    // greater than ('>') if $length is greater than 3.
    if ($length > 4) {
      $this->assertContains('&', $string);
      $this->assertContains('>', $string);
    }
  }

  /**
   * @covers ::randomObject
   * @dataProvider providerRandomItems
   */
  public function testRandomObject($size) {
    $test_base = $this->getTestBaseForAssertionTests('test_id');
    // Note: count((array)object) works for now, maybe not later.
    $this->assertEquals($size, count((array) $test_base->randomObject($size)));
  }

  /**
   * @covers ::checkRequirements
   */
  public function testCheckRequirements() {
    $test_base = $this->getTestBaseForAssertionTests('test_id');
    $this->assertInternalType(
        'array',
        $this->invokeProtectedMethod($test_base, 'checkRequirements', [])
    );
  }

  /**
   * Data provider for testAssert().
   *
   * @return array
   *   Standard dataProvider array of arrays:
   *   - Expected result from assert().
   *   - Expected status stored in TestBase->assertions.
   *   - Status, passed to assert().
   *   - Message, passed to assert().
   *   - Group, passed to assert().
   *   - Caller, passed to assert().
   */
  public function providerAssert() {
    return [
      [TRUE, 'pass', TRUE, 'Yay pass', 'test', []],
      [FALSE, 'fail', FALSE, 'Boo fail', 'test', []],
      [TRUE, 'pass', 'pass', 'Yay pass', 'test', []],
      [FALSE, 'fail', 'fail', 'Boo fail', 'test', []],
      [FALSE, 'exception', 'exception', 'Boo fail', 'test', []],
      [FALSE, 'debug', 'debug', 'Boo fail', 'test', []],
    ];
  }

  /**
   * @covers ::assert
   * @dataProvider providerAssert
   */
  public function testAssert($expected, $assertion_status, $status, $message, $group, $caller) {
    $test_id = 'luke_i_am_your_' . $assertion_status;
    $test_base = $this->getTestBaseForAssertionTests($test_id);

    // Verify some startup values.
    $this->assertAttributeEmpty('assertions', $test_base);
    if (is_string($status)) {
      $this->assertEquals(0, $test_base->results['#' . $status]);
    }

    // assert() is protected so we have to make it accessible.
    $ref_assert = new \ReflectionMethod($test_base, 'assert');
    $ref_assert->setAccessible(TRUE);

    // Call assert() from within our hall of mirrors.
    $this->assertEquals(
        $expected,
        $ref_assert->invokeArgs($test_base,
          [$status, $message, $group, $caller]
        )
    );

    // Check the side-effects of assert().
    if (is_string($status)) {
      $this->assertEquals(1, $test_base->results['#' . $status]);
    }
    $this->assertAttributeNotEmpty('assertions', $test_base);
    // Make a ReflectionProperty for the assertions property,
    // since it's protected.
    $ref_assertions = new \ReflectionProperty($test_base, 'assertions');
    $ref_assertions->setAccessible(TRUE);
    $assertions = $ref_assertions->getValue($test_base);
    $assertion = reset($assertions);
    $this->assertEquals($assertion_status, $assertion['status']);
    $this->assertEquals($test_id, $assertion['test_id']);
    $this->assertEquals(get_class($test_base), $assertion['test_class']);
    $this->assertEquals($message, $assertion['message']);
    $this->assertEquals($group, $assertion['message_group']);
  }

  /**
   * Data provider for assertTrue().
   */
  public function providerAssertTrue() {
    return [
      [TRUE, TRUE],
      [FALSE, FALSE],
    ];
  }

  /**
   * @covers ::assertTrue
   * @dataProvider providerAssertTrue
   */
  public function testAssertTrue($expected, $value) {
    $test_base = $this->getTestBaseForAssertionTests('test_id');
    $this->assertEquals(
        $expected,
        $this->invokeProtectedMethod($test_base, 'assertTrue', [$value])
    );
  }

  /**
   * @covers ::assertFalse
   * @dataProvider providerAssertTrue
   */
  public function testAssertFalse($expected, $value) {
    $test_base = $this->getTestBaseForAssertionTests('test_id');
    $this->assertEquals(
        (!$expected),
        $this->invokeProtectedMethod($test_base, 'assertFalse', [$value])
    );
  }

  /**
   * Data provider for assertNull().
   */
  public function providerAssertNull() {
    return [
      [TRUE, NULL],
      [FALSE, ''],
    ];
  }

  /**
   * @covers ::assertNull
   * @dataProvider providerAssertNull
   */
  public function testAssertNull($expected, $value) {
    $test_base = $this->getTestBaseForAssertionTests('test_id');
    $this->assertEquals(
        $expected,
        $this->invokeProtectedMethod($test_base, 'assertNull', [$value])
    );
  }

  /**
   * @covers ::assertNotNull
   * @dataProvider providerAssertNull
   */
  public function testAssertNotNull($expected, $value) {
    $test_base = $this->getTestBaseForAssertionTests('test_id');
    $this->assertEquals(
        (!$expected),
        $this->invokeProtectedMethod($test_base, 'assertNotNull', [$value])
    );
  }

  /**
   * Data provider for tests of equality assertions.
   *
   * Used by testAssertIdentical(), testAssertEqual(), testAssertNotIdentical(),
   * and testAssertNotEqual().
   *
   * @return
   *   Array of test data.
   *   - Expected assertion value for identical comparison.
   *   - Expected assertion value for equal comparison.
   *   - First value to compare.
   *   - Second value to compare.
   */
  public function providerEqualityAssertions() {
    return [
      // Integers and floats.
      [TRUE, TRUE, 0, 0],
      [FALSE, TRUE, 0, 0.0],
      [FALSE, TRUE, '0', 0],
      [FALSE, TRUE, '0.0', 0.0],
      [FALSE, FALSE, 23, 77],
      [TRUE, TRUE, 23.0, 23.0],
      // Strings.
      [FALSE, FALSE, 'foof', 'yay'],
      [TRUE, TRUE, 'yay', 'yay'],
      // Bools with type conversion.
      [TRUE, TRUE, TRUE, TRUE],
      [TRUE, TRUE, FALSE, FALSE],
      [FALSE, TRUE, NULL, FALSE],
      [FALSE, TRUE, 'TRUE', TRUE],
      [FALSE, FALSE, 'FALSE', FALSE],
      [FALSE, TRUE, 0, FALSE],
      [FALSE, TRUE, 1, TRUE],
      [FALSE, TRUE, -1, TRUE],
      [FALSE, TRUE, '1', TRUE],
      [FALSE, TRUE, '1.3', TRUE],
      // Null.
      [FALSE, FALSE, 'NULL', NULL],
      [TRUE, TRUE, NULL, NULL],
    ];
  }

  /**
   * @covers ::assertIdentical
   * @dataProvider providerEqualityAssertions
   */
  public function testAssertIdentical($expected_identical, $expected_equal, $first, $second) {
    $test_base = $this->getTestBaseForAssertionTests('test_id');
    $this->assertEquals(
        $expected_identical,
        $this->invokeProtectedMethod($test_base, 'assertIdentical', [$first, $second])
    );
  }

  /**
   * @covers ::assertNotIdentical
   * @dataProvider providerEqualityAssertions
   */
  public function testAssertNotIdentical($expected_identical, $expected_equal, $first, $second) {
    $test_base = $this->getTestBaseForAssertionTests('test_id');
    $this->assertEquals(
        (!$expected_identical),
        $this->invokeProtectedMethod($test_base, 'assertNotIdentical', [$first, $second])
    );
  }

  /**
   * @covers ::assertEqual
   * @dataProvider providerEqualityAssertions
   */
  public function testAssertEqual($expected_identical, $expected_equal, $first, $second) {
    $test_base = $this->getTestBaseForAssertionTests('test_id');
    $this->assertEquals(
        $expected_equal,
        $this->invokeProtectedMethod($test_base, 'assertEqual', [$first, $second])
    );
  }

  /**
   * @covers ::assertNotEqual
   * @dataProvider providerEqualityAssertions
   */
  public function testAssertNotEqual($expected_identical, $expected_equal, $first, $second) {
    $test_base = $this->getTestBaseForAssertionTests('test_id');
    $this->assertEquals(
        (!$expected_equal),
        $this->invokeProtectedMethod($test_base, 'assertNotEqual', [$first, $second])
    );
  }

  /**
   * Data provider for testAssertIdenticalObject().
   */
  public function providerAssertIdenticalObject() {
    $obj1 = new \stdClass();
    $obj1->foof = 'yay';
    $obj2 = $obj1;
    $obj3 = clone $obj1;
    $obj4 = new \stdClass();
    return [
      [TRUE, $obj1, $obj2],
      [TRUE, $obj1, $obj3],
      [FALSE, $obj1, $obj4],
    ];
  }

  /**
   * @covers ::assertIdenticalObject
   * @dataProvider providerAssertIdenticalObject
   */
  public function testAssertIdenticalObject($expected, $first, $second) {
    $test_base = $this->getTestBaseForAssertionTests('test_id');
    $this->assertEquals(
      $expected,
      $this->invokeProtectedMethod($test_base, 'assertIdenticalObject', [$first, $second])
    );
  }

  /**
   * @covers ::pass
   */
  public function testPass() {
    $test_base = $this->getTestBaseForAssertionTests('test_id');
    $this->assertEquals(
        TRUE,
        $this->invokeProtectedMethod($test_base, 'pass', [])
    );
  }

  /**
   * @covers ::fail
   */
  public function testFail() {
    $test_base = $this->getTestBaseForAssertionTests('test_id');
    $this->assertEquals(
        FALSE,
        $this->invokeProtectedMethod($test_base, 'fail', [])
    );
  }

  /**
   * Data provider for testError().
   *
   * @return array
   *   - Expected status for assertion.
   *   - Group for use in assert().
   */
  public function providerError() {
    return [
      ['debug', 'User notice'],
      ['exception', 'Not User notice'],
    ];
  }

  /**
   * @covers ::error
   * @dataProvider providerError
   */
  public function testError($status, $group) {
    // Mock up a TestBase object.
    $mock_test_base = $this->getMockBuilder('Drupal\simpletest\TestBase')
      ->setMethods(['assert'])
      ->getMockForAbstractClass();

    // Set expectations for assert().
    $mock_test_base->expects($this->once())
      ->method('assert')
      // The first argument to assert() should be the expected $status. This is
      // the most important expectation of this test.
      ->with($status)
      // Arbitrary return value.
      ->willReturn("$status:$group");

    // Invoke error().
    $this->assertEquals(
        "$status:$group",
        $this->invokeProtectedMethod($mock_test_base, 'error', ['msg', $group])
    );
  }

  /**
   * @covers ::getRandomGenerator
   */
  public function testGetRandomGenerator() {
    $test_base = $this->getTestBaseForAssertionTests('test_id');
    $this->assertInstanceOf(
        'Drupal\Component\Utility\Random',
        $this->invokeProtectedMethod($test_base, 'getRandomGenerator', [])
    );
  }

}
