<?php

/**
 * @file
 * Tax tests.
 */

/**
 * Tests the tax functionality.
 */
class UbercartTaxesTestCase extends UbercartTestHelper {

  public static function getInfo() {
    return array(
      'name' => 'Taxes',
      'description' => 'Ensures that taxes are calculated, stored and displayed correctly.',
      'group' => 'Ubercart',
    );
  }

  /**
   * Overrides DrupalWebTestCase::setUp().
   */
  function setUp() {
    $modules = array('uc_product_kit', 'uc_attribute', 'uc_cart', 'uc_payment', 'uc_payment_pack', 'uc_taxes');
    $permissions = array('bypass node access', 'administer content types', 'administer rules', 'configure taxes');
    parent::setUp($modules, $permissions);
  }

  function testInclusiveTaxes() {
    $this->drupalLogin($this->adminUser);

    // Create a 20% inclusive tax rate.
    $rate = (object) array(
      'name' => $this->randomName(8),
      'rate' => 0.2,
      'taxed_product_types' => array('product'),
      'taxed_line_items' => array(),
      'weight' => 0,
      'shippable' => 0,
      'display_include' => 1,
      'inclusion_text' => $this->randomName(6),
    );
    uc_taxes_rate_save($rate);

    // Ensure Rules picks up the new condition.
    entity_flush_caches();

    // Create a $10 product.
    $product = $this->createProduct(array(
      'sell_price' => 10,
    ));

    // Create an attribute.
    $attribute = (object) array(
      'name' => $this->randomName(8),
      'label' => $this->randomName(8),
      'description' => $this->randomName(8),
      'required' => TRUE,
      'display' => 1,
      'ordering' => 0,
    );
    uc_attribute_save($attribute);

    // Create an option with a price adjustment of $5.
    $option = (object) array(
      'aid' => $attribute->aid,
      'name' => $this->randomName(8),
      'cost' => 0,
      'price' => 5,
      'weight' => 0,
      'ordering' => 0,
    );
    uc_attribute_option_save($option);

    // Attach the attribute to the product.
    $attribute = uc_attribute_load($attribute->aid);
    uc_attribute_subject_save($attribute, 'product', $product->nid, TRUE);

    // Create a product kit containing the product.
    $kit = $this->drupalCreateNode(array(
      'type' => 'product_kit',
      'products' => array($product->nid),
      'ordering' => 0,
      'mutable' => UC_PRODUCT_KIT_UNMUTABLE_WITH_LIST,
      'default_qty' => 1,
    ));

    // Set the kit total to $9 to automatically apply a discount.
    $kit = node_load($kit->nid);
    $kit->kit_total = 9;
    node_save($kit);
    $kit = node_load($kit->nid);
    $this->assertEqual($kit->products[$product->nid]->discount, -1, 'Product kit component has correct discount applied.');

    // Ensure the price is displayed tax-inclusively on the add-to-cart form.
    $this->drupalGet('node/' . $kit->nid);
    $this->assertText('$10.80' . $rate->inclusion_text, 'Tax inclusive price on node-view form is accurate.'); // $10.80 = $9.00 + 20%
    $this->assertRaw($option->name . ', +$6.00</option>', 'Tax inclusive option price on node view form is accurate.'); // $6.00 = $5.00 + 20%

    // Add the product kit to the cart, selecting the option.
    $attribute_key = 'products[' . $product->nid . '][attributes][' . $attribute->aid . ']';
    $this->drupalPost('node/' . $kit->nid, array($attribute_key => $option->oid), t('Add to cart'));

    // Check that the subtotal is $16.80 ($10 base + $5 option - $1 discount, with 20% tax)
    $this->drupalGet('cart');
    $this->assertText('Subtotal: $16.80', 'Order subtotal is correct on cart page.');

    // Make sure that the subtotal is also correct on the checkout page.
    $this->drupalPost('cart', array(), 'Checkout');
    $this->assertText('Subtotal: $16.80', 'Order subtotal is correct on checkout page.');

    // Manually proceed to checkout review.
    $zone_id = db_query_range('SELECT zone_id FROM {uc_zones} WHERE zone_country_id = :country ORDER BY rand()', 0, 1, array('country' => variable_get('uc_store_country', 840)))->fetchField();
    $edit = array(
      'panes[delivery][delivery_first_name]' => $this->randomName(10),
      'panes[delivery][delivery_last_name]' => $this->randomName(10),
      'panes[delivery][delivery_street1]' => $this->randomName(10),
      'panes[delivery][delivery_city]' => $this->randomName(10),
      'panes[delivery][delivery_zone]' => $zone_id,
      'panes[delivery][delivery_postal_code]' => mt_rand(10000, 99999),

      'panes[billing][billing_first_name]' => $this->randomName(10),
      'panes[billing][billing_last_name]' => $this->randomName(10),
      'panes[billing][billing_street1]' => $this->randomName(10),
      'panes[billing][billing_city]' => $this->randomName(10),
      'panes[billing][billing_zone]' => $zone_id,
      'panes[billing][billing_postal_code]' => mt_rand(10000, 99999),
    );
    $this->drupalPost('cart/checkout', $edit, t('Review order'));
    $this->assertRaw(t('Your order is almost complete.'));

    // Make sure the price is still listed tax-inclusively.
    // !TODO This could be handled more specifically with a regex.
    $this->assertText('$16.80' . $rate->inclusion_text, 'Tax inclusive price appears in cart pane on checkout review page');

    // Ensure the tax-inclusive price is listed on the order admin page.
    $order_id = db_query("SELECT order_id FROM {uc_orders} WHERE delivery_first_name = :name", array(':name' => $edit['panes[delivery][delivery_first_name]']))->fetchField();
    $this->assertTrue($order_id, 'Order was created successfully');
    $this->drupalGet('admin/store/orders/' . $order_id);
    $this->assertText('$16.80' . $rate->inclusion_text, 'Tax inclusive price appears on the order view page.');

    // And on the invoice.
    $this->drupalGet('admin/store/orders/' . $order_id . '/invoice');
    $this->assertText('$16.80' . $rate->inclusion_text, 'Tax inclusive price appears on the invoice.');

    // And on the printable invoice.
    $this->drupalGet('admin/store/orders/' . $order_id . '/invoice');
    $this->assertText('$16.80' . $rate->inclusion_text, 'Tax inclusive price appears on the printable invoice.');
  }

