<?php

/**
 * @file
 * Tests for the token module.
 */

/**
 * Helper test class with some added functions for testing.
 */
class TokenTestHelper extends DrupalWebTestCase {
  function setUp(array $modules = array()) {
    $modules[] = 'path';
    $modules[] = 'token';
    $modules[] = 'token_test';
    parent::setUp($modules);

    // Clear the token static cache.
    token_get_values('reset');
  }

  function assertToken($type, $object, $token, $expected, array $options = array()) {
    $this->assertTokens($type, $object, array($token => $expected), $options);
  }

  function assertTokens($type, $object, array $tokens, array $options = array()) {
    $values = token_get_values($type, $object, FALSE, $options);
    $values = array_combine($values->tokens, $values->values);
    foreach ($tokens as $token => $expected) {
      if (!isset($expected)) {
        $this->assertTrue(!isset($values[$token]), t("Token value for [@token] was not generated.", array('@token' => $token)));
      }
      elseif (!isset($values[$token])) {
        $this->fail(t("Token value for [@token] was not generated.", array('@token' => $token)));
      }
      else {
        $this->assertIdentical($values[$token], $expected, t("Token value for [@token] was '@actual', expected value '@expected'.", array('@token' => $token, '@actual' => $values[$token], '@expected' => $expected)));
      }
    }
  }

  /**
   * Make a page request and test for token generation.
   */
  function assertPageTokens($url, array $tokens, array $data = array('global' => NULL), array $options = array()) {
    if (empty($tokens)) {
      return TRUE;
    }

    $token_page_tokens = array(
      'tokens' => $tokens,
      'data' => $data,
      'options' => $options,
    );
    variable_set('token_page_tokens', $token_page_tokens);

    $options += array('url_options' => array());
    $this->drupalGet($url, $options['url_options']);
    $this->refreshVariables();
    $result = variable_get('token_page_tokens', array());

    if (!isset($result['values']) || !is_array($result['values'])) {
      return $this->fail('Failed to generate tokens.');
    }

    foreach ($tokens as $token => $expected) {
      if (!isset($expected)) {
        $this->assertTrue(!isset($result['values'][$token]) || $result['values'][$token] === $token, t("Token value for @token was not generated.", array('@token' => $token)));
      }
      elseif (!isset($result['values'][$token])) {
        $this->fail(t('Failed to generate token @token.', array('@token' => $token)));
      }
      else {
        $this->assertIdentical($result['values'][$token], (string) $expected, t("Token value for @token was '@actual', expected value '@expected'.", array('@token' => $token, '@actual' => $result['values'][$token], '@expected' => $expected)));
      }
    }
  }
}

class TokenUnitTestCase extends TokenTestHelper {
  public static function getInfo() {
    return array(
      'name' => 'Token unit tests',
      'description' => 'Test basic, low-level token functions.',
      'group' => 'Token',
    );
  }

