<?php

namespace Drupal\Tests\commerce_payment\Functional;

use Drupal\Core\Session\AccountInterface;
use Drupal\Tests\commerce\Functional\CommerceBrowserTestBase;
use Drupal\commerce_payment\Entity\PaymentMethod;
use Drupal\user\Entity\Role;

/**
 * Tests the payment method UI.
 *
 * @group commerce
 */
class PaymentMethodTest extends CommerceBrowserTestBase {

  /**
   * A normal user with minimum permissions.
   *
   * @var \Drupal\User\UserInterface
   */
  protected $user;

  /**
   * The payment method collection url.
   *
   * @var string
   */
  protected $collectionUrl;

  /**
   * Payment method entity storage.
   *
   * @var \Drupal\commerce_payment\PaymentMethodStorageInterface
   */
  public $paymentMethodStorage;

  /**
   * An on-site payment gateway.
   *
   * @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface
   */
  protected $paymentGateway;

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'commerce_payment',
    'commerce_payment_example',
  ];

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();

    $permissions = [
      'manage own commerce_payment_method',
    ];
    $this->user = $this->drupalCreateUser($permissions);
    $this->drupalLogin($this->user);

    $this->store->set('billing_countries', ['FR', 'US']);
    $this->store->save();

    $this->collectionUrl = 'user/' . $this->user->id() . '/payment-methods';

    /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */
    $this->paymentGateway = $this->createEntity('commerce_payment_gateway', [
      'id' => 'example',
      'label' => 'Example',
      'plugin' => 'example_onsite',
    ]);
    $this->paymentMethodStorage = $this->container->get('entity_type.manager')
      ->getStorage('commerce_payment_method');
  }

  /**
   * Tests accessing the payment method pages.
   */
  public function testUserAccess() {
    $this->drupalGet('user/' . $this->adminUser->id() . '/payment-methods');
    $this->assertSession()->statusCodeEquals(403);

    $this->drupalGet('user/' . $this->adminUser->id() . '/payment-methods/add');
    $this->assertSession()->statusCodeEquals(403);

    $this->drupalLogout();
    // Ensure anonymous users don't have access to the manage payment methods
    // page, even if either the "manage own payment methods" permission or
    // the "administer commerce_payment_method" is granted.
    $role = Role::load(AccountInterface::ANONYMOUS_ROLE);
    $role->grantPermission('manage own commerce_payment_method');
    $role->grantPermission('administer commerce_payment_method');
    $role->trustData()->save();
    $this->drupalGet('user/0/payment-methods');
    $this->assertSession()->statusCodeEquals(403);

    $this->drupalGet('user/0/payment-methods/add');
    $this->assertSession()->statusCodeEquals(403);
  }

  /**
   * Tests creating and updating a payment method.
   */
  public function testPaymentMethodCreationAndUpdate() {
    $default_address = [
      'country_code' => 'US',
      'administrative_area' => 'SC',
      'locality' => 'Greenville',
      'postal_code' => '29616',
      'address_line1' => '9 Drupal Ave',
      'given_name' => 'Bryan',
      'family_name' => 'Centarro',
    ];
    $default_profile = $this->createEntity('profile', [
      'type' => 'customer',
      'uid' => $this->user->id(),
      'address' => $default_address,
    ]);

    /** @var \Drupal\commerce_payment_example\Plugin\Commerce\PaymentGateway\OnsiteInterface $plugin */
    $this->drupalGet($this->collectionUrl);
    $this->getSession()->getPage()->clickLink('Add payment method');
    $this->assertSession()->addressEquals($this->collectionUrl . '/add');
    $rendered_address = $default_address;
    // Note that the full country name is rendered (not just the country code).
    $rendered_address['country_code'] = 'United States';
    // Confirm that the default profile's address is rendered.
    foreach ($rendered_address as $property => $value) {
      $prefix = 'add_payment_method[billing_information][address][0][address]';
      $this->assertSession()->pageTextContains($value);
      $this->assertSession()->fieldNotExists($prefix . '[' . $property . ']');
    }

    $form_values = [
      'add_payment_method[payment_details][number]' => '4111111111111111',
      'add_payment_method[payment_details][expiration][month]' => '01',
      'add_payment_method[payment_details][expiration][year]' => date('Y') + 1,
      'add_payment_method[payment_details][security_code]' => '111',
    ];
    $this->submitForm($form_values, 'Save');
    $this->assertSession()->addressEquals($this->collectionUrl);
    $this->assertSession()->pageTextContains('Visa ending in 1111 saved to your payment methods.');

    $payment_method = PaymentMethod::load(1);
    $billing_profile = $payment_method->getBillingProfile();
    $this->assertEquals($this->user->id(), $payment_method->getOwnerId());
    $this->assertEquals($default_address, array_filter($billing_profile->get('address')->first()->getValue()));
    $this->assertEquals(2, $payment_method->getBillingProfile()->id());

    $this->drupalGet($this->collectionUrl . '/' . $payment_method->id() . '/edit');
    // Confirm that the default profile's address is rendered.
    foreach ($rendered_address as $property => $value) {
      $prefix = 'add_payment_method[billing_information][address][0][address]';
      $this->assertSession()->pageTextContains($value);
      $this->assertSession()->fieldNotExists($prefix . '[' . $property . ']');
    }
    $this->getSession()->getPage()->pressButton('billing_edit');

    $form_values = [
      'payment_method[payment_details][expiration][month]' => '02',
      'payment_method[payment_details][expiration][year]' => $this->futureYear(),
      'payment_method[billing_information][address][0][address][given_name]' => 'Johnny',
      'payment_method[billing_information][address][0][address][family_name]' => 'Appleseed',
      'payment_method[billing_information][address][0][address][address_line1]' => '123 New York Drive',
      'payment_method[billing_information][address][0][address][locality]' => 'New York City',
      'payment_method[billing_information][address][0][address][administrative_area]' => 'NY',
      'payment_method[billing_information][address][0][address][postal_code]' => '10001',
    ];
    $this->submitForm($form_values, 'Save');
    $this->assertSession()->addressEquals($this->collectionUrl);
    $this->assertSession()->pageTextContains('2/' . $this->futureYear());

    \Drupal::entityTypeManager()->getStorage('commerce_payment_method')->resetCache([1]);
    \Drupal::entityTypeManager()->getStorage('profile')->resetCache([2]);
    $payment_method = PaymentMethod::load(1);
    $this->assertEquals($this->futureYear(), $payment_method->get('card_exp_year')->value);
    /** @var \Drupal\profile\Entity\ProfileInterface $billing_profile */
    $billing_profile = $payment_method->getBillingProfile();
    $this->assertEquals($this->user->id(), $payment_method->getOwnerId());
    $this->assertEquals('NY', $billing_profile->get('address')->first()->getAdministrativeArea());
    $this->assertEquals(2, $payment_method->getBillingProfile()->id());
    // Confirm that the address book profile was updated.
    $default_profile = $this->reloadEntity($default_profile);
    $this->assertTrue($billing_profile->get('address')->equals($default_profile->get('address')));
  }

  /**
   * Tests creating and updating a payment method without billing information.
   */
  public function testPaymentMethodCreationAndUpdateWithoutBilling() {
    $this->paymentGateway->setPluginConfiguration([
      'collect_billing_information' => FALSE,
    ]);
    $this->paymentGateway->save();

    /** @var \Drupal\commerce_payment_example\Plugin\Commerce\PaymentGateway\OnsiteInterface $plugin */
    $this->drupalGet($this->collectionUrl);
    $this->getSession()->getPage()->clickLink('Add payment method');
    $this->assertSession()->addressEquals($this->collectionUrl . '/add');
    $this->assertSession()->pageTextNotContains('Country');
    $form_values = [
      'add_payment_method[payment_details][number]' => '4111111111111111',
      'add_payment_method[payment_details][expiration][month]' => '01',
      'add_payment_method[payment_details][expiration][year]' => date('Y') + 1,
      'add_payment_method[payment_details][security_code]' => '111',
    ];
    $this->submitForm($form_values, 'Save');
    $this->assertSession()->addressEquals($this->collectionUrl);
    $this->assertSession()->pageTextContains('Visa ending in 1111 saved to your payment methods.');

    $payment_method = PaymentMethod::load(1);
    $this->assertNull($payment_method->getBillingProfile());

    $this->drupalGet($this->collectionUrl . '/' . $payment_method->id() . '/edit');
    $this->assertSession()->pageTextNotContains('Country');
    $form_values = [
      'payment_method[payment_details][expiration][month]' => '02',
      'payment_method[payment_details][expiration][year]' => $this->futureYear(),
    ];
    $this->submitForm($form_values, 'Save');
    $this->assertSession()->addressEquals($this->collectionUrl);

    $this->assertSession()->pageTextContains('2/' . $this->futureYear());

    \Drupal::entityTypeManager()->getStorage('commerce_payment_method')->resetCache([1]);
    $payment_method = PaymentMethod::load(1);
    $this->assertEquals($this->futureYear(), $payment_method->get('card_exp_year')->value);
    $this->assertNull($payment_method->getBillingProfile());
  }

  /**
   * Tests creating a payment method declined by the remote API.
   */
  public function testPaymentMethodDecline() {
    /** @var \Drupal\commerce_payment_example\Plugin\Commerce\PaymentGateway\OnsiteInterface $plugin */
    $this->drupalGet($this->collectionUrl);
    $this->getSession()->getPage()->clickLink('Add payment method');
    $this->assertSession()->addressEquals($this->collectionUrl . '/add');

    $form_values = [
      'add_payment_method[payment_details][number]' => '4111111111111111',
      'add_payment_method[payment_details][expiration][month]' => '01',
      'add_payment_method[payment_details][expiration][year]' => date('Y') + 1,
      'add_payment_method[payment_details][security_code]' => '111',
      'add_payment_method[billing_information][address][0][address][given_name]' => 'Johnny',
      'add_payment_method[billing_information][address][0][address][family_name]' => 'Appleseed',
      'add_payment_method[billing_information][address][0][address][address_line1]' => '123 New York Drive',
      'add_payment_method[billing_information][address][0][address][locality]' => 'Somewhere',
      'add_payment_method[billing_information][address][0][address][administrative_area]' => 'WI',
      'add_payment_method[billing_information][address][0][address][postal_code]' => '53141',
    ];
    $this->submitForm($form_values, 'Save');
    $this->assertSession()->addressNotEquals($this->collectionUrl);
    $this->assertSession()->pageTextNotContains('Visa ending in 1111 saved to your payment methods.');
    $this->assertSession()->pageTextContains('We encountered an error processing your payment method. Please verify your details and try again.');
  }

  /**
   * Tests deleting a payment method.
   */
  public function testPaymentMethodDeletion() {
    $payment_method = $this->createEntity('commerce_payment_method', [
      'uid' => $this->user->id(),
      'type' => 'credit_card',
      'payment_gateway' => 'example',
    ]);

    $details = [
      'type' => 'visa',
      'number' => '4111111111111111',
      'expiration' => ['month' => '01', 'year' => $this->futureYear()],
    ];
    $this->paymentGateway->getPlugin()->createPaymentMethod($payment_method, $details);
    $this->paymentGateway->save();

    $this->drupalGet($this->collectionUrl . '/' . $payment_method->id() . '/delete');

    $this->getSession()->getPage()->pressButton('Delete');
    $this->assertSession()->addressEquals($this->collectionUrl);

    $payment_method = $this->reloadEntity($payment_method);
    $this->assertNull($payment_method);
  }

  /**
   * Tests deleting a payment method without a gateway.
   */
  public function testPaymentMethodDeletionNoGateway() {
    $payment_method = $this->createEntity('commerce_payment_method', [
      'uid' => $this->user->id(),
      'type' => 'credit_card',
      'payment_gateway' => 'example',
    ]);
    $details = [
      'type' => 'visa',
      'number' => '4111111111111111',
      'expiration' => ['month' => '01', 'year' => $this->futureYear()],
    ];
    $this->paymentGateway->getPlugin()->createPaymentMethod($payment_method, $details);
    $this->paymentGateway->delete();

    $this->drupalGet($this->collectionUrl . '/' . $payment_method->id() . '/delete');

    $this->getSession()->getPage()->pressButton('Delete');
    $this->assertSession()->addressEquals($this->collectionUrl);

    $payment_method = $this->reloadEntity($payment_method);
    $this->assertNull($payment_method);
  }

  /**
   * Tests setting a payment method as default.
   */
  public function testPaymentMethodSetDefault() {
    /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */
    $payment_method = $this->createEntity('commerce_payment_method', [
      'uid' => $this->user->id(),
      'type' => 'credit_card',
      'payment_gateway' => 'example',
    ]);
    $details = [
      'type' => 'visa',
      'number' => '4111111111111111',
      'expiration' => ['month' => '01', 'year' => date("Y") + 1],
    ];
    $this->paymentGateway->getPlugin()->createPaymentMethod($payment_method, $details);
    $payment_method = $this->paymentMethodStorage->loadUnchanged($payment_method->id());
    // The first payment method should be set as default.
    $this->assertTrue($payment_method->isDefault());
    $this->drupalGet($this->collectionUrl);
    $this->assertSession()->pageTextNotContains('Mark as default');
    // Add a second payment method and test the 'Mark as default' operation.
    /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method2 */
    $payment_method2 = $this->createEntity('commerce_payment_method', [
      'uid' => $this->user->id(),
      'type' => 'credit_card',
      'payment_gateway' => 'example',
    ]);
    $details = [
      'type' => 'mastercard',
      'number' => '5555555555554444',
      'expiration' => ['month' => '01', 'year' => date("Y") + 1],
    ];
    $this->paymentGateway->getPlugin()->createPaymentMethod($payment_method2, $details);
    $this->assertFalse($payment_method2->isDefault());
    $this->drupalGet($this->collectionUrl);
    $this->getSession()->getPage()->clickLink('Mark as default');
    $this->assertSession()->addressEquals($this->collectionUrl);
    $this->assertSession()->pageTextContains(t('The @label payment method has been marked as default.', ['@label' => $payment_method2->label()]));
    // Test the updated payment methods.
    $payment_method = $this->paymentMethodStorage->loadUnchanged($payment_method->id());
    $payment_method2 = $this->paymentMethodStorage->loadUnchanged($payment_method2->id());
    $this->assertTrue($payment_method2->isDefault());
    $this->assertFalse($payment_method->isDefault());
  }

}
