<?php

use Civi\Api4\Activity;
use Civi\Api4\Contribution;
use Civi\Test\Api3TestTrait;
use Civi\Test\CiviEnvBuilder;
use Civi\Test\HeadlessInterface;
use Civi\Test\HookInterface;
use Civi\Test\TransactionalInterface;
use Civi\Api4\Contact;
use Civi\Api4\MembershipType;
use Civi\Api4\RelationshipType;
use Civi\Api4\Relationship;
use Civi\Api4\Event;
use Civi\Api4\PriceField;
use Civi\Api4\Participant;
use PHPUnit\Framework\TestCase;
use Civi\Test\ContactTestTrait;

/**
 * FIXME - Add test description.
 *
 * Tips:
 *  - With HookInterface, you may implement CiviCRM hooks directly in the test class.
 *    Simply create corresponding functions (e.g. "hook_civicrm_post(...)" or similar).
 *  - With TransactionalInterface, any data changes made by setUp() or test****() functions will
 *    rollback automatically -- as long as you don't manipulate schema or truncate tables.
 *    If this test needs to manipulate schema or truncate tables, then either:
 *       a. Do all that using setupHeadless() and Civi\Test.
 *       b. Disable TransactionalInterface, and handle all setup/teardown yourself.
 *
 * @group headless
 */
class CancelTest extends TestCase implements HeadlessInterface, HookInterface, TransactionalInterface {

  use Api3TestTrait;
  use ContactTestTrait;

  /**
   * Created ids.
   *
   * @var array
   */
  protected $ids = [];

  /**
   * The setupHeadless function runs at the start of each test case, right
   * before the headless environment reboots.
   *
   * It should perform any necessary steps required for putting the database
   * in a consistent baseline -- such as loading schema and extensions.
   *
   * The utility `\Civi\Test::headless()` provides a number of helper functions
   * for managing this setup, and it includes optimizations to avoid redundant
   * setup work.
   *
   * @throws \CRM_Extension_Exception_ParseException
   * @see \Civi\Test
   */
  public function setUpHeadless(): CiviEnvBuilder {
    // Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
    // See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest
    return \Civi\Test::headless()
      ->installMe(__DIR__)
      ->apply();
  }

  /**
   * Test that a cancel from paypal pro results in an order being cancelled.
   *
   * @throws \API_Exception
   * @throws \CRM_Core_Exception
   * @throws \Civi\API\Exception\UnauthorizedException
   */
  public function testPaypalProCancel(): void {
    $this->createContact();
    $this->createMembershipType();
    Relationship::create()->setValues([
      'contact_id_a' => $this->ids['contact'][0],
      'contact_id_b' => Contact::create()->setValues(['first_name' => 'Bugs', 'last_name' => 'Bunny'])->execute()->first()['id'],
      'relationship_type_id' => RelationshipType::get()->addWhere('name_a_b', '=', 'AB')->execute()->first()['id'],
    ])->execute();

    $this->createMembershipOrder();

    $memberships = $this->callAPISuccess('Membership', 'get')['values'];
    $this->assertCount(2, $memberships);

    $ipn = new CRM_Core_Payment_PayPalProIPN([
      'rp_invoice_id' => http_build_query([
        'b' => $this->ids['Contribution'][0],
        'm' => 'contribute',
        'i' => 'zyx',
        'c' => $this->ids['contact'][0],
      ]),
      'mc_gross' => 200,
      'payment_status' => 'Refunded',
      'processor_id' => $this->createPaymentProcessor(),
    ]);
    $ipn->main();
    $this->callAPISuccessGetSingle('Contribution', ['contribution_status_id' => 'Cancelled']);
    $this->callAPISuccessGetCount('Membership', ['status_id' => 'Cancelled'], 2);
  }

  /**
   * Create an order with more than one membership.
   *
   * @throws \CRM_Core_Exception
   */
  protected function createMembershipOrder(): void {
    $priceFieldID = $this->callAPISuccessGetValue('price_field', [
      'return' => 'id',
      'label' => 'Membership Amount',
      'options' => ['limit' => 1, 'sort' => 'id DESC'],
    ]);
    $generalPriceFieldValueID = $this->callAPISuccessGetValue('price_field_value', [
      'return' => 'id',
      'label' => 'General',
      'options' => ['limit' => 1, 'sort' => 'id DESC'],
    ]);

    $orderID = $this->callAPISuccess('Order', 'create', [
      'financial_type_id' => 'Member Dues',
      'contact_id' => $this->ids['contact'][0],
      'is_test' => 0,
      'payment_instrument_id' => 'Credit card',
      'receive_date' => '2019-07-25 07:34:23',
      'invoice_id' => 'zyx',
      'line_items' => [
        [
          'params' => [
            'contact_id' => $this->ids['contact'][0],
            'source' => 'Payment',
            'membership_type_id' => 'General',
            // This is interim needed while we improve the BAO - if the test passes without it it can go!
            'skipStatusCal' => TRUE,
          ],
          'line_item' => [
            [
              'label' => 'General',
              'qty' => 1,
              'unit_price' => 200,
              'line_total' => 200,
              'financial_type_id' => 1,
              'entity_table' => 'civicrm_membership',
              'price_field_id' => $priceFieldID,
              'price_field_value_id' => $generalPriceFieldValueID,
            ],
          ],
        ],
      ],
    ])['id'];
    $this->ids['Contribution'][0] = $orderID;
  }