  /**
   * Test token_get_invalid_tokens() and token_get_invalid_tokens_by_context().
   */
  public function testGetInvalidTokens() {
    $tests = array();
    $tests[] = array(
      'valid tokens' => array(
        '[title-raw]',
        '[yyyy]',
        '[mod-yyyy]',
        '[site-name]',
        '[site-slogan]',
        '[user-id]',
      ),
      'invalid tokens' => array(
        '[title-invalid]',
        '[invalid]',
        '[mod-invalid]',
        '[invalid-title]',
        '[site-invalid]',
        '[uid]',
        '[comment-cid]',
      ),
      'types' => array('node'),
    );
    $tests[] = array(
      'valid tokens' => array(
        '[title-raw]',
        '[yyyy]',
        '[mod-yyyy]',
        '[site-name]',
        '[site-slogan]',
        '[user-id]',
        '[uid]',
        '[comment-cid]',
      ),
      'invalid tokens' => array(
        '[title-invalid]',
        '[invalid]',
        '[mod-invalid]',
        '[invalid-title]',
        '[site-invalid]',
      ),
      'types' => array('all'),
    );
    $tests[] = array(
      'valid tokens' => array(
        '[alpha]',
        '[beta-1]',
        '[beta-2]',
        '[gamma_A]',
        '[delta-extra]',
        '[epsilon-zeta-A]',
      ),
      'invalid tokens' => array(
        '[alpha-plus]',
        '[beta]',
        '[beta-]',
        '[beta_]',
        '[beta_1]',
        '[beta-A]',
        '[gamma]',
        '[gamma_]',
        '[gamma-A]',
        '[delta]',
        '[epsilon-zeta-]',
      ),
      'types' => array('all'),
    );

    foreach ($tests as $test) {
      $tokens = array_merge($test['valid tokens'], $test['invalid tokens']);
      shuffle($tokens);

      $invalid_tokens = token_get_invalid_tokens_by_context(implode(' ', $tokens), $test['types']);

      sort($invalid_tokens);
      sort($test['invalid tokens']);
      $this->assertEqual($invalid_tokens, $test['invalid tokens'], 'Invalid tokens detected properly: ' . implode(', ', $invalid_tokens));
    }
  }

  /**
   * Test the $options['clear'] parameter for token_replace().
   */
  public function testClearOption() {
    $tests[] = array(
      'input' => 'Foo [site-name][invalid-token] bar [another-invalid-token] [invalid-token]',
      'output' => 'Foo Drupal bar  ',
      'options' => array('clear' => TRUE),
    );
    $tests[] = array(
      'input' => 'Foo [site-name][invalid-token] bar [another-invalid-token] [invalid-token]',
      'output' => 'Foo Drupal[invalid-token] bar [another-invalid-token] [invalid-token]',
      'options' => array(),
    );

    foreach ($tests as $test) {
      $output = token_replace($test['input'], 'global', NULL, TOKEN_PREFIX, TOKEN_SUFFIX, $test['options']);
      $this->assertIdentical($output, $test['output']);
    }
  }

  /**
   * Test whether token-replacement works in various contexts.
   *
   * @see http://drupal.org/node/733192
   */
  function testSystemTokenRecognition() {
    global $language;

    // Generate prefixes and suffixes for the token context.
    $tests = array(
      array('prefix' => 'this is the ', 'suffix' => ' site'),
      array('prefix' => 'this is the', 'suffix' => 'site'),
      array('prefix' => '[', 'suffix' => ']'),
      array('prefix' => '', 'suffix' => ']]]'),
      array('prefix' => '[[[', 'suffix' => ''),
      array('prefix' => ':[:', 'suffix' => '--]'),
      array('prefix' => '-[-', 'suffix' => ':]:'),
      array('prefix' => '[:', 'suffix' => ']'),
      array('prefix' => '[site:', 'suffix' => ':name]'),
      array('prefix' => '[site:', 'suffix' => ']'),
    );

    // Check if the token is recognized in each of the contexts.
    foreach ($tests as $test) {
      $input = $test['prefix'] . '[site-name]' . $test['suffix'];
      $expected = $test['prefix'] . 'Drupal' . $test['suffix'];
      $output = token_replace($input);
      $this->assertEqual($output, $expected);
    }
  }

  /**
   * Test token caching.
   */
  function testTokenCaching() {
    // Run global tokens once so that the cache is primed.
    $tokens = array(
      'option-foo' => '',
    );
    $this->assertTokens('global', NULL, $tokens);

    // Run global tokens again with different options. This should return a
    // different value for the [option-foo] token.
    $tokens = array(
      'option-foo' => 'bar',
    );
    $this->assertTokens('global', NULL, $tokens, array('foo' => 'bar'));
  }