  function loadTaxLine($order_id) {
    $order = uc_order_load($order_id, TRUE);
    foreach ($order->line_items as $line) {
      if ($line['type'] == 'tax') {
        return $line;
      }
    }
    return FALSE;
  }

  function assertTaxLineCorrect($line, $rate, $when) {
    $this->assertTrue($line, t('The tax line item was saved to the order ' . $when));
    $this->assertTrue(number_format($rate * $this->product->sell_price, 2) == number_format($line['amount'], 2), t('Stored tax line item has the correct amount ' . $when));
    $this->assertFieldByName('line_items[' . $line['line_item_id'] . '][li_id]', $line['line_item_id'], t('Found the tax line item ID ' . $when));
    $this->assertText($line['title'], t('Found the tax title ' . $when));
    $this->assertText(uc_currency_format($line['amount']), t('Tax display has the correct amount ' . $when));
  }

  function testStoredTaxDisplay() {
    $this->drupalLogin($this->adminUser);

    // Enable a payment method for the payment preview checkout pane.
    $edit = array('uc_payment_method_check_checkout' => 1);
    $this->drupalPost('admin/store/settings/payment', $edit, t('Save configuration'));

    // Create a 20% inclusive tax rate.
    $rate = (object) array(
      'name' => $this->randomName(8),
      'rate' => 0.2,
      'taxed_product_types' => array('product'),
      'taxed_line_items' => array(),
      'weight' => 0,
      'shippable' => 0,
      'display_include' => 1,
      'inclusion_text' => '',
    );
    uc_taxes_rate_save($rate);

    $this->drupalGet('admin/store/settings/taxes');
    $this->assertText($rate->name, t('Tax was saved successfully.'));

    $this->drupalGet("admin/store/settings/taxes/manage/uc_taxes_$rate->id");
    $this->assertText(t('Conditions'), t('Rules configuration linked to tax.'));

    $this->drupalPost('node/' . $this->product->nid, array(), t('Add to cart'));

    // Manually step through checkout. $this->checkout() doesn't know about taxes.
    $this->drupalPost('cart', array(), 'Checkout');
    $this->assertText(
      t('Enter your billing address and information here.'),
      t('Viewed cart page: Billing pane has been displayed.')
    );
    $this->assertRaw($rate->name, t('Tax line item displayed.'));
    $this->assertRaw(uc_currency_format($rate->rate * $this->product->sell_price), t('Correct tax amount displayed.'));

    // Build the panes.
    $zone_id = db_query_range('SELECT zone_id FROM {uc_zones} WHERE zone_country_id = :country ORDER BY rand()', 0, 1, array('country' => variable_get('uc_store_country', 840)))->fetchField();
    $edit = array(
      'panes[delivery][delivery_first_name]' => $this->randomName(10),
      'panes[delivery][delivery_last_name]' => $this->randomName(10),
      'panes[delivery][delivery_street1]' => $this->randomName(10),
      'panes[delivery][delivery_city]' => $this->randomName(10),
      'panes[delivery][delivery_zone]' => $zone_id,
      'panes[delivery][delivery_postal_code]' => mt_rand(10000, 99999),

      'panes[billing][billing_first_name]' => $this->randomName(10),
      'panes[billing][billing_last_name]' => $this->randomName(10),
      'panes[billing][billing_street1]' => $this->randomName(10),
      'panes[billing][billing_city]' => $this->randomName(10),
      'panes[billing][billing_zone]' => $zone_id,
      'panes[billing][billing_postal_code]' => mt_rand(10000, 99999),
    );

    // Submit the checkout page.
    $this->drupalPost('cart/checkout', $edit, t('Review order'));
    $this->assertRaw(t('Your order is almost complete.'));
    $this->assertRaw($rate->name, t('Tax line item displayed.'));
    $this->assertRaw(uc_currency_format($rate->rate * $this->product->sell_price), t('Correct tax amount displayed.'));

    // Complete the review page.
    $this->drupalPost(NULL, array(), t('Submit order'));

    $order_id = db_query("SELECT order_id FROM {uc_orders} WHERE delivery_first_name = :name", array(':name' => $edit['panes[delivery][delivery_first_name]']))->fetchField();
    if ($order_id) {
      $this->pass(
        t('Order %order_id has been created', array('%order_id' => $order_id))
      );

      $this->drupalGet('admin/store/orders/' . $order_id . '/edit');
      $this->assertTaxLineCorrect($this->loadTaxLine($order_id), $rate->rate, 'on initial order load');

      $this->drupalPost('admin/store/orders/' . $order_id . '/edit', array(), t('Submit changes'));
      $this->assertText(t('Order changes saved.'));
      $this->assertTaxLineCorrect($this->loadTaxLine($order_id), $rate->rate, 'after saving order');

      // Change tax rate and ensure order doesn't change.
      $oldrate = $rate->rate;
      $rate->rate = 0.1;
      $rate = uc_taxes_rate_save($rate);

      // Save order because tax changes are only updated on save.
      $this->drupalPost('admin/store/orders/' . $order_id . '/edit', array(), t('Submit changes'));
      $this->assertText(t('Order changes saved.'));
      $this->assertTaxLineCorrect($this->loadTaxLine($order_id), $oldrate, 'after rate change');

      // Change taxable products and ensure order doesn't change.
      $class = $this->createProductClass();
      $rate->taxed_product_types = array($class->name);
      uc_taxes_rate_save($rate);
      entity_flush_caches();
      $this->drupalPost('admin/store/orders/' . $order_id . '/edit', array(), t('Submit changes'));
      $this->assertText(t('Order changes saved.'));
      $this->assertTaxLineCorrect($this->loadTaxLine($order_id), $oldrate, 'after applicable product change');

      // Change order Status back to in_checkout and ensure tax-rate changes now update the order.
      uc_order_update_status($order_id, 'in_checkout');
      $this->drupalPost('admin/store/orders/' . $order_id . '/edit', array(), t('Submit changes'));
      $this->assertText(t('Order changes saved.'));
      $this->assertFalse($this->loadTaxLine($order_id), t('The tax line was removed from the order when order status changed back to in_checkout.'));

      // Restore taxable product and ensure new tax is added.
      $rate->taxed_product_types = array('product');
      uc_taxes_rate_save($rate);
      $this->drupalPost('admin/store/orders/' . $order_id . '/edit', array(), t('Submit changes'));
      $this->assertText(t('Order changes saved.'));
      $this->assertTaxLineCorrect($this->loadTaxLine($order_id), $rate->rate, 'when order status changed back to in_checkout');
    }
    else {
      $this->fail(t('No order was created.'));
    }
  }

