<?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       |
 +--------------------------------------------------------------------+
 */

/**
 * Class api_v3_OptionValueTest
 * @group headless
 */
class api_v3_OptionValueTest extends CiviUnitTestCase {
  protected $_apiversion = 3;

  public function setUp(): void {
    parent::setUp();
    $this->useTransaction(TRUE);
  }

  public function testGetCount(): void {
    $result = $this->callAPISuccess('option_value', 'getcount', []);
    $this->assertGreaterThan(100, $result);
  }

  public function testGetOptionValueByID(): void {
    $result = $this->callAPISuccess('option_value', 'get', ['id' => 1]);
    $this->assertEquals(1, $result['count']);
    $this->assertEquals(1, $result['id']);
  }

  public function testGetOptionValueByValue(): void {
    $result = $this->callAPISuccess('option_value', 'get', ['option_group_id' => 1, 'value' => '1']);
    $this->assertEquals(1, $result['count']);
    $this->assertEquals(1, $result['id']);
  }

  /**
   * Test limit param.
   */
  public function testGetOptionValueLimit(): void {
    $params = [];
    $result = $this->callAPISuccess('option_value', 'get', $params);
    $this->assertGreaterThan(1, $result['count'], "Check more than one exists In line " . __LINE__);
    $params['options']['limit'] = 1;
    $result = $this->callAPISuccess('option_value', 'get', $params);
    $this->assertEquals(1, $result['count'], "Check only 1 retrieved " . __LINE__);
  }

  /**
   * Test offset param.
   */
  public function testGetOptionValueOffSet(): void {

    $result = $this->callAPISuccess('option_value', 'get', [
      'option_group_id' => 1,
      'value' => '1',
    ]);
    $result2 = $this->callAPISuccess('option_value', 'get', [
      'option_group_id' => 1,
      'value' => '1',
      'options' => ['offset' => 1],
    ]);
    $this->assertGreaterThan($result2['count'], $result['count']);
  }

  /**
   * Test offset param.
   */
  public function testGetSingleValueOptionValueSort(): void {
    $result = $this->callAPISuccess('option_value', 'getsingle', [
      'option_group_id' => 1,
      'options' => [
        'sort' => 'label ASC',
        'limit' => 1,
      ],
    ]);
    $params = [
      'option_group_id' => 1,
      'options' => [
        'sort' => 'label DESC',
        'limit' => 1,
      ],
    ];
    $result2 = $this->callAPISuccess('option_value', 'getsingle', $params);
    $this->assertGreaterThan($result['label'], $result2['label']);
  }

  /**
   * Try to emulate a pagination: fetch the first page of 10 options, then fetch the second page with an offset of 9 (instead of 10) and check the start of the second page is the end of the 1st one.
   */
  public function testGetValueOptionPagination(): void {
    $pageSize = 10;
    $page1 = $this->callAPISuccess('option_value', 'get', ['options' => ['limit' => $pageSize]]);
    $page2 = $this->callAPISuccess('option_value', 'get', [
      'options' => [
        'limit' => $pageSize,
        // if you use it for pagination, option.offset=pageSize*pageNumber
        'offset' => $pageSize - 1,
      ],
    ]);
    $this->assertEquals($pageSize, $page1['count'], "Check only 10 retrieved in the 1st page " . __LINE__);
    $this->assertEquals($pageSize, $page2['count'], "Check only 10 retrieved in the 2nd page " . __LINE__);

    $last = array_pop($page1['values']);
    $first = array_shift($page2['values']);

    $this->assertEquals($first, $last, "the first item of the second page should be the last of the 1st page" . __LINE__);
  }

  public function testGetOptionGroup(): void {
    $params = ['option_group_id' => 1];
    $result = $this->callAPISuccess('option_value', 'get', $params);
    $this->assertGreaterThan(1, $result['count']);
  }

  /**
   * Test that using option_group_name returns more than 1 & less than all
   */
  public function testGetOptionGroupByName(): void {
    $activityTypesParams = ['option_group_name' => 'activity_type', 'option.limit' => 100];
    $params = ['option.limit' => 100];
    $activityTypes = $this->callAPISuccess('option_value', 'get', $activityTypesParams);
    $result = $this->callAPISuccess('option_value', 'get', $params);
    $this->assertGreaterThan(1, $activityTypes['count']);
    $this->assertGreaterThan($activityTypes['count'], $result['count']);
  }