  /**
   * Test the token_scan() function.
   */
  function testTokenScan() {
    $tests = array(
      array('text' => 'Test [foo] [[bar]] test.', 'tokens' => array('foo', 'bar')),
      array('text' => 'Test [foo] [] test.',    'tokens' => array('foo')),
      array('text' => 'Test [foo][] test.',     'tokens' => array('foo')),
      array('text' => 'Test [foo][bar] test.',  'tokens' => array('foo', 'bar')),
      // Test the e-mail token syntax.
      array('text' => 'Test %foo %%bar test.',   'tokens' => array('foo', 'bar'), 'leading' => '%', 'trailing' => ''),
      array('text' => 'Test %foo % test.',      'tokens' => array('foo'),        'leading' => '%', 'trailing' => ''),
      array('text' => 'Test %foo% test.',       'tokens' => array('foo'),        'leading' => '%', 'trailing' => ''),
      array('text' => 'Test %foo%%bar test.',    'tokens' => array('foo', 'bar'), 'leading' => '%', 'trailing' => ''),
      // Test the rules token syntax.
      array('text' => 'Test [global:foo] [global:bar] test.', 'tokens' => array('foo', 'bar'), 'leading' => '[global:'),
      array('text' => 'Test [node:foo] [node:] test.', 'tokens' => array('foo'), 'leading' => '[node:'),
      array('text' => 'Test [node:foo][node:] test.', 'tokens' => array('foo'), 'leading' => '[node:'),
      array('text' => 'Test [node:foo][node:bar] test.', 'tokens' => array('foo', 'bar'), 'leading' => '[node:'),
    );
    foreach ($tests as $test) {
      $test += array('leading' => TOKEN_PREFIX, 'trailing' => TOKEN_SUFFIX);
      $this->assertEqual(token_scan($test['text'], $test['leading'], $test['trailing']), $test['tokens']);
    }
  }
}

class TokenNodeTestCase extends TokenTestHelper {
  public static function getInfo() {
    return array(
      'name' => 'Node token tests',
      'description' => 'Test the node tokens.',
      'group' => 'Token',
    );
  }

  function testNodeTokens() {
    $time = time();
    $created = gmmktime(0, 0, 0, 11, 19, 1978);
    $changed = gmmktime(0, 0, 0, 7, 4, 1984);
    $node = $this->drupalCreateNode(array(
      'type' => 'page',
      'language' => 'und',
      'created' => $created,
      'log' => '<blink>' . $this->randomName() . '</blink>',
    ));
    $node->changed = $changed;
    path_set_alias('node/' . $node->nid, 'content/first-node');

    $tokens = array(
      'nid' => $node->nid,
      'type' => 'page',
      'type-name' => 'Page',
      'language' => 'und',
      'node-path' => 'content/first-node',
      'node-url' => url('node/' . $node->nid, array('absolute' => TRUE)),
      'small' => '11/19/1978 - 00:00',
      'yyyy' => '1978',
      'yy' => '78',
      'month' => 'November',
      'mon' => 'Nov',
      'mm' => '11',
      'm' => '11',
      'ww' => '46',
      'date' => '7',
      'day' => 'Sunday',
      'ddd' => 'Sun',
      'dd' => '19',
      'd' => '19',
      'raw' => 280281600,
      'since' => format_interval($time - 280281600),
      'mod-small' => '07/04/1984 - 00:00',
      'mod-yyyy' => '1984',
      'mod-yy' => '84',
      'mod-month' => 'July',
      'mod-mon' => 'Jul',
      'mod-mm' => '07',
      'mod-m' => '7',
      'mod-ww' => '27',
      'mod-date' => '3',
      'mod-day' => 'Wednesday',
      'mod-ddd' => 'Wed',
      'mod-dd' => '04',
      'mod-d' => '4',
      'mod-raw' => 457747200,
      'mod-since' => format_interval($time - 457747200),
      'log' => filter_xss($node->log),
      'log-raw' => $node->log,
    );
    $this->assertTokens('node', $node, $tokens);

    // Check that a new revision of a node returns different tokens.
    $node->revision = TRUE;
    $node->title = 'New revision';
    node_save($node);
    $this->assertTokens('node', $node, array('title' => 'New revision'));
  }
}

