<?php
/*
 +--------------------------------------------------------------------+
 | Copyright CiviCRM LLC. All rights reserved.                        |
 |                                                                    |
 | This work is published under the GNU AGPLv3 license with some      |
 | permitted exceptions and without any warranty. For full license    |
 | and copyright information, see https://civicrm.org/licensing       |
 +--------------------------------------------------------------------+
 */

use Civi\Api4\MembershipType;
use Civi\Api4\Relationship;
use Civi\Api4\RelationshipType;

/**
 *  Test APIv3 civicrm_membership functions
 *
 * @package CiviCRM_APIv3
 * @subpackage API_Member
 */

/**
 * Class api_v3_MembershipTest
 * @group headless
 */
class api_v3_MembershipTest extends CiviUnitTestCase {

  use CRMTraits_Financial_OrderTrait;

  protected $_contactID;
  protected $_membershipID;
  protected $_membershipID2;
  protected $_membershipID3;
  protected $_membershipTypeID;
  protected $_membershipTypeID2;
  protected $_membershipStatusID;
  protected $_entity;
  protected $_params;

  /**
   * Set up for tests.
   */
  public function setUp(): void {
    parent::setUp();
    $this->hookClass->setHook('civicrm_custom', [$this, 'hook_custom']);
    $this->_contactID = $this->individualCreate();
    $this->_membershipTypeID = $this->membershipTypeCreate(['member_of_contact_id' => $this->ids['Contact']['individual_0']]);
    $this->_membershipTypeID2 = $this->membershipTypeCreate([
      'period_type' => 'fixed',
       // Ie. 1 March.
      'fixed_period_start_day' => '301',
      // Ie. 11 Nov.
      'fixed_period_rollover_day' => '1111',
      'name' => 'Another one',
    ]);
    $this->_membershipStatusID = $this->membershipStatusCreate();

    $this->_entity = 'Membership';
    $this->_params = [
      'contact_id' => $this->ids['Contact']['individual_0'],
      'membership_type_id' => 'General',
      'join_date' => '2009-01-21',
      'start_date' => '2009-01-21',
      'end_date' => '2009-12-21',
      'source' => 'Payment',
      'is_override' => 1,
      'status_id' => $this->ids['MembershipStatus']['test member status'],
    ];
  }

  /**
   * Clean up after tests.
   *
   * @throws \Exception
   */
  public function tearDown(): void {
    $this->quickCleanUpFinancialEntities();
    $this->quickCleanup(['civicrm_uf_match', 'civicrm_contact'], TRUE);
    parent::tearDown();
  }

  /**
   * Get the id for the given type.
   *
   * @param string $name
   *
   * @return int
   */
  public function getMembershipTypeID(string $name): int {
    return CRM_Core_PseudoConstant::getKey('CRM_Member_BAO_Membership', 'membership_type_id', $name);
  }

  /**
   * Test membership deletion.
   */
  public function testMembershipDelete(): void {
    $membershipID = $this->contactMembershipCreate($this->_params);
    $this->assertDBRowExist('CRM_Member_DAO_Membership', $membershipID);
    $params = [
      'id' => $membershipID,
    ];
    $this->callAPISuccess('membership', 'delete', $params);
    $this->assertDBRowNotExist('CRM_Member_DAO_Membership', $membershipID);
  }

  public function testMembershipDeleteEmpty(): void {
    $this->callAPIFailure('membership', 'delete', []);
  }

  public function testMembershipDeleteInvalidID(): void {
    $this->callAPIFailure('membership', 'delete', ['id' => 'blah']);
  }

  /**
   * Test membership deletion and with the preserve contribution param.
   */
  public function testMembershipDeletePreserveContribution(): void {
    //DELETE
    $membershipID = $this->contactMembershipCreate($this->_params);
    //DELETE
    $this->assertDBRowExist('CRM_Member_DAO_Membership', $membershipID);
    $ContributionCreate = $this->callAPISuccess('Contribution', 'create', [
      'sequential' => 1,
      'financial_type_id' => 'Member Dues',
      'total_amount' => 100,
      'contact_id' => $this->_params['contact_id'],
    ]);
    $this->callAPISuccess('MembershipPayment', 'create', [
      'sequential' => 1,
      'contribution_id' => $ContributionCreate['values'][0]['id'],
      'membership_id' => $membershipID,
    ]);
    $memParams = [
      'id' => $membershipID,
      'preserve_contribution' => 1,
    ];
    $contribParams = [
      'id' => $ContributionCreate['values'][0]['id'],
    ];
    $this->callAPISuccess('membership', 'delete', $memParams);
    $this->assertDBRowNotExist('CRM_Member_DAO_Membership', $membershipID);
    $this->assertDBRowExist('CRM_Contribute_DAO_Contribution', $ContributionCreate['values'][0]['id']);
    $this->callAPISuccess('Contribution', 'delete', $contribParams);
    $this->assertDBRowNotExist('CRM_Contribute_DAO_Contribution', $ContributionCreate['values'][0]['id']);
  }

  /**
   * Test Multiple Membership Status for same contribution id.
   */
  public function testMultipleMembershipsContribution(): void {
    $contactId1 = $this->individualCreate();
    $contactId2 = $this->individualCreate();
    $contactId3 = $this->individualCreate();
    $order = $this->callAPISuccess('Order', 'create', [
      'financial_type_id' => '1',
      'contact_id' => $contactId1,
      'payment_instrument_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'payment_instrument_id', 'Check'),
      'is_pay_later' => 1,
      'receive_date' => date('Ymd'),
      'line_items' => [
        [
          'line_item' => [
            [
              'membership_type_id' => 'General',
              'source' => 'Webform Payment',
              'line_total' => 50,
              'qty' => 1,
            ],
          ],
          'params' => [
            'contact_id' => $contactId1,
            'membership_type_id' => 'General',
          ],
        ],
        [
          'line_item' => [
            [
              'membership_type_id' => 'General',
              'source' => 'Webform Payment',
              'line_total' => 50,
              'qty' => 1,
            ],
          ],
          'params' => [
            'contact_id' => $contactId2,
            'membership_type_id' => 'General',
          ],
        ],
        [
          'line_item' => [
            [
              'membership_type_id' => 'General',
              'source' => 'Webform Payment',
              'line_total' => 50,
              'qty' => 1,
            ],
          ],
          'params' => [
            'contact_id' => $contactId3,
            'membership_type_id' => 'General',
          ],
        ],
      ],
    ]);

    $_REQUEST['action'] = 'update';
    $_REQUEST['id'] = $order['id'];
    // Change Payment Status to Completed
    /** @var CRM_Contribute_Form_Contribution $form */
    $form = $this->getFormObject('CRM_Contribute_Form_Contribution', [
      'total_amount' => 150,
      'financial_type_id' => '1',
      'contact_id' => $contactId1,
      'payment_instrument_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'payment_instrument_id', 'Check'),
      'contribution_status_id' => 1,
    ]);
    $form->buildForm();
    $form->postProcess();
    $memberships = $this->callAPISuccess('Membership', 'get', ['sort' => 'contact_id', 'sequential' => 1])['values'];

    $this->assertEquals($contactId1, $memberships[0]['contact_id']);
    $this->assertEquals('New', CRM_Core_PseudoConstant::getName('CRM_Member_BAO_Membership', 'status_id', $memberships[0]['status_id']));

    // check for Membership 2
    $this->assertEquals($contactId2, $memberships[1]['contact_id']);
    $this->assertEquals('New', CRM_Core_PseudoConstant::getName('CRM_Member_BAO_Membership', 'status_id', $memberships[1]['status_id']));

    // check for Membership 3
    $this->assertEquals($contactId3, $memberships[2]['contact_id']);
    $this->assertEquals('New', CRM_Core_PseudoConstant::getName('CRM_Member_BAO_Membership', 'status_id', $memberships[2]['status_id']));
  }