  /**
   * Create the general membership type.
   *
   * @throws \API_Exception
   * @throws \Civi\API\Exception\UnauthorizedException
   */
  protected function createMembershipType(): void {
    MembershipType::create()->setValues([
      'name' => 'General',
      'duration_unit' => 'year',
      'duration_interval' => 1,
      'period_type' => 'rolling',
      'member_of_contact_id' => 1,
      'domain_id' => 1,
      'financial_type_id' => 2,
      'relationship_type_id' => RelationshipType::create(FALSE)->setValues(['name_a_b' => 'AB', 'name_b_a' => 'BA'])->execute()->first()['id'],
      'relationship_direction' => 'a_b',
      'is_active' => 1,
      'sequential' => 1,
      'visibility' => 'Public',
    ])->execute();
  }

  /**
   * Create a payment processor.
   *
   * @param array $params
   *
   * @return int
   * @throws \CRM_Core_Exception
   */
  public function createPaymentProcessor($params = []): int {
    $params = array_merge([
      'name' => 'demo',
      'domain_id' => CRM_Core_Config::domainID(),
      'payment_processor_type_id' => 'PayPal',
      'is_active' => 1,
      'is_default' => 0,
      'is_test' => 1,
      'user_name' => 'sunil._1183377782_biz_api1.webaccess.co.in',
      'password' => '1183377788',
      'signature' => 'APixCoQ-Zsaj-u3IH7mD5Do-7HUqA9loGnLSzsZga9Zr-aNmaJa3WGPH',
      'url_site' => 'https://www.sandbox.paypal.com/',
      'url_api' => 'https://api-3t.sandbox.paypal.com/',
      'url_button' => 'https://www.paypal.com/en_US/i/btn/btn_xpressCheckout.gif',
      'class_name' => 'Payment_PayPalImpl',
      'billing_mode' => 3,
      'financial_type_id' => 1,
      'financial_account_id' => 12,
      // Credit card = 1 so can pass 'by accident'.
      'payment_instrument_id' => 'Debit Card',
    ], $params);
    if (!is_numeric($params['payment_processor_type_id'])) {
      // really the api should handle this through getoptions but it's not exactly api call so lets just sort it
      //here
      $params['payment_processor_type_id'] = $this->callAPISuccess('payment_processor_type', 'getvalue', [
        'name' => $params['payment_processor_type_id'],
        'return' => 'id',
      ], 'integer');
    }
    $result = $this->callAPISuccess('payment_processor', 'create', $params);
    return (int) $result['id'];
  }

  /**
   * Test that a cancel from paypal pro results in an order being cancelled.
   *
   * @throws \API_Exception
   * @throws \CRM_Core_Exception|\CiviCRM_API3_Exception
   */
  public function testPaypalStandardCancel(): void {
    $this->createContact();
    $orderID = $this->createEventOrder();
    $ipn = new CRM_Core_Payment_PayPalIPN([
      'mc_gross' => 200,
      'contactID' => $this->ids['contact'][0],
      'contributionID' => $orderID,
      'module' => 'event',
      'invoice' => 123,
      'eventID' => $this->ids['event'][0],
      'participantID' => Participant::get()->addWhere('event_id', '=', $this->ids['event'][0])->addSelect('id')->execute()->first()['id'],
      'payment_status' => 'Refunded',
      'processor_id' => $this->createPaymentProcessor(['payment_processor_type_id' => 'PayPal_Standard']),
    ]);
    $ipn->main();
    $this->callAPISuccessGetSingle('Contribution', ['contribution_status_id' => 'Cancelled']);
    $this->callAPISuccessGetCount('Participant', ['status_id' => 'Cancelled'], 1);
  }

  /**
   * Test cancel order api
   * @throws API_Exception
   * @throws CRM_Core_Exception
   */
  public function testCancelOrderWithParticipant(): void {
    $this->createContact();
    $orderID = $this->createEventOrder();
    $this->callAPISuccess('Order', 'cancel', ['contribution_id' => $orderID]);
    $this->callAPISuccess('Order', 'get', ['contribution_id' => $orderID]);
    $this->callAPISuccessGetSingle('Contribution', ['contribution_status_id' => 'Cancelled']);
    $this->callAPISuccessGetCount('Participant', ['status_id' => 'Cancelled'], 1);
  }