class TokenCommentTestCase extends TokenTestHelper {
  protected $node;

  public static function getInfo() {
    return array(
      'name' => 'Comment token tests',
      'description' => 'Test the comment tokens.',
      'group' => 'Token',
    );
  }

  function setUp(array $modules = array()) {
    $modules[] = 'comment';
    parent::setUp($modules);
    $this->node = $this->drupalCreateNode(array('comment' => 2));
  }

  function loadComment($cid) {
    return db_fetch_object(db_query('SELECT c.cid, c.pid, c.nid, c.subject, c.comment, c.format, c.timestamp, c.name, c.mail, c.homepage, u.uid, u.name AS registered_name, u.signature, u.signature_format, u.picture, u.data, c.status FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.cid = %d', $cid));
  }

  function createComment(array $comment) {
    $comment += array(
      'cid' => 0,
      'nid' => $this->node->nid,
      'pid' => 0,
      'uid' => 0,
      'subject' => $this->randomName(),
      'comment' => $this->randomName(64),
      'format' => 1,
      'timestamp' => gmmktime(0, 0, 0, 7, 4, 1984),
      'status' => COMMENT_PUBLISHED,
    );

    $cid = comment_save($comment);
    return $this->loadComment($cid);
  }

  function testCommentTokens() {
    $time = time();
    $comment = $this->createComment(array(
      'timestamp' => gmmktime(0, 0, 0, 7, 4, 1984),
    ));

    $tokens = array(
      'comment-cid' => $comment->cid,
      'comment-nid' => $this->node->nid,
      'comment-yyyy' => '1984',
      'comment-yy' => '84',
      'comment-month' => 'July',
      'comment-mon' => 'Jul',
      'comment-mm' => '07',
      'comment-m' => '7',
      'comment-ww' => '27',
      'comment-date' => '3',
      'comment-day' => 'Wednesday',
      'comment-ddd' => 'Wed',
      'comment-dd' => '04',
      'comment-d' => '4',
      'comment-raw' => '457747200',
      'comment-since' => format_interval($time - 457747200),
      'comment-node-title' => check_plain($this->node->title),
      'comment-node-title-raw' => $this->node->title,
    );
    $this->assertTokens('comment', $comment, $tokens);

  }
}

class TokenTaxonomyTestCase extends TokenTestHelper {
  protected $vocabulary;

  public static function getInfo() {
    return array(
      'name' => 'Taxonomy and vocabulary token tests',
      'description' => 'Test the taxonomy tokens.',
      'group' => 'Token',
    );
  }

  function setUp(array $modules = array()) {
    $modules[] = 'taxonomy';
    parent::setUp($modules);
    // Reset the static taxonomy.module caches.
    taxonomy_vocabulary_load(0, TRUE);
    taxonomy_get_term(0, TRUE);
  }

  function addVocabulary(array $vocabulary = array()) {
    $vocabulary += array(
      'name' => drupal_strtolower($this->randomName(5)),
      'nodes' => array('story' => 'story'),
    );
    taxonomy_save_vocabulary($vocabulary);
    return (object) $vocabulary;
  }

  function addTerm(stdClass $vocabulary, array $term = array()) {
    $term += array(
      'name' => drupal_strtolower($this->randomName(5)),
      'vid' => $vocabulary->vid,
    );
    taxonomy_save_term($term);
    return (object) $term;
  }