  /**
   * Test civicrm_membership_get with params not array.
   *
   * Gets treated as contact_id, memberships expected.
   *
   * @throws \CRM_Core_Exception
   */
  public function testGetInSyntax(): void {
    $this->_membershipID = $this->contactMembershipCreate($this->_params);
    $this->_membershipID2 = $this->contactMembershipCreate($this->_params);
    $this->_membershipID3 = $this->contactMembershipCreate($this->_params);
    $params = [
      'id' => ['IN' => [$this->_membershipID, $this->_membershipID3]],
    ];
    $membership = $this->callAPISuccess('membership', 'get', $params);
    $this->assertEquals(2, $membership['count']);
    $this->assertEquals([$this->_membershipID, $this->_membershipID3], array_keys($membership['values']));
    $params = [
      'id' => ['NOT IN' => [$this->_membershipID, $this->_membershipID3]],
    ];
    $membership = $this->callAPISuccess('membership', 'get', $params);
    $this->assertEquals(1, $membership['count']);
    $this->assertEquals([$this->_membershipID2], array_keys($membership['values']));
  }

  /**
   * Test civicrm_membership_get with params not array.
   * Gets treated as contact_id, memberships expected.
   */
  public function testGetInSyntaxOnContactID(): void {
    $this->_membershipID = $this->contactMembershipCreate($this->_params);
    $contact2 = $this->individualCreate();
    $contact3 = $this->individualCreate(['first_name' => 'Scout', 'last_name' => 'Canine']);
    $this->_membershipID2 = $this->contactMembershipCreate(array_merge($this->_params, ['contact_id' => $contact2]));
    $this->_membershipID3 = $this->contactMembershipCreate(array_merge($this->_params, ['contact_id' => $contact3]));
    $params = [
      'contact_id' => ['IN' => [$this->_contactID, $contact3]],
    ];
    $membership = $this->callAPISuccess('membership', 'get', $params);
    $this->assertEquals(2, $membership['count']);
    $this->assertEquals([$this->_membershipID, $this->_membershipID3], array_keys($membership['values']));
    $params = [
      'contact_id' => ['NOT IN' => [$this->_contactID, $contact3]],
    ];
    $membership = $this->callAPISuccess('membership', 'get', $params);
    $this->assertEquals(1, $membership['count']);
    $this->assertEquals([$this->_membershipID2], array_keys($membership['values']));
  }

  /**
   * Test civicrm_membership_get with params not array.
   * Gets treated as contact_id, memberships expected.
   */
  public function testGetWithParamsMemberShipTypeIdContactID(): void {
    $params = $this->_params;
    $this->callAPISuccess('Membership', 'create', $params);
    $params['membership_type_id'] = $this->_membershipTypeID2;
    $this->callAPISuccess('Membership', 'create', $params);
    $this->callAPISuccessGetCount('membership', ['contact_id' => $this->_contactID], 2);
    $params = [
      'membership_type_id' => $this->_membershipTypeID,
      'contact_id' => $this->_contactID,
    ];
    $this->callAPISuccessGetCount('Membership', $params, 1);

    $params = [
      'membership_type_id' => $this->_membershipTypeID2,
      'contact_id' => $this->_contactID,
      'return' => ['membership_type_id', 'contact_id'],
    ];
    $result = $this->callAPISuccessGetSingle('Membership', $params);
    $this->assertEquals($result['contact_id'], $this->_contactID);
    $this->assertEquals($result['membership_type_id'], $this->_membershipTypeID2);
  }

  /**
   * Check with complete array + custom field.
   *
   * Note that the test is written on purpose without any
   * variables specific to participant so it can be replicated into other entities
   * and / or moved to the automated test suite
   */
  public function testGetWithParamsMemberShipIdAndCustom(): void {
    $ids = $this->entityCustomGroupWithSingleFieldCreate(__FUNCTION__, __FILE__);

    $params = $this->_params;
    $params['custom_' . $ids['custom_field_id']] = 'custom string';

    $result = $this->callAPISuccess($this->_entity, 'create', $params);

    $getParams = ['membership_type_id' => $params['membership_type_id'], 'return' => 'custom_' . $ids['custom_field_id']];
    $check = $this->callAPISuccess($this->_entity, 'get', $getParams);
    $this->assertEquals('custom string', $check['values'][$result['id']]['custom_' . $ids['custom_field_id']]);
  }

  /**
   * Test civicrm_membership_get with proper params.
   * Memberships expected.
   */
  public function testGet(): void {
    $this->contactMembershipCreate($this->_params);
    $params = [
      'contact_id' => $this->_contactID,
      'return' => array_keys($this->_params),
    ];

    $result = $this->callAPISuccessGetSingle('Membership', $params);
    $this->assertEquals('2009-01-21', $result['join_date']);
    $this->assertEquals($result['contact_id'], $this->_contactID);
    $this->assertEquals($result['membership_type_id'], $this->getMembershipTypeID('General'));
    $this->assertEquals($result['status_id'], $this->ids['MembershipStatus']['test member status']);

    $this->assertEquals('2009-01-21', $result['start_date']);
    $this->assertEquals('2009-12-21', $result['end_date']);
    $this->assertEquals('Payment', $result['source']);
    $this->assertEquals(1, $result['is_override']);
  }

  /**
   * Test civicrm_membership_get with proper params.
   * Memberships expected.
   */
  public function testGetWithId(): void {
    $membershipID = $this->contactMembershipCreate($this->_params);
    $params = [
      'contact_id' => $this->_contactID,
      'id' => $this->_membershipID,
      'return' => 'id',
    ];
    $result = $this->callAPISuccess('membership', 'get', $params);
    $this->assertEquals($membershipID, $result['id']);
    $params = [
      'contact_id' => $this->_contactID,
      'membership_id' => $this->_membershipID,
      'return' => 'membership_id',
    ];
    $result = $this->callAPISuccess('membership', 'get', $params);
    $this->assertEquals($membershipID, $result['id']);
  }

  /**
   * Test civicrm_membership_get for only active.
   * Memberships expected.
   */
  public function testGetOnlyActive(): void {
    $this->_membershipID = $this->contactMembershipCreate($this->_params);
    $params = [
      'contact_id' => $this->_contactID,
      'active_only' => 1,
    ];

    $membership = $this->callAPISuccess('membership', 'get', $params);
    $this->assertEquals($membership['values'][$this->_membershipID]['status_id'], $this->_membershipStatusID);
    $this->assertEquals($membership['values'][$this->_membershipID]['contact_id'], $this->_contactID);
    $params = [
      'contact_id' => $this->_contactID,
      'filters' => [
        'is_current' => 1,
      ],
    ];

    $membership = $this->callAPISuccess('membership', 'get', $params);
    $this->assertEquals($membership['values'][$this->_membershipID]['status_id'], $this->_membershipStatusID);
    $this->assertEquals($membership['values'][$this->_membershipID]['contact_id'], $this->_contactID);
  }