  /**
   * Test cancel order api when a pledge is linked.
   *
   * The pledge status should be updated. I believe the contribution should also be unlinked but
   * the goal at this point is no change.
   *
   * @throws CRM_Core_Exception
   * @throws API_Exception
   */
  public function testCancelOrderWithPledge(): void {
    $this->createContact();
    $pledgeID = (int) $this->callAPISuccess('Pledge', 'create', ['contact_id' => $this->ids['contact'][0], 'amount' => 4, 'installments' => 2, 'frequency_unit' => 'month', 'original_installment_amount' => 2, 'create_date' => 'now', 'financial_type_id' => 'Donation', 'start_date' => '+5 days'])['id'];
    $orderID = (int) $this->callAPISuccess('Order', 'create', ['contact_id' => $this->ids['contact'][0], 'total_amount' => 2, 'financial_type_id' => 'Donation', 'api.Payment.create' => ['total_amount' => 2]])['id'];
    $pledgePayments = $this->callAPISuccess('PledgePayment', 'get')['values'];
    $this->callAPISuccess('PledgePayment', 'create', ['id' => key($pledgePayments), 'pledge_id' => $pledgeID, 'contribution_id' => $orderID, 'status_id' => 'Completed', 'actual_amount' => 2]);
    $beforePledge = $this->callAPISuccessGetSingle('Pledge', ['id' => $pledgeID]);
    $this->assertEquals(2, $beforePledge['pledge_total_paid']);
    $this->callAPISuccess('Order', 'cancel', ['contribution_id' => $orderID]);

    $this->callAPISuccessGetSingle('Contribution', ['contribution_status_id' => 'Cancelled']);
    $afterPledge = $this->callAPISuccessGetSingle('Pledge', ['id' => $pledgeID]);
    $this->assertEquals('', $afterPledge['pledge_total_paid']);
  }

  /**
   * Test cancelling a contribution with a membership on the contribution edit
   * form.
   *
   * @throws \API_Exception
   * @throws \CRM_Core_Exception
   * @throws \CiviCRM_API3_Exception
   */
  public function testCancelFromContributionForm(): void {
    $this->createContact();
    $this->createMembershipType();
    $this->createMembershipOrder();
    $this->createLoggedInUser();
    $formValues = [
      'id' => $this->ids['Contribution'][0],
      'contact_id' => $this->ids['contact'][0],
      'contribution_status_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Cancelled'),
    ];
    $form = new CRM_Contribute_Form_Contribution();
    $_SERVER['REQUEST_METHOD'] = 'GET';
    $form->controller = new CRM_Core_Controller();
    $form->controller->setStateMachine(new CRM_Core_StateMachine($form->controller));
    $form->testSubmit($formValues, CRM_Core_Action::UPDATE);

    $contribution = Contribution::get()
      ->addWhere('id', '=', $this->ids['Contribution'][0])
      ->addSelect('contribution_status_id:name')
      ->execute()->first();
    $this->assertEquals('Cancelled', $contribution['contribution_status_id:name']);
    $membership = $this->callAPISuccessGetSingle('Membership', []);
    $this->assertEquals('Cancelled', CRM_Core_PseudoConstant::getName('CRM_Member_BAO_Membership', 'status_id', $membership['status_id']));
    $activity = Activity::get()
      ->addSelect('subject', 'source_record_id', 'status_id')
      ->addWhere('activity_type_id:name', '=', 'Change Membership Status')
      ->execute();
    $this->assertCount(1, $activity);
    $this->assertEquals('Status changed from Pending to Cancelled', $activity->first()['subject']);
  }

  /**
   * Create an event and an order for a participant in that event.
   *
   * @return int
   * @throws API_Exception
   * @throws CRM_Core_Exception
   */
  protected function createEventOrder(): int {
    $this->ids['event'][0] = (int) Event::create()->setValues(['title' => 'Event', 'start_date' => 'tomorrow', 'event_type_id:name' => 'Workshop'])->execute()->first()['id'];
    $order = $this->callAPISuccess('Order', 'create', [
      'contact_id' => $this->ids['contact'][0],
      'financial_type_id' => 'Donation',
      'invoice_id' => 123,
      'line_items' => [
        [
          'line_item' => [
            [
              'line_total' => 5,
              'qty' => 1,
              'financial_type_id' => 1,
              'entity_table' => 'civicrm_participant',
              'price_field_id' => PriceField::get()->addSelect('id')->addWhere('name', '=', 'contribution_amount')->execute()->first()['id'],
            ],
          ],
          'params' => [
            'contact_id' => $this->ids['contact'][0],
            'event_id' => $this->ids['event'][0],
          ],
        ],
      ],
    ]);
    return (int) $order['id'];
  }

  /**
   * Create a contact for use in the test.
   *
   * @throws API_Exception
   */
  protected function createContact(): void {
    $this->ids['contact'][0] = Civi\Api4\Contact::create()->setValues(['first_name' => 'Brer', 'last_name' => 'Rabbit'])->execute()->first()['id'];
  }

}