  function testTaxonomyTokens() {
    $vocabulary = $this->addVocabulary(array(
      'name' => '<blink>Vocab Name</blink>',
      'description' => '<blink>Vocab Description</blink>',
    ));
    $term = $this->addTerm($vocabulary, array(
      'name' => '<blink>Term Name</blink>',
      'description' => '<blink>Term Description</blink>',
    ));

    $tokens = array(
      'tid' => $term->tid,
      'cat' => check_plain($term->name),
      'cat-raw' => $term->name,
      'cat-description' => 'Term Description',
      'vid' => $vocabulary->vid,
      'vocab' => check_plain($vocabulary->name),
      'vocab-raw' => $vocabulary->name,
      'vocab-description' => 'Vocab Description',
      'vocab-description-raw' => $vocabulary->description,
    );
    $this->assertTokens('taxonomy', $term, $tokens);

    $tokens = array(
      'vocabulary-vid' => $vocabulary->vid,
      'vocabulary-name' => check_plain($vocabulary->name),
      'vocabulary-name-raw' => $vocabulary->name,
      'vocabulary-description' => 'Vocab Description',
      'vocabulary-description-raw' => $vocabulary->description,
    );
    $this->assertTokens('vocabulary', $vocabulary, $tokens);
  }
}

class TokenMenuTestCase extends TokenTestHelper {
  public static function getInfo() {
    return array(
      'name' => 'Menu token tests',
      'description' => 'Test the menu tokens.',
      'group' => 'Token',
    );
  }

  function setUp(array $modules = array()) {
    $modules[] = 'menu';
    parent::setUp($modules);
  }

  function testMenuTokens() {
    $root_link = array(
      'link_path' => 'root',
      'link_title' => 'Root link',
      'menu_name' => 'primary-links',
    );
    menu_link_save($root_link);

    // Add another link with the root link as the parent
    $parent_link = array(
      'link_path' => 'root/parent',
      'link_title' => 'Parent link',
      'menu_name' => 'primary-links',
      'plid' => $root_link['mlid'],
    );
    menu_link_save($parent_link);

    $node_link = array(
      'enabled' => TRUE,
      'link_title' => 'Node link',
      'plid' => $parent_link['mlid'],
      'customized' => 0,
    );
    $node = $this->drupalCreateNode(array('menu' => $node_link));

    // Test [node:menu] tokens.
    $tokens = array(
      'menu' => 'Primary links',
      'menu-raw' => 'Primary links',
      'menupath' => 'Root link/Parent link/Node link',
      'menupath-raw' => 'Root link/Parent link/Node link',
      'menu-link-title' => 'Node link',
      'menu-link-title-raw' => 'Node link',
      'menu-link-mlid' => $node->menu['mlid'],
      'menu-link-plid' => $node->menu['plid'],
      'menu-link-plid' => $parent_link['mlid'],
      'menu-link-parent-path' => 'root/parent',
      'menu-link-parent-path-raw' => 'root/parent',
    );
    $this->assertTokens('node', $node, $tokens);

    // Reload the node which will not have $node->menu defined and re-test.
    $loaded_node = node_load($node->nid);
    // We have to reset the token static cache because tokens are cached by
    // node ID only and not if the node object has changed.
    $this->assertTokens('node', $loaded_node, $tokens, array('reset' => TRUE));

    // Regression test for http://drupal.org/node/1317926 to ensure the
    // original node object is not changed when calling menu_node_prepare().
    $this->assertTrue(!isset($loaded_node->menu), t('The $node->menu property was not modified during token replacement.'), 'Regression');

  }
}

/*
 * Unit tests for the book tokens provided by Pathauto.
 */
class TokenBookTestCase extends TokenTestHelper {
  public static function getInfo() {
    return array(
      'name' => 'Book tokens',
      'description' => 'Tests the book tokens.',
      'group' => 'Token',
    );
  }

  function setUp(array $modules = array()) {
    $modules[] = 'book';
    $modules[] = 'menu';
    parent::setUp($modules);

    variable_set('book_allowed_types', array('book', 'page'));
  }