  /**
   * Test civicrm_membership_create with relationships.
   * create/get Memberships.
   *
   * Test suite for CRM-14758: API ( contact, create ) does not always create related membership
   * and max_related property for Membership_Type and Membership entities
   *
   * @throws \CRM_Core_Exception
   */
  public function testCreateWithRelationship(): void {
    // Create membership type: inherited through employment, max_related = 2
    $params = [
      'name_a_b' => 'Employee of',
    ];
    $result = $this->callAPISuccess('relationship_type', 'get', $params);
    $relationshipTypeId = $result['id'];
    $membershipOrgId = $this->organizationCreate();
    $params = [
      'name' => 'Corporate Membership',
      'duration_unit' => 'year',
      'duration_interval' => 1,
      'period_type' => 'rolling',
      'member_of_contact_id' => $membershipOrgId,
      'domain_id' => 1,
      'financial_type_id' => 1,
      'relationship_type_id' => $relationshipTypeId,
      'relationship_direction' => 'b_a',
      'max_related' => 2,
      'is_active' => 1,
    ];
    $result = $this->callAPISuccess('membership_type', 'create', $params);
    $membershipTypeId = $result['id'];

    // Create employer and first employee
    $employerId[0] = $this->organizationCreate([], 1);
    $memberContactId[0] = $this->individualCreate(['employer_id' => $employerId[0]], 0);

    // Create organization's membership
    $params = [
      'contact_id' => $employerId[0],
      'membership_type_id' => $membershipTypeId,
      'source' => 'Test suite',
      'start_date' => date('Y-m-d'),
      'end_date' => '+1 year',
    ];
    $OrganizationMembershipID = $this->contactMembershipCreate($params);

    // Check that the employee inherited the membership
    $params = [
      'contact_id' => $memberContactId[0],
      'membership_type_id' => $membershipTypeId,
    ];

    $result = $this->callAPISuccess('membership', 'get', $params);

    $this->assertEquals(1, $result['count']);
    $result = $result['values'][$result['id']];
    $this->assertEquals($OrganizationMembershipID, $result['owner_membership_id']);

    // Create second employee
    $memberContactId[1] = $this->individualCreate(['employer_id' => $employerId[0]], 1);

    // Check that the employee inherited the membership
    $params = [
      'contact_id' => $memberContactId[1],
      'membership_type_id' => $membershipTypeId,
    ];
    $result = $this->callAPISuccess('membership', 'get', $params);
    // If it fails here CRM-14758 is not fixed
    $this->assertEquals(1, $result['count']);
    $result = $result['values'][$result['id']];
    $this->assertEquals($OrganizationMembershipID, $result['owner_membership_id']);

    // Create third employee
    $memberContactId[2] = $this->individualCreate(['employer_id' => $employerId[0]], 2);

    // Check that employee does NOT inherit the membership (max_related = 2)
    $params = [
      'contact_id' => $memberContactId[2],
      'membership_type_id' => $membershipTypeId,
    ];
    $result = $this->callAPISuccess('membership', 'get', $params);
    $this->assertEquals(0, $result['count']);

    // Increase max_related for the employer's membership
    $params = [
      'id' => $OrganizationMembershipID,
      'max_related' => 3,
    ];
    $this->callAPISuccess('Membership', 'create', $params);

    // Check that the employee inherited the membership
    $params = [
      'contact_id' => $memberContactId[2],
      'membership_type_id' => $membershipTypeId,
    ];
    $result = $this->callAPISuccess('membership', 'get', $params);
    $this->assertEquals(1, $result['count']);
    $result = $result['values'][$result['id']];
    $this->assertEquals($OrganizationMembershipID, $result['owner_membership_id']);

    // First employee moves to a new job
    $employerId[1] = $this->organizationCreate([], 2);
    $params = [
      'id' => $memberContactId[0],
      'employer_id' => $employerId[1],
    ];
    $this->callAPISuccess('contact', 'create', $params);

    // Check that employee does NO LONGER inherit the membership
    $params = [
      'contact_id' => $memberContactId[0],
      'membership_type_id' => $membershipTypeId,
    ];
    $result = $this->callAPISuccess('membership', 'get', $params);
    $this->assertEquals(0, $result['count']);

    //Create pay_later membership for organization.
    $employerId[2] = $this->organizationCreate([], 1);
    $params = [
      'contact_id' => $employerId[2],
      'membership_type_id' => $membershipTypeId,
      'source' => 'Test pay later suite',
      'is_pay_later' => 1,
      'status_id' => 5,
    ];
    $organizationMembershipID = $this->callAPISuccess('Membership', 'create', $params)['id'];

    $memberContactId[3] = $this->individualCreate(['employer_id' => $employerId[2]], 0);
    // Check that the employee inherited the membership
    $params = [
      'contact_id' => $memberContactId[3],
      'membership_type_id' => $membershipTypeId,
    ];
    $result = $this->callAPISuccessGetSingle('membership', $params);
    $this->assertEquals($organizationMembershipID, $result['owner_membership_id']);

    // Set up params for enable/disable checks
    $relationship1 = $this->callAPISuccess('relationship', 'get', ['contact_id_a' => $memberContactId[1]]);
    $params = [
      'contact_id' => $memberContactId[1],
      'membership_type_id' => $membershipTypeId,
    ];

    // Deactivate relationship using create and assert membership is not inherited
    $this->callAPISuccess('relationship', 'create', ['id' => $relationship1['id'], 'is_active' => 0]);
    $result = $this->callAPISuccess('membership', 'get', $params);
    $this->assertEquals(0, $result['count']);

    // Re-enable relationship using create and assert membership is inherited
    $this->callAPISuccess('relationship', 'create', ['id' => $relationship1['id'], 'is_active' => 1]);
    $result = $this->callAPISuccess('membership', 'get', $params);
    $this->assertEquals(1, $result['count']);

    // Deactivate relationship using setvalue and assert membership is not inherited
    $this->callAPISuccess('relationship', 'setvalue', ['id' => $relationship1['id'], 'field' => 'is_active', 'value' => 0]);
    $result = $this->callAPISuccess('membership', 'get', $params);
    $this->assertEquals(0, $result['count']);

    // Re-enable relationship using setvalue and assert membership is inherited
    $this->callAPISuccess('relationship', 'setvalue', ['id' => $relationship1['id'], 'field' => 'is_active', 'value' => 1]);
    $result = $this->callAPISuccess('membership', 'get', $params);
    $this->assertEquals(1, $result['count']);

    // Delete relationship and assert membership is not inherited
    $this->callAPISuccess('relationship', 'delete', ['id' => $relationship1['id']]);
    $result = $this->callAPISuccess('membership', 'get', $params);
    $this->assertEquals(0, $result['count']);

    // Tear down - reverse of creation to be safe
    $this->contactDelete($memberContactId[2]);
    $this->contactDelete($memberContactId[1]);
    $this->contactDelete($memberContactId[0]);
    $this->contactDelete($employerId[1]);
    $this->contactDelete($employerId[0]);
    $this->membershipTypeDelete(['id' => $membershipTypeId]);
    $this->contactDelete($membershipOrgId);
  }