  public function testGetOptionDoesNotExist(): void {
    $result = $this->callAPISuccess('option_value', 'get', ['label' => 'FSIGUBSFGOMUUBSFGMOOUUBSFGMOOBUFSGMOOIIB']);
    $this->assertEquals(0, $result['count']);
  }

  /**
   * Tests legacy adapter for accessing SiteEmailAddress via the OptionValue api
   * @see \Civi\API\Subscriber\SiteEmailLegacyOptionValueAdapter
   */
  public function testLegacyFromEmailAddressOptionGroup(): void {
    $email1 = $this->callAPISuccess('OptionValue', 'create', [
      'sequential' => 1,
      'option_group_id' => 'from_email_address',
      'domain_id' => 1,
      'value' => 1,
      'weight' => 1,
      'label' => '" Legacy Test"   <my@1.com>',
    ])['values'][0];
    $email2 = $this->callAPISuccess('option_value', 'create', [
      'sequential' => 1,
      'option_group_id' => 'from_email_address',
      'domain_id' => 2,
      'weight' => 2,
      'value' => 2,
      'name' => ' "Legacy Test2"<my@2.com> ',
    ])['values'][0];

    $getSingle = $this->callAPISuccess('optionValue', 'getsingle', [
      'option_group_id' => 'from_email_address',
      'value' => $email1['value'],
    ]);
    $this->assertEquals('"Legacy Test" <my@1.com>', $getSingle['label']);

    // Test sorting by weight (really sorts by display_name)
    $result = $this->callAPISuccess('option_value', 'get', [
      'sequential' => 1,
      'option_group_id' => 'from_email_address',
      'options' => ['sort' => 'weight'],
      'value' => ['IN' => [$email1['value'], $email2['value']]],
    ])['values'];
    $this->assertSame('"Legacy Test" <my@1.com>', $result[0]['label']);
    $this->assertSame('1', $result[0]['weight']);
    $this->assertSame('"Legacy Test2" <my@2.com>', $result[1]['label']);
    $this->assertSame('2', $result[1]['weight']);

    // Update email2
    $this->callAPISuccess('option_value', 'create', [
      'id' => $email2['id'],
      'label' => '"An Updated Address" <new@2.com>',
    ]);

    // Sorting by weight really sorts by display_name so the order will be reversed now
    $result = $this->callAPISuccess('option_value', 'get', [
      'sequential' => 1,
      'option_group_id' => 'from_email_address',
      'options' => ['sort' => 'weight'],
      'value' => ['IN' => [$email1['value'], $email2['value']]],
    ])['values'];
    $this->assertSame('"An Updated Address" <new@2.com>', $result[0]['label']);
    $this->assertSame('1', $result[0]['weight']);
    $this->assertSame('"Legacy Test" <my@1.com>', $result[1]['label']);
    $this->assertSame('2', $result[1]['weight']);

    // Confirm email was renamed
    $optionValue = $this->callAPISuccess('option_value', 'get', [
      'label' => '"Legacy Test" <my@2.com>',
      'option_group_id' => 'from_email_address',
    ]);
    $this->assertEquals(0, $optionValue['count']);

    $getValue = $this->callAPISuccess('option_value', 'getvalue', [
      'option_group_id' => 'from_email_address',
      'value' => $email2['value'],
      'return' => 'label',
    ]);
    $this->assertEquals('"An Updated Address" <new@2.com>', $getValue);

    $optionValue = $this->callAPISuccess('option_value', 'get', [
      'sequential' => 1,
      'name' => '"Legacy Test" <my@1.com>',
      'option_group_id' => 'from_email_address',
      'return' => ['domain_id', 'label', 'name'],
      'api.option_value.delete' => [],
    ])['values'][0];
    $this->assertEquals('"Legacy Test" <my@1.com>', $optionValue['label']);
    $this->assertEquals('"Legacy Test" <my@1.com>', $optionValue['name']);
    $this->assertEquals(1, $optionValue['domain_id']);
    $this->assertEquals(1, $optionValue['api.site_email_address.delete']['count']);

    $optionValue = $this->callAPISuccess('option_value', 'get', [
      'sequential' => 1,
      'label' => '"An Updated Address" <new@2.com>',
      'option_group_id' => 'from_email_address',
      'return' => 'domain_id,label',
      'api.option_value.delete' => [],
    ])['values'][0];
    $this->assertEquals('"An Updated Address" <new@2.com>', $optionValue['label']);
    $this->assertEquals(2, $optionValue['domain_id']);
    $this->assertEquals(1, $optionValue['api.site_email_address.delete']['count']);

    // Option was deleted via chaining
    $optionValue = $this->callAPISuccess('option_value', 'get', [
      'label' => '"An Updated Address" <new@2.com>',
      'option_group_id' => 'from_email_address',
    ]);
    $this->assertEquals(0, $optionValue['count']);
  }