  function testTaxProductClassUpdate() {
    $this->drupalLogin($this->adminUser);

    // Create a new product class.
    $type = strtolower($this->randomName(12));
    $edit = array(
      'pcid' => $type,
      'name' => $type,
      'description' => $this->randomName(32),
    );
    $this->drupalPost('admin/store/products/classes', $edit, t('Save'));
    node_types_rebuild();

    // Create a tax rate.
    $tax = $this->randomName(8);
    $rate = (object) array(
      'id' => 0, // TODO: should not have to set this
      'name' => $tax,
      'rate' => rand(1, 20) / 10,
      'taxed_product_types' => array($type),
      'taxed_line_items' => array(),
      'weight' => 0,
      'shippable' => 0,
    );
    uc_taxes_rate_save($rate);

    // Check that the tax rate shows up at checkout.
    $product = $this->createProduct(array('type' => $type));
    $this->drupalPost('node/' . $product->nid, array(), t('Add to cart'));
    $this->drupalGet('cart/checkout');
    $this->assertText($tax, 'Tax line item displayed.');

    // Change the machine name of the product class.
    $new_type = strtolower($this->randomName(12));
    $edit = array(
      'name' => $new_type,
      'type' => $new_type,
    );
    $this->drupalPost('admin/structure/types/manage/' . $type, $edit, t('Save content type'));

    // Check that the tax rate still shows up at checkout.
    $this->drupalPost('cart', array(), t('Remove'));
    $this->drupalPost('node/' . $product->nid, array(), t('Add to cart'));
    $this->drupalGet('cart/checkout');
    $this->assertText($tax, 'Tax line item displayed after changing product class node type.');
  }

}