  /**
   * Test that loops are not created when adding spouse relationships.
   *
   * This add a test for https://issues.civicrm.org/jira/browse/CRM-4213 in the hope of removing
   * the buggy fix for that without a resurgence.
   *
   * @throws \CRM_Core_Exception
   * @throws \Civi\API\Exception\UnauthorizedException
   */
  public function testCreateWithSpouseRelationship(): void {
    $relationshipTypeID = RelationshipType::get()->addSelect('id')->addWhere('name_a_b', '=', 'Spouse of')->execute()->first()['id'];
    MembershipType::update()->setValues([
      'relationship_direction' => ['b_a', 'a_b'],
      'relationship_type_id' => [$relationshipTypeID, $relationshipTypeID],
    ])
      ->addWhere('name', '=', 'General')
      ->execute()->first()['id'];

    $spouse1ID = $this->individualCreate(['first_name' => 'him']);
    $spouse2ID = $this->individualCreate(['first_name' => 'her']);
    $spouse3ID = $this->individualCreate(['first_name' => 'they']);
    $spouse4ID = $this->individualCreate(['first_name' => 'them']);
    Relationship::create()->setValues([
      'contact_id_a' => $spouse1ID,
      'contact_id_b' => $spouse2ID,
      'relationship_type_id' => $relationshipTypeID,
    ])->execute();

    $this->contactMembershipCreate([
      'contact_id' => $spouse1ID,
      'start_date' => date('Y-m-d'),
      'end_date' => '+1 year',
    ]);

    $this->callAPISuccessGetSingle('Membership', [
      'contact_id' => $spouse2ID,
      'membership_type_id' => 'General',
    ]);

    $this->callAPISuccessGetSingle('Membership', [
      'contact_id' => $spouse1ID,
      'membership_type_id' => 'General',
    ]);
    // Add another Spouse
    Relationship::create()->setValues([
      'contact_id_a' => $spouse3ID,
      'contact_id_b' => $spouse1ID,
      'relationship_type_id' => $relationshipTypeID,
    ])->execute();
    $this->callAPISuccessGetSingle('Membership', [
      'contact_id' => $spouse3ID,
      'membership_type_id' => 'General',
    ]);
    $this->callAPISuccessGetCount('Membership', [], 3);
    Relationship::create()->setValues([
      'contact_id_a' => $spouse1ID,
      'contact_id_b' => $spouse4ID,
      'relationship_type_id' => $relationshipTypeID,
    ])->execute();

    $this->callAPISuccessGetSingle('Membership', [
      'contact_id' => $spouse4ID,
      'membership_type_id' => 'General',
    ]);

    $this->callAPISuccessGetCount('Membership', [], 4);
  }

  /**
   * We are checking for no e-notices + only id & end_date returned
   *
   * @throws \CRM_Core_Exception
   */
  public function testMembershipGetWithReturn(): void {
    $this->contactMembershipCreate($this->_params);
    $result = $this->callAPISuccess('membership', 'get', ['return' => 'end_date']);
    foreach ($result['values'] as $membership) {
      $this->assertEquals(['id', 'end_date'], array_keys($membership));
    }
  }

  ///////////////// civicrm_membership_create methods

  /**
   * Test dates are calculated for pending membership.
   */
  public function testCreatePending(): void {
    $membership = $this->callAPISuccess('Membership', 'create', [
      'contact_id' => $this->individualCreate(),
      'membership_type_id' => 'General',
      'status_id' => 'Pending',
      'sequential' => 1,
    ])['values'][0];
    $this->assertEquals(date('Ymd'), $membership['start_date']);
    $this->assertEquals(date('Ymd', strtotime('+1 year -1 day')), $membership['end_date']);
  }

  /**
   * Test dates are calculated for pending membership.
   */
  public function testCreatePendingWithSkipStatusCalc(): void {
    $membership = $this->callAPISuccess('Membership', 'create', [
      'contact_id' => $this->individualCreate(),
      'membership_type_id' => 'General',
      'status_id' => 'Pending',
      'sequential' => 1,
      'skipStatusCal' => TRUE,
    ])['values'][0];
    $this->assertEquals(date('Ymd'), $membership['start_date']);
    $this->assertEquals(date('Ymd'), $membership['join_date']);
    $this->assertEquals(date('Ymd', strtotime('+1 year -1 day')), $membership['end_date']);
  }

  /**
   * If is_overide is passed in status must also be passed in.
   */
  public function testCreateOverrideNoStatus(): void {
    $params = $this->_params;
    unset($params['status_id']);
    $this->callAPIFailure('membership', 'create', $params);
  }

  public function testMembershipCreateMissingRequired(): void {
    $params = [
      'membership_type_id' => '1',
      'join_date' => '2006-01-21',
      'start_date' => '2006-01-21',
      'end_date' => '2006-12-21',
      'source' => 'Payment',
      'status_id' => '2',
    ];

    $this->callAPIFailure('membership', 'create', $params);
  }

  public function testMembershipCreate(): void {
    $params = [
      'contact_id' => $this->_contactID,
      'membership_type_id' => $this->getMembershipTypeID('General'),
      'join_date' => '2006-01-21',
      'start_date' => '2006-01-21',
      'end_date' => '2006-12-21',
      'source' => 'Payment',
      'is_override' => 1,
      'status_id' => $this->_membershipStatusID,
    ];

    $result = $this->callAPISuccess('membership', 'create', $params);
    $this->getAndCheck($params, $result['id'], $this->_entity);
    $this->assertNotNull($result['id']);
    $this->assertEquals($this->_contactID, $result['values'][$result['id']]['contact_id'], " in line " . __LINE__);
    $this->assertEquals($result['id'], $result['values'][$result['id']]['id'], " in line " . __LINE__);
  }

  /**
   * Check for useful message if contact doesn't exist
   */
  public function testMembershipCreateWithInvalidContact(): void {
    $params = [
      'contact_id' => 999,
      'membership_type_id' => 'General',
      'join_date' => '2006-01-21',
      'start_date' => '2006-01-21',
      'end_date' => '2006-12-21',
      'source' => 'Payment',
      'is_override' => 1,
      'status_id' => $this->_membershipStatusID,
    ];

    $this->callAPIFailure('membership', 'create', $params,
      'contact_id is not valid : 999'
    );
  }

  /**
   * Check with complete array + custom field
   * Note that the test is written on purpose without any
   * variables specific to participant so it can be replicated into other entities
   * and / or moved to the automated test suite
   */
  public function testCreateWithCustom(): void {
    $ids = $this->entityCustomGroupWithSingleFieldCreate(__FUNCTION__, __FILE__);

    $params = $this->_params;
    $params['custom_' . $ids['custom_field_id']] = "custom string";

    $result = $this->callAPISuccess($this->_entity, 'create', $params);
    $check = $this->callAPISuccess($this->_entity, 'get', [
      'id' => $result['id'],
      'contact_id' => $this->_contactID,
    ]);
    $this->assertEquals("custom string", $check['values'][$result['id']]['custom_' . $ids['custom_field_id']], ' in line ' . __LINE__);
  }

  /**
   * Search on custom field value.
   */
  public function testSearchWithCustomDataCRM16036(): void {
    // Create a custom field on membership
    $ids = $this->entityCustomGroupWithSingleFieldCreate(__FUNCTION__, __FILE__);

    // Create a new membership, but don't assign anything to the custom field.
    $this->callAPISuccess('Membership', 'create', $this->_params);

    // search memberships with CRM-16036 as custom field value.
    // Since we did not touch the custom field of any membership,
    // this should not return any results.
    $check = $this->callAPISuccess('Membership', 'get', [
      'custom_' . $ids['custom_field_id'] => 'CRM-16036',
    ]);
    $this->assertEquals(0, $check['count']);
  }

  /**
   * Test civicrm_contact_memberships_create with membership id (edit
   * membership).
   *
   * @dataProvider versionThreeAndFour
   */
  public function testMembershipCreateWithId($version): void {
    $this->_apiversion = $version;
    $membershipID = $this->contactMembershipCreate($this->_params);
    $params = [
      'id' => $membershipID,
      'contact_id' => $this->_contactID,
      'membership_type_id' => 'General',
      'join_date' => '2006-01-21',
      'start_date' => '2006-01-21',
      'end_date' => '2006-12-21',
      'source' => 'Payment',
      'is_override' => 1,
      'status_id' => $this->_membershipStatusID,
    ];

    $result = $this->callAPISuccess('membership', 'create', $params);

    //Update Status and check activities created.
    $updateStatus = [
      'id' => $result['id'],
      'status_id' => CRM_Core_PseudoConstant::getKey('CRM_Member_BAO_Membership', 'status_id', 'Cancelled'),
    ];
    $this->callAPISuccess('Membership', 'create', $updateStatus);
    $activities = CRM_Activity_BAO_Activity::getContactActivity($this->_contactID);
    $this->assertEquals(2, count($activities));
    $activityNames = array_flip(CRM_Utils_Array::collect('activity_name', $activities));
    $this->assertArrayHasKey('Membership Signup', $activityNames);
    $this->assertArrayHasKey('Change Membership Status', $activityNames);

    $this->callAPISuccess('Membership', 'Delete', [
      'id' => $result['id'],
    ]);
    $this->assertEquals($result['id'], $membershipID, "in line " . __LINE__);
  }