  /**
   * Check that component_id is honoured.
   */
  public function testCreateOptionSpecifyComponentID(): void {
    $result = $this->callAPISuccess('option_group', 'get', [
      'name' => 'preferred_communication_method',
      'sequential' => 1,
      'api.option_value.create' => ['component_id' => 2, 'name' => 'my@y.com'],
    ]);
    $this->assertAPISuccess($result);
    $optionValueId = $result['values'][0]['api.option_value.create']['id'];
    $component_id = $this->callAPISuccess('option_value', 'getvalue', [
      'id' => $optionValueId,
      'return' => 'component_id',
    ]);
    $this->assertEquals(2, $component_id);
    $this->callAPISuccess('option_value', 'delete', ['id' => $optionValueId]);
  }

  /**
   * Check that component string is honoured.
   */
  public function testCreateOptionSpecifyComponentString(): void {
    $result = $this->callAPISuccess('option_group', 'get', [
      'name' => 'preferred_communication_method',
      'sequential' => 1,
      'api.option_value.create' => [
        'component_id' => 'CiviContribute',
        'name' => 'my@y.com',
      ],
    ]);
    $this->assertAPISuccess($result);
    $optionValueId = $result['values'][0]['api.option_value.create']['id'];
    $component_id = $this->callAPISuccess('option_value', 'getvalue', [
      'id' => $optionValueId,
      'return' => 'component_id',
    ]);
    $this->assertEquals(2, $component_id);
    $this->callAPISuccess('option_value', 'delete', ['id' => $optionValueId]);
  }

  /**
   * Check that component is honoured when fetching options.
   */
  public function testGetOptionWithComponent(): void {
    $components = Civi::settings()->get('enable_components');
    CRM_Core_BAO_ConfigSetting::enableComponent('CiviContribute');
    $this->callAPISuccess('option_group', 'get', [
      'name' => 'gender',
      'api.option_value.create' => [
        'component_id' => 'CiviContribute',
        'name' => 'Contrib',
      ],
    ]);
    // Verify new option is present
    $genders = $this->callAPISuccess('contact', 'getoptions', [
      'field' => 'gender_id',
      'context' => 'create',
    ]);
    $this->assertContains('Contrib', $genders['values']);

    // Disable relevant component
    CRM_Core_BAO_ConfigSetting::disableComponent('CiviContribute');
    CRM_Core_PseudoConstant::flush();
    // New option should now be hidden for "create" context
    $genders = $this->callAPISuccess('contact', 'getoptions', [
      'field' => 'gender_id',
      'context' => 'create',
    ]);
    $this->assertNotContains('Contrib', $genders['values']);
    // New option should be visible for "get" context even with component disabled
    $genders = $this->callAPISuccess('contact', 'getoptions', [
      'field' => 'gender_id',
      'context' => 'get',
    ]);
    $this->assertContains('Contrib', $genders['values']);

    // Now disable all components and ensure we can still fetch options with no errors
    CRM_Core_BAO_ConfigSetting::setEnabledComponents([]);
    CRM_Core_PseudoConstant::flush();
    // New option should still be hidden for "create" context
    $genders = $this->callAPISuccess('contact', 'getoptions', [
      'field' => 'gender_id',
      'context' => 'create',
    ]);
    $this->assertNotContains('Contrib', $genders['values']);

    // Restore original state
    CRM_Core_BAO_ConfigSetting::setEnabledComponents($components);
  }

  /**
   * Check that domain_id is honoured.
   */
  public function testCRM12133CreateOptionWeightNoValue(): void {
    $optionGroup = $this->callAPISuccess(
      'option_group', 'get', [
        'name' => 'gender',
        'sequential' => 1,
      ]
    );
    $this->assertAPISuccess($optionGroup);
    $params = [
      'option_group_id' => $optionGroup['id'],
      'label' => 'my@y.com',
      'weight' => 3,
    ];
    $optionValue = $this->callAPISuccess('option_value', 'create', $params);
    $this->assertAPISuccess($optionValue);
    $params['weight'] = 4;
    $optionValue2 = $this->callAPISuccess('option_value', 'create', $params);
    $this->assertAPISuccess($optionValue2);
    $options = $this->callAPISuccess('option_value', 'get', ['option_group_id' => $optionGroup['id']]);
    $this->assertNotEquals($options['values'][$optionValue['id']]['value'], $options['values'][$optionValue2['id']]['value']);

    //cleanup
    $this->callAPISuccess('option_value', 'delete', ['id' => $optionValue['id']]);
    $this->callAPISuccess('option_value', 'delete', ['id' => $optionValue2['id']]);
  }