  function testBookTokens() {
    // Add a non-book node.
    $non_book_node = $this->drupalCreateNode(array('type' => 'book'));
    $tokens = array(
      'book' => '',
      'book-raw' => '',
      'book_id' => '',
      'bookpath' => '',
      'bookpath-raw' => '',
    );
    $this->assertTokens('node', $non_book_node, $tokens);

    // Add a root book page.
    $parent_node = $this->drupalCreateNode(array(
      'type' => 'book',
      'title' => 'Root',
      'book' => array('bid' => 'new') + _book_link_defaults('new'),
    ));
    $tokens = array(
      'book' => 'Root',
      'book-raw' => 'Root',
      'book_id' => $parent_node->book['bid'],
      'bookpath' => '',
      'bookpath-raw' => '',
      // Check that even those book menu links have been created for this node,
      // that the menu links still return nothing.
      'menu' => '',
      'menupath' => '',
      'menu-link-title' => '',
      'menu-link-title-raw' => '',
      'menu-link-mlid' => '',
      'menu-link-plid' => '',
      'menu-link-parent-path' => '',
    );
    $this->assertTokens('node', $parent_node, $tokens);

    // Add a first child page.
    $child_node1 = $this->drupalCreateNode(array(
      'type' => 'book',
      'title' => 'Sub page1',
      'book' => array(
        'bid' => $parent_node->book['bid'],
        'plid' => $parent_node->book['mlid'],
      ) + _book_link_defaults('new'),
    ));
    $tokens = array(
      'book' => 'Root',
      'book-raw' => 'Root',
      'book_id' => $parent_node->book['bid'],
      'bookpath' => 'Root',
      'bookpath-raw' => 'Root',
    );
    $this->assertTokens('node', $child_node1, $tokens);

    // Add a second child page.
    $child_node2 = $this->drupalCreateNode(array(
      'type' => 'book',
      'title' => 'Sub page2',
      'book' => array(
        'bid' => $parent_node->book['bid'],
        'plid' => $parent_node->book['mlid'],
      ) + _book_link_defaults('new'),
    ));
    $tokens = array(
      'book' => 'Root',
      'book-raw' => 'Root',
      'book_id' => $parent_node->book['bid'],
      'bookpath' => 'Root',
      'bookpath-raw' => 'Root',
    );
    $this->assertTokens('node', $child_node2, $tokens);

    // Add a child page on an existing child page.
    $sub_child_node1 = $this->drupalCreateNode(array(
      'type' => 'page',
      'title' => 'Sub-sub Page1',
      'book' => array(
        'bid' => $parent_node->book['bid'],
        'plid' => $child_node1->book['mlid'],
      ) + _book_link_defaults('new'),
    ));
    $tokens = array(
      'book' => 'Root',
      'book-raw' => 'Root',
      'book_id' => $parent_node->book['bid'],
      'bookpath' => 'Root/Sub page1',
      'bookpath-raw' => 'Root/Sub page1',
    );
    $this->assertTokens('node', $sub_child_node1, $tokens);
  }
}

/**
 * Test the current page tokens.
 */
class TokenCurrentPageTestCase extends TokenTestHelper {
  public static function getInfo() {
    return array(
      'name' => 'Current page token tests',
      'description' => 'Test the current page tokens.',
      'group' => 'Token',
    );
  }

  function testCurrentPageTokens() {
    $tokens = array(
      '[current-page-title]' => '',
      '[current-page-path]' => 'node',
      '[current-page-url]' => url('node', array('absolute' => TRUE)),
      '[current-page-number]' => 1,
    );
    $this->assertPageTokens('', $tokens);

    $node = $this->drupalCreateNode(array('title' => 'Node title', 'path' => 'node-alias'));
    $tokens = array(
      '[current-page-title]' => 'Node title',
      '[current-page-path]' => 'node-alias',
      '[current-page-url]' => url("node/{$node->nid}", array('absolute' => TRUE)),
      '[current-page-number]' => 1,
    );
    $this->assertPageTokens("node/{$node->nid}", $tokens);
  }
}