  /**
   * Test civicrm_contact_memberships_create with membership id (edit
   * membership).
   * success expected.
   */
  public function testMembershipCreateUpdateWithIdNoContact(): void {
    $membershipID = $this->contactMembershipCreate($this->_params);
    $params = [
      'id' => $membershipID,
      'membership_type_id' => $this->_membershipTypeID,
      'contact_id' => $this->_contactID,
      'join_date' => '2006-01-21',
      'start_date' => '2006-01-21',
      'end_date' => '2006-12-21',
      'source' => 'Payment',
      'is_override' => 1,
      'status_id' => $this->_membershipStatusID,
    ];

    $result = $this->callAPISuccess('membership', 'create', $params);
    $this->callAPISuccess('Membership', 'Delete', [
      'id' => $result['id'],
    ]);

    $this->assertEquals($result['id'], $membershipID, "in line " . __LINE__);
  }

  /**
   * Test civicrm_contact_memberships_create with membership id (edit
   * membership).
   * success expected.
   */
  public function testMembershipCreateUpdateWithIdNoDates(): void {
    $membershipID = $this->contactMembershipCreate($this->_params);
    $params = [
      'id' => $membershipID,
      'contact_id' => $this->_contactID,
      'membership_type_id' => $this->_membershipTypeID,
      'source' => 'Payment',
      'is_override' => 1,
      'status_id' => $this->_membershipStatusID,
    ];

    $result = $this->callAPISuccess('membership', 'create', $params);
    $this->callAPISuccess('Membership', 'Delete', [
      'id' => $result['id'],
    ]);
    $this->assertEquals($result['id'], $membershipID, "in line " . __LINE__);
  }

  /**
   * Test civicrm_contact_memberships_create with membership id (edit
   * membership).
   * success expected.
   */
  public function testMembershipCreateUpdateWithIdNoDatesNoType(): void {
    $membershipID = $this->contactMembershipCreate($this->_params);
    $params = [
      'id' => $membershipID,
      'source' => 'not much here',
      'contact_id' => $this->_contactID,
      'is_override' => 1,
      'status_id' => $this->_membershipStatusID,
    ];

    $result = $this->callAPISuccess('membership', 'create', $params);
    $this->callAPISuccess('Membership', 'Delete', [
      'id' => $result['id'],
    ]);
    $this->assertEquals($result['id'], $membershipID, "in line " . __LINE__);
  }

  /**
   * Test civicrm_contact_memberships_create with membership id (edit
   * membership).
   * success expected.
   */
  public function testMembershipCreateUpdateWithIDAndSource(): void {
    $membershipID = $this->contactMembershipCreate($this->_params);
    $params = [
      'id' => $membershipID,
      'source' => 'changed',
      'contact_id' => $this->_contactID,
      'status_id' => $this->_membershipStatusID,
      'membership_type_id' => $this->_membershipTypeID,
      'skipStatusCal' => 1,
    ];
    $result = $this->callAPISuccess('membership', 'create', $params);
    $this->assertEquals($result['id'], $membershipID, "in line " . __LINE__);
    $this->callAPISuccess('Membership', 'Delete', [
      'id' => $result['id'],
    ]);
  }

  /**
   * Change custom field using update.
   */
  public function testUpdateWithCustom(): void {
    $ids = $this->entityCustomGroupWithSingleFieldCreate(__FUNCTION__, __FILE__);

    $params = $this->_params;
    $params['custom_' . $ids['custom_field_id']] = "custom string";
    $result = $this->callAPISuccess($this->_entity, 'create', $params);
    $result = $this->callAPISuccess($this->_entity, 'create', [
      'id' => $result['id'],
      'custom_' . $ids['custom_field_id'] => "new custom",
    ]);
    $check = $this->callAPISuccess($this->_entity, 'get', [
      'id' => $result['id'],
      'contact_id' => $this->_contactID,
    ]);

    $this->assertEquals("new custom", $check['values'][$result['id']]['custom_' . $ids['custom_field_id']], ' in line ' . __LINE__);
    $this->callAPISuccess('Membership', 'Delete', [
      'id' => $check['id'],
    ]);

    $this->customFieldDelete($ids['custom_field_id']);
    $this->customGroupDelete($ids['custom_group_id']);
  }

  /**
   * per CRM-15746 check that the id can be altered in an update hook
   */
  public function testMembershipUpdateCreateHookCRM15746(): void {
    $this->hookClass->setHook('civicrm_pre', [$this, 'hook_civicrm_pre_update_create_membership']);
    $result = $this->callAPISuccess('membership', 'create', $this->_params);
    $this->callAPISuccess('membership', 'create', ['id' => $result['id'], 'end_date' => '1 year ago']);
    $this->callAPISuccessGetCount('membership', [], 2);
    $this->hookClass->reset();
    $this->callAPISuccess('membership', 'create', ['id' => $result['id'], 'end_date' => '1 year ago']);
    $this->callAPISuccessGetCount('membership', [], 2);
  }

  /**
   * Custom hook for update membership.
   *
   * @param string $op
   * @param object $objectName
   * @param int $id
   * @param array $params
   *
   * @throws \Exception
   */
  public function hook_civicrm_pre_update_create_membership($op, $objectName, $id, &$params) {
    if ($objectName === 'Membership' && $op === 'edit') {
      $existingMembership = $this->callAPISuccessGetSingle('membership', ['id' => $params['id']]);
      unset($params['id'], $params['membership_id']);
      $params['join_date'] = $params['membership_start_date'] = $params['start_date'] = date('Ymd000000', strtotime($existingMembership['start_date']));
      $params = array_merge($existingMembership, $params);
      $params['id'] = NULL;
    }
  }

  /**
   * Test civicrm_contact_memberships_create Invalid membership data.
   * Error expected.
   */
  public function testMembershipCreateInvalidMemData(): void {
    //membership_contact_id as string
    $params = [
      'membership_contact_id' => 'Invalid',
      'membership_type_id' => $this->_membershipTypeID,
      'join_date' => '2011-01-21',
      'start_date' => '2010-01-21',
      'end_date' => '2008-12-21',
      'source' => 'Payment',
      'is_override' => 1,
      'status_id' => $this->_membershipStatusID,
    ];

    $this->callAPIFailure('membership', 'create', $params);

    //membership_contact_id which is no in contact table
    $params['membership_contact_id'] = 999;
    $this->callAPIFailure('membership', 'create', $params);

    //invalid join date
    unset($params['membership_contact_id']);
    $params['join_date'] = "invalid";
    $this->callAPIFailure('Membership', 'Create', $params);
  }

  /**
   * Test civicrm_contact_memberships_create with membership_contact_id
   * membership).
   * Success expected.
   */
  public function testMembershipCreateWithMemContact(): void {
    $params = [
      'membership_contact_id' => $this->_contactID,
      'membership_type_id' => $this->_membershipTypeID,
      'join_date' => '2011-01-21',
      'start_date' => '2010-01-21',
      'end_date' => '2008-12-21',
      'source' => 'Payment',
      'is_override' => 1,
      'status_id' => $this->_membershipStatusID,
    ];

    $result = $this->callAPISuccess('membership', 'create', $params);

    $this->callAPISuccess('Membership', 'Delete', [
      'id' => $result['id'],
    ]);
  }