  /**
   * Check that domain_id is honoured.
   */
  public function testCreateOptionNoName(): void {
    $optionGroup = $this->callAPISuccess('option_group', 'get', [
      'name' => 'gender',
      'sequential' => 1,
    ]);

    $params = ['option_group_id' => $optionGroup['id'], 'label' => 'my@y.com'];
    $optionValue = $this->callAPISuccess('option_value', 'create', $params);
    $this->assertAPISuccess($optionValue);
    $this->getAndCheck($params, $optionValue['id'], 'option_value');
  }

  /**
   * Check that pseudoconstant reflects new value added.
   */
  public function testCRM11876CreateOptionPseudoConstantUpdated(): void {
    $optionGroupID = $this->callAPISuccess('option_group', 'getvalue', [
      'name' => 'payment_instrument',
      'return' => 'id',
    ]);
    $newOption = (string) time();
    $apiResult = $this->callAPISuccess('option_value', 'create', [
      'option_group_id' => $optionGroupID,
      'label' => $newOption,
    ]);

    $fields = $this->callAPISuccess('Contribution', 'getoptions', ['field' => 'payment_instrument_id']);
    $this->assertContains($newOption, $fields['values']);

    $this->callAPISuccess('OptionValue', 'delete', ['id' => $apiResult['id']]);

    $fields = $this->callAPISuccess('Contribution', 'getoptions', ['field' => 'payment_instrument_id']);
    $this->assertNotContains($newOption, $fields['values']);
  }

  /**
   * Update option value with 'id' parameter and the value to update
   * and not passing option group id
   */
  public function testUpdateOptionValueNoGroupId(): void {
    // create a option group
    $og = $this->callAPISuccess('option_group', 'create', ['name' => 'our test Option Group', 'is_active' => 1]);
    // create a option value
    $ov = $this->callAPISuccess('option_value', 'create',
      ['option_group_id' => $og['id'], 'label' => 'test option value']
    );
    // update option value without 'option_group_id'
    $res = $this->callAPISuccess('option_value', 'create', ['id' => $ov['id'], 'is_active' => 0]);
    $val = $this->callAPISuccess('option_value', 'getvalue', [
      'id' => $ov['id'],
      'return' => 'is_active',
    ]);
    $this->assertEquals($val, 0, "update with no group id is not proper" . __LINE__);
  }

  /**
   * Update option value with 'id' parameter and the value to update
   * and as well as option group id
   */
  public function testUpdateOptionValueWithGroupId(): void {
    // create a option group
    $og = $this->callAPISuccess('option_group', 'create', [
      'name' => 'our test Option Group for with group id',
      'is_active' => 1,
    ]);
    // create a option value
    $ov = $this->callAPISuccess('option_value', 'create',
      ['option_group_id' => $og['id'], 'label' => 'test option value']
    );
    // update option value without 'option_group_id'
    $this->callAPISuccess('option_value', 'create', [
      'id' => $ov['id'],
      'option_group_id' => $og['id'],
      'is_active' => 0,
    ]);
    $val = $this->callAPISuccess('option_value', 'getvalue', [
      'id' => $ov['id'],
      'return' => 'is_active',
    ]);
    $this->assertEquals($val, 0, "update with group id is not proper " . __LINE__);
  }

  /**
   * CRM-19346 Ensure that Option Values cannot share same value in the same option value group
   */
  public function testCreateOptionValueWithSameValue(): void {
    $og = $this->callAPISuccess('option_group', 'create', [
      'name' => 'our test Option Group for with group id',
      'is_active' => 1,
    ]);
    // create a option value
    $ov = $this->callAPISuccess('option_value', 'create',
      ['option_group_id' => $og['id'], 'label' => 'test option value']
    );
    // update option value without 'option_group_id'
    $this->callAPIFailure('option_value', 'create',
      ['option_group_id' => $og['id'], 'label' => 'Test 2nd option value', 'value' => $ov['values'][$ov['id']]['value']]
    );
  }