  /**
   * Test civicrm_contact_memberships_create with membership_contact_id
   * membership).
   * Success expected.
   */
  public function testMembershipCreateValidMembershipTypeString(): void {
    $params = [
      'membership_contact_id' => $this->_contactID,
      'membership_type_id' => 'General',
      'join_date' => '2011-01-21',
      'start_date' => '2010-01-21',
      'end_date' => '2008-12-21',
      'source' => 'Payment',
      'is_override' => 1,
      'status_id' => $this->_membershipStatusID,
    ];

    $result = $this->callAPISuccess('membership', 'create', $params);
    $this->assertEquals($this->getMembershipTypeID('General'), $result['values'][$result['id']]['membership_type_id']);
  }

  /**
   * Test civicrm_contact_memberships_create with membership_contact_id
   * membership).
   * Success expected.
   */
  public function testMembershipCreateInValidMembershipTypeString(): void {
    $params = [
      'membership_contact_id' => $this->_contactID,
      'membership_type_id' => 'invalid',
      'join_date' => '2011-01-21',
      'start_date' => '2010-01-21',
      'end_date' => '2008-12-21',
      'source' => 'Payment',
      'is_override' => 1,
      'status_id' => $this->_membershipStatusID,
    ];

    $this->callAPIFailure('membership', 'create', $params);
  }

  /**
   * Test that if membership join date is not set it defaults to today.
   */
  public function testEmptyJoinDate(): void {
    unset($this->_params['join_date'], $this->_params['is_override']);
    $result = $this->callAPISuccess($this->_entity, 'create', $this->_params);
    $result = $this->callAPISuccess($this->_entity, 'getsingle', ['id' => $result['id']]);
    $this->assertEquals(date('Y-m-d'), $result['join_date']);
    $this->assertEquals('2009-01-21', $result['start_date']);
    $this->assertEquals('2009-12-21', $result['end_date']);
  }

  /**
   * Test that if membership start date is not set it defaults to correct end date.
   *  - fixed
   */
  public function testEmptyStartDateFixed(): void {
    unset($this->_params['start_date'], $this->_params['is_override']);
    $this->_params['membership_type_id'] = $this->_membershipTypeID2;
    $result = $this->callAPISuccess($this->_entity, 'create', $this->_params);
    $result = $this->callAPISuccess($this->_entity, 'getsingle', ['id' => $result['id']]);
    $this->assertEquals('2009-01-21', $result['join_date']);
    $this->assertEquals('2008-03-01', $result['start_date']);
    $this->assertEquals('2009-12-21', $result['end_date']);
  }

  /**
   * Test that if membership start date is not set it defaults to correct end date
   *  - fixed
   */
  public function testEmptyStartEndDateFixedOneYear(): void {
    unset($this->_params['start_date'], $this->_params['is_override'], $this->_params['end_date']);
    $this->callAPISuccess('membership_type', 'create', ['id' => $this->_membershipTypeID2, 'duration_interval' => 1]);
    $this->_params['membership_type_id'] = $this->_membershipTypeID2;
    $result = $this->callAPISuccess($this->_entity, 'create', $this->_params);
    $result = $this->callAPISuccess($this->_entity, 'getsingle', ['id' => $result['id']]);
    $this->assertEquals('2009-01-21', $result['join_date']);
    $this->assertEquals('2008-03-01', $result['start_date']);
    $this->assertEquals('2010-02-28', $result['end_date']);
  }

  /**
   * Test that if membership start date is not set it defaults to correct end date for fixed multi year memberships.
   */
  public function testEmptyStartEndDateFixedMultiYear(): void {
    unset($this->_params['start_date'], $this->_params['is_override'], $this->_params['end_date']);
    $this->callAPISuccess('membership_type', 'create', ['id' => $this->_membershipTypeID2, 'duration_interval' => 5]);
    $this->_params['membership_type_id'] = $this->_membershipTypeID2;
    $result = $this->callAPISuccess($this->_entity, 'create', $this->_params);
    $result = $this->callAPISuccess($this->_entity, 'getsingle', ['id' => $result['id']]);
    $this->assertEquals('2009-01-21', $result['join_date']);
    $this->assertEquals('2008-03-01', $result['start_date']);
    $this->assertEquals('2014-02-28', $result['end_date']);
  }

  /**
   * CRM-18503 - Test membership join date is correctly set for fixed memberships.
   *
   * @throws \CRM_Core_Exception
   */
  public function testMembershipJoinDateFixed(): void {
    $memStatus = CRM_Member_PseudoConstant::membershipStatus();
    // Update the fixed membership type to 1 year duration.
    $this->callAPISuccess('membership_type', 'create', ['id' => $this->_membershipTypeID2, 'duration_interval' => 1]);
    $contactId = $this->createLoggedInUser();
    // Create membership with 'Pending' status.
    $params = [
      'contact_id' => $contactId,
      'membership_type_id' => $this->_membershipTypeID2,
      'source' => 'test membership',
      'is_pay_later' => 0,
      'status_id' => 'Pending',
      'skipStatusCal' => 1,
      'is_for_organization' => 1,
    ];
    $membership = $this->callAPISuccess('Membership', 'create', $params);

    // Update membership to 'Completed' and check the dates.
    $memParams = [
      'id' => $membership['id'],
      'contact_id' => $contactId,
      'is_test' => 0,
      'membership_type_id' => $this->_membershipTypeID2,
      'num_terms' => 1,
      'status_id' => 'New',
    ];
    $result = $this->callAPISuccess('Membership', 'create', $memParams);

    // Extend duration interval if join_date exceeds the rollover period.
    $joinDate = date('Y-m-d');
    $year = date('Y');
    $startDate = date('Y-m-d', strtotime(date('Y-03-01')));
    $rollOver = TRUE;
    if (strtotime($startDate) > time()) {
      $rollOver = FALSE;
      $startDate = date('Y-m-d', strtotime(date('Y-03-01') . '- 1 year'));
    }
    $membershipTypeDetails = CRM_Member_BAO_MembershipType::getMembershipType($this->_membershipTypeID2);
    $fixedPeriodRollover = CRM_Member_BAO_MembershipType::isDuringFixedAnnualRolloverPeriod($joinDate, $membershipTypeDetails, $year, $startDate);
    $y = 1;
    if ($fixedPeriodRollover && $rollOver) {
      ++$y;
    }

    $expectedDates = [
      'join_date' => date('Ymd'),
      'start_date' => str_replace('-', '', $startDate),
      'end_date' => date('Ymd', strtotime(date('Y-03-01') . "+ {$y} year - 1 day")),
    ];
    foreach ($result['values'] as $values) {
      foreach ($expectedDates as $date => $val) {
        $this->assertEquals($val, $values[$date], "Failed asserting {$date} values");
      }
    }
  }

  /**
   * Test correct end and start dates are calculated for fixed multi year memberships.
   *
   * The empty start date is calculated to be the start_date (1 Jan prior to the join_date - so 1 Jan 15)
   *
   * In this set our start date is after the start day and before the rollover day so we don't get an extra year
   * and we end one day before the rollover day. Start day is 1 Jan so we end on 31 Dec
   * and we add on 4 years rather than 5 because we are not after the rollover day - so we calculate 31 Dec 2019
   */
  public function testFixedMultiYearDateSetTwoEmptyStartEndDate(): void {
    unset($this->_params['start_date'], $this->_params['is_override'], $this->_params['end_date']);

    $this->callAPISuccess('membership_type', 'create', [
      'id' => $this->_membershipTypeID2,
      'duration_interval' => 5,
      // Ie 1 Jan.
      'fixed_period_start_day' => '101',
      // Ie. 1 Nov.
      'fixed_period_rollover_day' => '1101',
    ]);
    $this->_params['membership_type_id'] = $this->_membershipTypeID2;
    $dates = [
      'join_date' => '28-Jan 2015',
    ];
    $result = $this->callAPISuccess($this->_entity, 'create', array_merge($this->_params, $dates));
    $result = $this->callAPISuccess($this->_entity, 'getsingle', ['id' => $result['id']]);
    $this->assertEquals('2015-01-28', $result['join_date']);
    $this->assertEquals('2015-01-01', $result['start_date']);
    $this->assertEquals('2019-12-31', $result['end_date']);
  }

  /**
   * Test that correct end date is calculated for fixed multi year memberships and start date is not changed.
   *
   * In this set our start date is after the start day and before the rollover day so we don't get an extra year
   * and we end one day before the rollover day. Start day is 1 Jan so we end on 31 Dec
   * and we add on 4 years rather than 5 because we are not after the rollover day - so we calculate 31 Dec 2019
   */
  public function testFixedMultiYearDateSetTwoEmptyEndDate(): void {
    unset($this->_params['start_date'], $this->_params['is_override'], $this->_params['end_date']);

    $this->callAPISuccess('membership_type', 'create', [
      'id' => $this->_membershipTypeID2,
      'duration_interval' => 5,
      // Ie 1 Jan.
      'fixed_period_start_day' => '101',
      // Ie. 1 Nov.
      'fixed_period_rollover_day' => '1101',
    ]);
    $this->_params['membership_type_id'] = $this->_membershipTypeID2;
    $dates = [
      'start_date' => '28-Jan 2015',
      'join_date' => '28-Jan 2015',
    ];
    $result = $this->callAPISuccess($this->_entity, 'create', array_merge($this->_params, $dates));
    $result = $this->callAPISuccess($this->_entity, 'getsingle', ['id' => $result['id']]);
    $this->assertEquals('2015-01-28', $result['join_date']);
    $this->assertEquals('2015-01-28', $result['start_date']);
    $this->assertEquals('2019-12-31', $result['end_date']);
  }

  /**
   * Test correct end and start dates are calculated for fixed multi year memberships.
   *
   * The empty start date is calculated to be the start_date (1 Jan prior to the join_date - so 1 Jan 15)
   *
   * In this set our start date is after the start day and before the rollover day so we don't get an extra year
   * and we end one day before the rollover day. Start day is 1 Jan so we end on 31 Dec
   * and we add on <1 years rather than > 1 because we are not after the rollover day - so we calculate 31 Dec 2015
   */
  public function testFixedSingleYearDateSetTwoEmptyStartEndDate(): void {
    unset($this->_params['start_date'], $this->_params['is_override'], $this->_params['end_date']);

    $this->callAPISuccess('membership_type', 'create', [
      'id' => $this->_membershipTypeID2,
      'duration_interval' => 1,
      // Ie 1 Jan.
      'fixed_period_start_day' => '101',
      // Ie. 1 Nov.
      'fixed_period_rollover_day' => '1101',
    ]);
    $this->_params['membership_type_id'] = $this->_membershipTypeID2;
    $dates = [
      'join_date' => '28-Jan 2015',
    ];
    $result = $this->callAPISuccess($this->_entity, 'create', array_merge($this->_params, $dates));
    $result = $this->callAPISuccess($this->_entity, 'getsingle', ['id' => $result['id']]);
    $this->assertEquals('2015-01-28', $result['join_date']);
    $this->assertEquals('2015-01-01', $result['start_date']);
    $this->assertEquals('2015-12-31', $result['end_date']);
  }

  /**
   * Test correct end date for fixed single year memberships is calculated and start_date is not changed.
   *
   * In this set our start date is after the start day and before the rollover day so we don't get an extra year
   * and we end one day before the rollover day. Start day is 1 Jan so we end on 31 Dec
   * and we add on <1 years rather than > 1 because we are not after the rollover day - so we calculate 31 Dec 2015
   */
  public function testFixedSingleYearDateSetTwoEmptyEndDate(): void {
    unset($this->_params['start_date'], $this->_params['is_override'], $this->_params['end_date']);

    $this->callAPISuccess('membership_type', 'create', [
      'id' => $this->_membershipTypeID2,
      'duration_interval' => 1,
      // Ie 1 Jan.
      'fixed_period_start_day' => '101',
      // Ie. 1 Nov.
      'fixed_period_rollover_day' => '1101',
    ]);
    $this->_params['membership_type_id'] = $this->_membershipTypeID2;
    $dates = [
      'start_date' => '28-Jan 2015',
      'join_date' => '28-Jan 2015',
    ];
    $result = $this->callAPISuccess($this->_entity, 'create', array_merge($this->_params, $dates));
    $result = $this->callAPISuccess($this->_entity, 'getsingle', ['id' => $result['id']]);
    $this->assertEquals('2015-01-28', $result['join_date']);
    $this->assertEquals('2015-01-28', $result['start_date']);
    $this->assertEquals('2015-12-31', $result['end_date']);
  }

  /**
   * Test that correct end date is calculated for fixed multi year memberships and start date is not changed.
   *
   * In this set our start date is after the start day and after the rollover day so we do get an extra year
   * and we end one day before the rollover day. Start day is 1 Nov so we end on 31 Oct
   * and we add on 1 year we are after the rollover day - so we calculate 31 Oct 2016
   */
  public function testFixedSingleYearDateSetThreeEmptyEndDate(): void {
    unset($this->_params['start_date'], $this->_params['is_override'], $this->_params['end_date']);

    $this->callAPISuccess('membership_type', 'create', [
      'id' => $this->_membershipTypeID2,
      'duration_interval' => 1,
      // Ie. 1 Nov.
      'fixed_period_start_day' => '1101',
      // Ie 1 Jan.
      'fixed_period_rollover_day' => '101',
    ]);
    $this->_params['membership_type_id'] = $this->_membershipTypeID2;
    $dates = [
      'start_date' => '28-Jan 2015',
      'join_date' => '28-Jan 2015',
    ];
    $result = $this->callAPISuccess($this->_entity, 'create', array_merge($this->_params, $dates));
    $result = $this->callAPISuccess($this->_entity, 'getsingle', ['id' => $result['id']]);
    $this->assertEquals('2015-01-28', $result['join_date']);
    $this->assertEquals('2015-01-28', $result['start_date']);
    $this->assertEquals('2016-10-31', $result['end_date']);
  }

  /**
   * Test correct end and start dates are calculated for fixed multi year memberships.
   *
   * The empty start date is calculated to be the start_date (1 Nov prior to the join_date - so 1 Nov 14)
   *
   * In this set our start date is after the start day and after the rollover day so we do get an extra year
   * and we end one day before the rollover day. Start day is 1 Nov so we end on 31 Oct
   * and we add on 1 year we are after the rollover day - so we calculate 31 Oct 2016
   */
  public function testFixedSingleYearDateSetThreeEmptyStartEndDate(): void {
    unset($this->_params['start_date'], $this->_params['is_override'], $this->_params['end_date']);

    $this->callAPISuccess('membership_type', 'create', [
      'id' => $this->_membershipTypeID2,
      'duration_interval' => 1,
      // Ie. 1 Nov.
      'fixed_period_start_day' => '1101',
      // Ie 1 Jan.
      'fixed_period_rollover_day' => '101',
    ]);
    $this->_params['membership_type_id'] = $this->_membershipTypeID2;
    $dates = [
      'join_date' => '28-Jan 2015',
    ];
    $result = $this->callAPISuccess($this->_entity, 'create', array_merge($this->_params, $dates));
    $result = $this->callAPISuccess($this->_entity, 'getsingle', ['id' => $result['id']]);
    $this->assertEquals('2015-01-28', $result['join_date']);
    $this->assertEquals('2014-11-01', $result['start_date']);
    $this->assertEquals('2016-10-31', $result['end_date']);
  }