  /**
   * CRM-21737 Ensure that language Option Values CAN share same value.
   */
  public function testCreateOptionValueWithSameValueLanguagesException(): void {
    $this->callAPISuccess('option_value', 'create',
      ['option_group_id' => 'languages', 'label' => 'Quasi English', 'name' => 'en_Qu', 'value' => 'en']
    );
    $this->callAPISuccess('option_value', 'create',
      ['option_group_id' => 'languages', 'label' => 'Semi English', 'name' => 'en_Se', 'value' => 'en']
    );

  }

  public function testCreateOptionValueWithSameValueDiffOptionGroup(): void {
    $og = $this->callAPISuccess('option_group', 'create', [
      'name' => 'our test Option Group',
      'is_active' => 1,
    ]);
    // create a option value
    $ov = $this->callAPISuccess('option_value', 'create',
      ['option_group_id' => $og['id'], 'label' => 'test option value']
    );
    $og2 = $this->callAPISuccess('option_group', 'create', [
      'name' => 'our test Option Group 2',
      'is_active' => 1,
    ]);
    // update option value without 'option_group_id'
    $ov2 = $this->callAPISuccess('option_value', 'create',
      ['option_group_id' => $og2['id'], 'label' => 'Test 2nd option value', 'value' => $ov['values'][$ov['id']]['value']]
    );
  }

  /**
   * Test to create and update payment method with financial account.
   */
  public function testCreateUpdateOptionValueForPaymentInstrument(): void {
    $assetFinancialAccountId = $this->callAPISuccessGetValue('FinancialAccount', [
      'return' => "id",
      'financial_account_type_id' => "Asset",
      'options' => ['limit' => 1],
    ]);
    // create new payment method with financial account
    $ov = $this->callAPISuccess('OptionValue', 'create', [
      'financial_account_id' => $assetFinancialAccountId,
      'option_group_id' => "payment_instrument",
      'label' => "Dummy Payment Method",
    ]);

    //check if relationship is created between Payment method and Financial Account
    $this->checkPaymentMethodFinancialAccountRelationship($ov['id'], $assetFinancialAccountId);

    // update payment method to have different non-asset financial Account
    $nonAssetFinancialAccountId = $this->callAPISuccessGetValue('FinancialAccount', [
      'return' => "id",
      'financial_account_type_id' => ['NOT IN' => ["Asset"]],
      'options' => ['limit' => 1],
    ]);
    try {
      $result = $this->callAPISuccess('OptionValue', 'create', [
        'financial_account_id' => $nonAssetFinancialAccountId,
        'id' => $ov['id'],
      ]);
      throw new CRM_Core_Exception(ts('Should throw error.'));
    }
    catch (Exception $e) {
      try {
        $assetAccountRelValue = $this->callAPISuccessGetValue('EntityFinancialAccount', [
          'return' => "account_relationship",
          'entity_table' => "civicrm_option_value",
          'entity_id' => $ov['id'],
          'financial_account_id' => $nonAssetFinancialAccountId,
        ]);
        throw new CRM_Core_Exception(ts('Should throw error.'));
      }
      catch (Exception $e) {
        $this->checkPaymentMethodFinancialAccountRelationship($ov['id'], $assetFinancialAccountId);
      }
    }
    // update payment method to have different asset financial Account
    $assetFinancialAccountId = $this->callAPISuccessGetValue('FinancialAccount', [
      'return' => "id",
      'financial_account_type_id' => "Asset",
      'options' => ['limit' => 1],
      'id' => ['NOT IN' => [$assetFinancialAccountId]],
    ]);
    $result = $this->callAPISuccess('OptionValue', 'create', [
      'financial_account_id' => $assetFinancialAccountId,
      'id' => $ov['id'],
    ]);
    //check if relationship is updated between Payment method and Financial Account
    $this->checkPaymentMethodFinancialAccountRelationship($ov['id'], $assetFinancialAccountId);
  }

  /**
   * Function to check relationship between FA and Payment method.
   *
   * @param int $paymentMethodId
   * @param int $financialAccountId
   */
  protected function checkPaymentMethodFinancialAccountRelationship($paymentMethodId, $financialAccountId) {
    $assetAccountRelValue = $this->callAPISuccessGetValue('EntityFinancialAccount', [
      'return' => "account_relationship",
      'entity_table' => "civicrm_option_value",
      'entity_id' => $paymentMethodId,
      'financial_account_id' => $financialAccountId,
    ]);
    $checkAssetAccountIs = $this->callAPISuccessGetValue('OptionValue', [
      'return' => "id",
      'option_group_id' => "account_relationship",
      'name' => "Asset Account is",
      'value' => $assetAccountRelValue,
    ]);
  }

}