  /**
   * Test that correct end date is calculated for fixed multi year memberships and start date is not changed.
   *
   * In this set our start date is after the start day and after the rollover day so we do get an extra year
   * and we end one day before the rollover day. Start day is 1 Nov so we end on 31 Oct
   * and we add on 5 years we are after the rollover day - so we calculate 31 Oct 2020
   */
  public function testFixedMultiYearDateSetThreeEmptyEndDate(): void {
    unset($this->_params['start_date'], $this->_params['is_override'], $this->_params['end_date']);

    $this->callAPISuccess('membership_type', 'create', [
      'id' => $this->_membershipTypeID2,
      'duration_interval' => 5,
      // Ie. 1 Nov.
      'fixed_period_start_day' => '1101',
      // Ie 1 Jan.
      'fixed_period_rollover_day' => '101',
    ]);
    $this->_params['membership_type_id'] = $this->_membershipTypeID2;
    $dates = [
      'start_date' => '28-Jan 2015',
      'join_date' => '28-Jan 2015',
    ];
    $result = $this->callAPISuccess($this->_entity, 'create', array_merge($this->_params, $dates));
    $result = $this->callAPISuccess($this->_entity, 'getsingle', ['id' => $result['id']]);
    $this->assertEquals('2015-01-28', $result['join_date']);
    $this->assertEquals('2015-01-28', $result['start_date']);
    $this->assertEquals('2020-10-31', $result['end_date']);
  }

  /**
   * Test correct end and start dates are calculated for fixed multi year memberships.
   *
   * The empty start date is calculated to be the start_date (1 Nov prior to the join_date - so 1 Nov 14)
   *
   * The empty start date is calculated to be the start_date (1 Nov prior to the join_date - so 1 Nov 14)
   * In this set our join date is after the start day and after the rollover day so we do get an extra year
   * and we end one day before the rollover day. Start day is 1 Nov so we end on 31 Oct
   * and we add on 5 years we are after the rollover day - so we calculate 31 Oct 2020
   */
  public function testFixedMultiYearDateSetThreeEmptyStartEndDate(): void {
    unset($this->_params['start_date'], $this->_params['is_override'], $this->_params['end_date']);

    $this->callAPISuccess('membership_type', 'create', [
      'id' => $this->_membershipTypeID2,
      'duration_interval' => 5,
      // Ie. 1 Nov.
      'fixed_period_start_day' => '1101',
      // Ie 1 Jan.
      'fixed_period_rollover_day' => '101',
    ]);
    $this->_params['membership_type_id'] = $this->_membershipTypeID2;
    $dates = [
      'join_date' => '28-Jan 2015',
    ];
    $result = $this->callAPISuccess($this->_entity, 'create', array_merge($this->_params, $dates));
    $result = $this->callAPISuccess($this->_entity, 'getsingle', ['id' => $result['id']]);
    $this->assertEquals('2015-01-28', $result['join_date']);
    $this->assertEquals('2014-11-01', $result['start_date']);
    $this->assertEquals('2020-10-31', $result['end_date']);
  }

  /**
   * Test that if membership start date is not set it defaults to correct end date for fixed single year memberships.
   */
  public function testEmptyStartDateRolling(): void {
    unset($this->_params['start_date'], $this->_params['is_override']);
    $result = $this->callAPISuccess($this->_entity, 'create', $this->_params);
    $result = $this->callAPISuccess($this->_entity, 'getsingle', ['id' => $result['id']]);
    $this->assertEquals('2009-01-21', $result['join_date']);
    $this->assertEquals('2009-01-21', $result['start_date']);
    $this->assertEquals('2009-12-21', $result['end_date']);
  }

  /**
   * Test that if membership end date is not set it defaults to correct end date.
   *  - rolling
   */
  public function testEmptyEndDateFixed(): void {
    unset($this->_params['start_date'], $this->_params['is_override'], $this->_params['end_date']);
    $this->_params['membership_type_id'] = $this->_membershipTypeID2;
    $result = $this->callAPISuccess($this->_entity, 'create', $this->_params);
    $result = $this->callAPISuccess($this->_entity, 'getsingle', ['id' => $result['id']]);
    $this->assertEquals('2009-01-21', $result['join_date']);
    $this->assertEquals('2008-03-01', $result['start_date']);
    $this->assertEquals('2010-02-28', $result['end_date']);
  }

  /**
   * Test that if membership end date is not set it defaults to correct end date.
   *  - rolling
   */
  public function testEmptyEndDateRolling(): void {
    unset($this->_params['is_override'], $this->_params['end_date']);
    $this->_params['membership_type_id'] = $this->_membershipTypeID;
    $result = $this->callAPISuccess($this->_entity, 'create', $this->_params);
    $result = $this->callAPISuccess($this->_entity, 'getsingle', ['id' => $result['id']]);
    $this->assertEquals('2009-01-21', $result['join_date']);
    $this->assertEquals('2009-01-21', $result['start_date']);
    $this->assertEquals('2010-01-20', $result['end_date']);
  }

  /**
   * Test that if dates are set they not over-ridden if id is passed in
   */
  public function testMembershipDatesNotOverridden(): void {
    $result = $this->callAPISuccess($this->_entity, 'create', $this->_params);
    unset($this->_params['end_date'], $this->_params['start_date']);
    $this->_params['id'] = $result['id'];
    $this->callAPISuccess($this->_entity, 'create', $this->_params);
    $result = $this->callAPISuccess($this->_entity, 'getsingle', ['id' => $result['id']]);
    $this->assertEquals('2009-01-21', $result['join_date']);
    $this->assertEquals('2009-01-21', $result['start_date']);
    $this->assertEquals('2009-12-21', $result['end_date']);

  }

  /**
   * Test that a contribution linked to multiple memberships results in all being updated.
   *
   * @throws \CRM_Core_Exception
   */
  public function testMultipleMembershipContribution(): void {
    $this->createMultipleMembershipOrder();
    $this->callAPISuccess('Payment', 'create', [
      'contribution_id' => $this->ids['Contribution'][0],
      'payment_instrument_id' => 'Check',
      'total_amount' => 400,
    ]);
    $memberships = $this->callAPISuccess('membership', 'get')['values'];
    $this->assertCount(2, $memberships);
  }

  /**
   * Test that all membership types are returned when getoptions is called.
   *
   * This test locks in current behaviour where types for all domains are returned. It should possibly be domain
   * specific but that should only be done in conjunction with adding a hook to allow that to be altered as the
   * multisite use case expects the master domain to be able to see all sites.
   *
   * See CRM-17075.
   */
  public function testGetOptionsMembershipTypeID(): void {
    $options = $this->callAPISuccess('Membership', 'getoptions', ['field' => 'membership_type_id']);
    $this->assertEquals('Another one', array_pop($options['values']));
    $this->assertEquals('General', array_pop($options['values']));
    $this->assertEquals(NULL, array_pop($options['values']));
  }

  /**
   * @param array $result
   *
   * @return void
   */
  public function validateGetResult(array $result): void {
    $this->assertEquals($result['contact_id'], $this->_contactID);
    $this->assertEquals($this->getMembershipTypeID('General'), $result['membership_type_id']);
    $this->assertEquals($result['status_id'], $this->ids['MembershipStatus']['test']);
    $this->assertEquals('2009-01-21', $result['join_date']);
    $this->assertEquals('2009-01-21', $result['start_date']);
    $this->assertEquals('2009-12-21', $result['end_date']);
    $this->assertEquals('Payment', $result['source']);
  }

  public function hook_custom($op, $groupID, $entityID, &$params) {
    foreach ($params as $field) {
      $this->assertTrue(array_key_exists('entity_table', $field));
    }
  }

}
