<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

namespace Magento\Tax\Api;

use Magento\Framework\Api\FilterBuilder;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\Api\SortOrderBuilder;
use Magento\TestFramework\Helper\Bootstrap;
use Magento\TestFramework\TestCase\WebapiAbstract;
use Magento\Webapi\Model\Rest\Config as HttpConstants;

/**
 * Tax Rule Repository API test
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 */
class TaxRuleRepositoryInterfaceTest extends WebapiAbstract
{
    const SERVICE_NAME = "taxTaxRuleRepositoryV1";
    const SERVICE_VERSION = "V1";
    const RESOURCE_PATH = "/V1/taxRules";

    /** @var \Magento\Tax\Model\Calculation\Rate[] */
    private $fixtureTaxRates;

    /** @var \Magento\Tax\Model\ClassModel[] */
    private $fixtureTaxClasses;

    /** @var \Magento\Tax\Model\Calculation\Rule[] */
    private $fixtureTaxRules;

    /** @var FilterBuilder */
    private $filterBuilder;

    /** @var SearchCriteriaBuilder */
    private $searchCriteriaBuilder;

    /** @var  \Magento\Tax\Api\Data\TaxClassInterface | \Magento\Tax\Model\ClassModel */
    private $taxClass;

    /** @var  \Magento\Tax\Api\Data\TaxRateInterface | \Magento\Tax\Model\Calculation\Rate */
    private $taxRate;

    /** @var  SortOrderBuilder */
    private $sortOrderBuilder;

    /**
     * Execute per test initialization.
     */
    protected function setUp(): void
    {
        $this->searchCriteriaBuilder = Bootstrap::getObjectManager()->create(
            \Magento\Framework\Api\SearchCriteriaBuilder::class
        );
        $this->filterBuilder = Bootstrap::getObjectManager()->create(
            \Magento\Framework\Api\FilterBuilder::class
        );
        $objectManager = Bootstrap::getObjectManager();

        $this->searchCriteriaBuilder = $objectManager->create(
            \Magento\Framework\Api\SearchCriteriaBuilder::class
        );
        $this->filterBuilder = $objectManager->create(
            \Magento\Framework\Api\FilterBuilder::class
        );
        $this->sortOrderBuilder = $objectManager->create(
            SortOrderBuilder::class
        );
        $this->taxClass = $objectManager->create(
            \Magento\Tax\Api\Data\TaxClassInterface::class
        );

        $this->taxRate = $objectManager->create(
            \Magento\Tax\Api\Data\TaxRateInterface::class
        );

        /** Initialize tax classes, tax rates and tax rules defined in fixture Magento/Tax/_files/tax_classes.php */
        $this->getFixtureTaxRates();
        $this->getFixtureTaxClasses();
        $this->getFixtureTaxRules();
    }

    protected function tearDown(): void
    {
        $taxRules = $this->getFixtureTaxRules();
        if (count($taxRules)) {
            $taxRates = $this->getFixtureTaxRates();
            $taxClasses = $this->getFixtureTaxClasses();
            foreach ($taxRules as $taxRule) {
                $taxRule->delete();
            }
            foreach ($taxRates as $taxRate) {
                $taxRate->delete();
            }
            foreach ($taxClasses as $taxClass) {
                $taxClass->delete();
            }
        }
    }

    /**
     * @magentoApiDataFixture Magento/Tax/_files/tax_classes.php
     */
    public function testDeleteTaxRule()
    {
        $fixtureRule = $this->getFixtureTaxRules()[0];
        $taxRuleId = $fixtureRule->getId();
        $serviceInfo = [
            'rest' => [
                'resourcePath' => self::RESOURCE_PATH . "/$taxRuleId",
                'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_DELETE,
            ],
            'soap' => [
                'service' => self::SERVICE_NAME,
                'serviceVersion' => self::SERVICE_VERSION,
                'operation' => self::SERVICE_NAME . 'DeleteById',
            ],
        ];
        $requestData = ['ruleId' => $taxRuleId];
        $result = $this->_webApiCall($serviceInfo, $requestData);
        $this->assertTrue($result);
        /** Ensure that tax rule was actually removed from DB */
        /** @var \Magento\Tax\Model\Calculation\Rule $taxRule */
        $taxRate = Bootstrap::getObjectManager()->create(\Magento\Tax\Model\Calculation\Rate::class);
        $this->assertNull($taxRate->load($taxRuleId)->getId(), 'Tax rule was not deleted from DB.');
    }

    public function testCreateTaxRule()
    {
        $serviceInfo = [
            'rest' => [
                'resourcePath' => self::RESOURCE_PATH,
                'httpMethod' => HttpConstants::HTTP_METHOD_POST,
            ],
            'soap' => [
                'service' => self::SERVICE_NAME,
                'serviceVersion' => self::SERVICE_VERSION,
                'operation' => self::SERVICE_NAME . 'Save',
            ],
        ];
        $requestData = [
            'rule' => [
                'code' => 'Test Rule ' . microtime(),
                'position' => 10,
                'priority' => 5,
                'customer_tax_class_ids' => [3],
                'product_tax_class_ids' => [2],
                'tax_rate_ids' => [1, 2],
                'calculate_subtotal' => 1,
            ],
        ];
        $taxRuleData = $this->_webApiCall($serviceInfo, $requestData);
        $this->assertArrayHasKey('id', $taxRuleData, "Tax rule ID is expected");
        $this->assertGreaterThan(0, $taxRuleData['id']);
        $taxRuleId = $taxRuleData['id'];
        unset($taxRuleData['id']);
        $this->assertEquals($requestData['rule'], $taxRuleData, "Tax rule is created with invalid data.");
        /** Ensure that tax rule was actually created in DB */
        /** @var \Magento\Tax\Model\Calculation\Rule $taxRule */
        $taxRule = Bootstrap::getObjectManager()->create(\Magento\Tax\Model\Calculation\Rule::class);
        $this->assertEquals($taxRuleId, $taxRule->load($taxRuleId)->getId(), 'Tax rule was not created in DB.');
        $taxRule->delete();
    }

    public function testCreateTaxRuleInvalidTaxClassIds()
    {
        $serviceInfo = [
            'rest' => [
                'resourcePath' => self::RESOURCE_PATH,
                'httpMethod' => HttpConstants::HTTP_METHOD_POST,
            ],
            'soap' => [
                'service' => self::SERVICE_NAME,
                'serviceVersion' => self::SERVICE_VERSION,
                'operation' => self::SERVICE_NAME . 'Save',
            ],
        ];
        $requestData = [
            'rule' => [
                'code' => 'Test Rule ' . microtime(),
                'position' => 10,
                'priority' => 5,
                'customer_tax_class_ids' => [2],
                'product_tax_class_ids' => [3],
                'tax_rate_ids' => [1, 2],
                'calculate_subtotal' => 1,
            ],
        ];

        try {
            $this->_webApiCall($serviceInfo, $requestData);
            $this->fail('Did not throw expected InputException');
        } catch (\SoapFault $e) {
            $this->assertStringContainsString(
                'No such entity with customer_tax_class_ids = %fieldValue',
                $e->getMessage()
            );
        } catch (\Exception $e) {
            $this->assertStringContainsString(
                'No such entity with customer_tax_class_ids = %fieldValue',
                $e->getMessage()
            );
        }
    }

    public function testCreateTaxRuleExistingCode()
    {
        $expectedMessage = '%1 already exists.';
        $requestData = [
            'rule' => [
                'code' => 'Test Rule ' . microtime(),
                'position' => 10,
                'priority' => 5,
                'customer_tax_class_ids' => [3],
                'product_tax_class_ids' => [2],
                'tax_rate_ids' => [1, 2],
                'calculate_subtotal' => 0,
            ],
        ];

        $serviceInfo = [
            'rest' => [
                'resourcePath' => self::RESOURCE_PATH,
                'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST,
            ],
            'soap' => [
                'service' => self::SERVICE_NAME,
                'serviceVersion' => self::SERVICE_VERSION,
                'operation' => self::SERVICE_NAME . 'Save',
            ],
        ];
        $newTaxRuleData = $this->_webApiCall($serviceInfo, $requestData);
        try {
            $this->_webApiCall($serviceInfo, $requestData);
            $this->fail('Expected exception was not raised');
        } catch (\SoapFault $e) {
            $this->assertStringContainsString(
                $expectedMessage,
                $e->getMessage(),
                'SoapFault does not contain expected message.'
            );
        } catch (\Exception $e) {
            $errorObj = $this->processRestExceptionResult($e);
            $this->assertEquals($expectedMessage, $errorObj['message']);
            $this->assertEquals(['Code'], $errorObj['parameters']);
        }

        // Clean up the new tax rule so it won't affect other tests
        /** @var \Magento\Tax\Model\Calculation\Rule $taxRule */
        $taxRule = Bootstrap::getObjectManager()->create(\Magento\Tax\Model\Calculation\Rule::class);
        $taxRule->load($newTaxRuleData['id']);
        $taxRule->delete();
    }

    /**
     * @magentoApiDataFixture Magento/Tax/_files/tax_classes.php
     */
    public function testGetTaxRule()
    {
        $fixtureRule = $this->getFixtureTaxRules()[0];
        $taxRuleId = $fixtureRule->getId();

        $serviceInfo = [
            'rest' => [
                'resourcePath' => self::RESOURCE_PATH . "/$taxRuleId",
                'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET,
            ],
            'soap' => [
                'service' => self::SERVICE_NAME,
                'serviceVersion' => self::SERVICE_VERSION,
                'operation' => self::SERVICE_NAME . 'Get',
            ],
        ];

        $expectedRuleData = [
            'id' => $taxRuleId,
            'code' => 'Test Rule Duplicate',
            'priority' => '0',
            'position' => '0',
            'customer_tax_class_ids' => array_values(array_unique($fixtureRule->getCustomerTaxClasses())),
            'product_tax_class_ids' => array_values(array_unique($fixtureRule->getProductTaxClasses())),
            'tax_rate_ids' => array_values(array_unique($fixtureRule->getRates())),
            'calculate_subtotal' => false,
        ];
        $requestData = ['ruleId' => $taxRuleId];
        $result = $this->_webApiCall($serviceInfo, $requestData);
        $this->assertEquals($expectedRuleData, $result);
    }

    /**
     * @magentoApiDataFixture Magento/Tax/_files/tax_classes.php
     */
    public function testSearchTaxRulesSimple()
    {
        // Find rules whose code is 'Test Rule'
        $filter = $this->filterBuilder->setField('code')
            ->setValue('Test Rule')
            ->create();

        $this->searchCriteriaBuilder->addFilters([$filter]);

        $fixtureRule = $this->getFixtureTaxRules()[1];

        $searchData = $this->searchCriteriaBuilder->create()->__toArray();
        $requestData = ['searchCriteria' => $searchData];
        $serviceInfo = [
            'rest' => [
                'resourcePath' => self::RESOURCE_PATH . '/search' . '?' . http_build_query($requestData),
                'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET,
            ],
            'soap' => [
                'service' => self::SERVICE_NAME,
                'serviceVersion' => self::SERVICE_VERSION,
                'operation' => self::SERVICE_NAME . 'GetList',
            ],
        ];

        /** @var \Magento\Framework\Api\SearchResults $searchResults */
        $searchResults = $this->_webApiCall($serviceInfo, $requestData);

        $this->assertEquals(1, $searchResults['total_count']);

        $expectedRuleData = [
            [
                'id' => $fixtureRule->getId(),
                'code' => 'Test Rule',
                'priority' => 0,
                'position' => 0,
                'calculate_subtotal' => 0,
                'customer_tax_class_ids' => array_values(array_unique($fixtureRule->getCustomerTaxClasses())),
                'product_tax_class_ids' => array_values(array_unique($fixtureRule->getProductTaxClasses())),
                'tax_rate_ids' => array_values(array_unique($fixtureRule->getRates())),
            ],
        ];
        $this->assertEquals($expectedRuleData, $searchResults['items']);
    }

    /**
     * @magentoApiDataFixture Magento/Tax/_files/tax_classes.php
     */
    public function testSearchTaxRulesCodeWithJoins()
    {
        $customerTaxClass =  clone $this->taxClass->load('CustomerTaxClass2', 'class_name');
        $productTaxClass = clone $this->taxClass->load('Taxable Goods', 'class_name');
        $taxRate = $this->taxRate->load('*', 'code');

        $filter2 = $this->filterBuilder
            ->setField('customer_tax_class_ids')
            ->setValue($customerTaxClass->getClassId())
            ->create();
        $filter3 = $this->filterBuilder
            ->setField('product_tax_class_ids')
            ->setValue($productTaxClass->getClassId())
            ->create();
        $filter4 = $this->filterBuilder
            ->setField('tax_calculation_rate_id')
            ->setValue($taxRate->getId())
            ->create();
        $sortOrder = $this->sortOrderBuilder
            ->setField('code')
            ->setDirection('DESC')
            ->create();

        $this->searchCriteriaBuilder->addFilters([$filter2, $filter3]);
        $this->searchCriteriaBuilder->addFilters([$filter4]);
        $this->searchCriteriaBuilder->addSortOrder($sortOrder);

        $fixtureRule = $this->getFixtureTaxRules()[1];

        $searchData = $this->searchCriteriaBuilder->create()->__toArray();
        $requestData = ['searchCriteria' => $searchData];
        $serviceInfo = [
            'rest' => [
                'resourcePath' => self::RESOURCE_PATH . '/search' . '?' . http_build_query($requestData),
                'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET,
            ],
            'soap' => [
                'service' => self::SERVICE_NAME,
                'serviceVersion' => self::SERVICE_VERSION,
                'operation' => self::SERVICE_NAME . 'GetList',
            ],
        ];

        /** @var \Magento\Framework\Api\SearchResults $searchResults */
        $searchResults = $this->_webApiCall($serviceInfo, $requestData);

        $this->assertEquals(2, $searchResults['total_count']);

        $expectedRuleData = [
            [
                'id' => $this->getFixtureTaxRules()[0]->getId(),
                'code' => 'Test Rule Duplicate',
                'priority' => 0,
                'position' => 0,
                'calculate_subtotal' => 0,
                'customer_tax_class_ids' => array_values(array_unique($fixtureRule->getCustomerTaxClasses())),
                'product_tax_class_ids' => array_values(array_unique($fixtureRule->getProductTaxClasses())),
                'tax_rate_ids' => array_values(array_unique($fixtureRule->getRates()))
            ],
            [
                'id' => $fixtureRule->getId(),
                'code' => 'Test Rule',
                'priority' => 0,
                'position' => 0,
                'calculate_subtotal' => 0,
                'customer_tax_class_ids' => array_values(array_unique($fixtureRule->getCustomerTaxClasses())),
                'product_tax_class_ids' => array_values(array_unique($fixtureRule->getProductTaxClasses())),
                'tax_rate_ids' => array_values(array_unique($fixtureRule->getRates())),
            ],
        ];
        $this->assertEquals($expectedRuleData, $searchResults['items']);
    }

    public function testGetTaxRuleNotExist()
    {
        $taxRuleId = 37865;
        $serviceInfo = [
            'rest' => [
                'resourcePath' => self::RESOURCE_PATH . "/$taxRuleId",
                'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET,
            ],
            'soap' => [
                'service' => self::SERVICE_NAME,
                'serviceVersion' => self::SERVICE_VERSION,
                'operation' => self::SERVICE_NAME . 'Get',
            ],
        ];
        $requestData = ['ruleId' => $taxRuleId];
        try {
            $this->_webApiCall($serviceInfo, $requestData);
            $this->fail('Expected exception was not raised');
        } catch (\Exception $e) {
            $expectedMessage = 'No such entity with %fieldName = %fieldValue';

            $this->assertStringContainsString(
                $expectedMessage,
                $e->getMessage(),
                "Exception does not contain expected message."
            );
        }
    }

    /**
     * @magentoApiDataFixture Magento/Tax/_files/tax_classes.php
     */
    public function testUpdateTaxRule()
    {
        $fixtureRule = $this->getFixtureTaxRules()[0];
        $requestData = [
            'rule' => [
                'id' => $fixtureRule->getId(),
                'code' => 'Test Rule ' . microtime(),
                'position' => 10,
                'priority' => 5,
                'customer_tax_class_ids' => [3],
                'product_tax_class_ids' => [2],
                'tax_rate_ids' => [1, 2],
                'calculate_subtotal' => 1,
            ],
        ];
        $serviceInfo = [
            'rest' => [
                'resourcePath' => self::RESOURCE_PATH,
                'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT,
            ],
            'soap' => [
                'service' => self::SERVICE_NAME,
                'serviceVersion' => self::SERVICE_VERSION,
                'operation' => self::SERVICE_NAME . 'Save',
            ],
        ];
        $this->_webApiCall($serviceInfo, $requestData);
        $expectedRuleData = $requestData['rule'];
        /** Ensure that tax rule was actually updated in DB */
        /** @var \Magento\Tax\Model\Calculation $taxCalculation */
        $taxCalculation = Bootstrap::getObjectManager()->create(\Magento\Tax\Model\Calculation::class);
        /** @var \Magento\Tax\Model\Calculation\Rule $taxRule */
        $taxRule = Bootstrap::getObjectManager()->create(
            \Magento\Tax\Model\Calculation\Rule::class,
            ['calculation' => $taxCalculation]
        );
        $taxRuleModel = $taxRule->load($fixtureRule->getId());
        $this->assertEquals($expectedRuleData['id'], $taxRuleModel->getId(), 'Tax rule was not updated in DB.');
        $this->assertEquals(
            $expectedRuleData['code'],
            $taxRuleModel->getCode(),
            'Tax rule code was updated incorrectly.'
        );
        $this->assertEquals(
            $expectedRuleData['position'],
            $taxRuleModel->getPosition(),
            'Tax rule sort order was updated incorrectly.'
        );
        $this->assertEquals(
            $expectedRuleData['priority'],
            $taxRuleModel->getPriority(),
            'Tax rule priority was updated incorrectly.'
        );
        $this->assertEquals(
            $expectedRuleData['customer_tax_class_ids'],
            array_values(array_unique($taxRuleModel->getCustomerTaxClasses())),
            'Customer Tax classes were updated incorrectly'
        );
        $this->assertEquals(
            $expectedRuleData['product_tax_class_ids'],
            array_values(array_unique($taxRuleModel->getProductTaxClasses())),
            'Product Tax classes were updated incorrectly.'
        );
        $this->assertEquals(
            $expectedRuleData['tax_rate_ids'],
            array_values(array_unique($taxRuleModel->getRates())),
            'Tax rates were updated incorrectly.'
        );
    }

    public function testUpdateTaxRuleNotExisting()
    {
        $requestData = [
            'rule' => [
                'id' => 12345,
                'code' => 'Test Rule ' . microtime(),
                'position' => 10,
                'priority' => 5,
                'customer_tax_class_ids' => [3],
                'product_tax_class_ids' => [2],
                'tax_rate_ids' => [1, 2],
            ],
        ];

        $serviceInfo = [
            'rest' => [
                'resourcePath' => self::RESOURCE_PATH,
                'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT,
            ],
            'soap' => [
                'service' => self::SERVICE_NAME,
                'serviceVersion' => self::SERVICE_VERSION,
                'operation' => self::SERVICE_NAME . 'Save',
            ],
        ];
        try {
            $this->_webApiCall($serviceInfo, $requestData);
            $this->fail('Expected exception was not raised');
        } catch (\Exception $e) {
            $expectedMessage = 'No such entity with %fieldName = %fieldValue';

            $this->assertStringContainsString(
                $expectedMessage,
                $e->getMessage(),
                "Exception does not contain expected message."
            );
        }
    }

    /**
     * @magentoApiDataFixture Magento/Tax/_files/tax_classes.php
     */
    public function testSearchTaxRule()
    {
        $fixtureRule = $this->getFixtureTaxRules()[0];

        $filter = $this->filterBuilder->setField('code')
            ->setValue($fixtureRule->getCode())
            ->create();
        $this->searchCriteriaBuilder->addFilters([$filter]);
        $searchData = $this->searchCriteriaBuilder->create()->__toArray();
        $requestData = ['searchCriteria' => $searchData];
        $serviceInfo = [
            'rest' => [
                'resourcePath' => self::RESOURCE_PATH . '/search' . '?' . http_build_query($requestData),
                'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET,
            ],
            'soap' => [
                'service' => self::SERVICE_NAME,
                'serviceVersion' => self::SERVICE_VERSION,
                'operation' => self::SERVICE_NAME . 'GetList',
            ],
        ];
        $searchResults = $this->_webApiCall($serviceInfo, $requestData);
        $this->assertEquals(1, $searchResults['total_count']);
        $this->assertEquals($fixtureRule->getId(), $searchResults['items'][0]["id"]);
        $this->assertEquals($fixtureRule->getCode(), $searchResults['items'][0]['code']);
    }

    /**
     * Get tax rates created in Magento\Tax\_files\tax_classes.php
     *
     * @return \Magento\Tax\Model\Calculation\Rate[]
     */
    private function getFixtureTaxRates()
    {
        if ($this->fixtureTaxRates === null) {
            $this->fixtureTaxRates = [];
            if ($this->getFixtureTaxRules()) {
                $taxRateIds = (array)$this->getFixtureTaxRules()[0]->getRates();
                foreach ($taxRateIds as $taxRateId) {
                    /** @var \Magento\Tax\Model\Calculation\Rate $taxRate */
                    $taxRate = Bootstrap::getObjectManager()->create(\Magento\Tax\Model\Calculation\Rate::class);
                    $this->fixtureTaxRates[] = $taxRate->load($taxRateId);
                }
            }
        }
        return $this->fixtureTaxRates;
    }

    /**
     * Get tax classes created in Magento\Tax\_files\tax_classes.php
     *
     * @return \Magento\Tax\Model\ClassModel[]
     */
    private function getFixtureTaxClasses()
    {
        if ($this->fixtureTaxClasses === null) {
            $this->fixtureTaxClasses = [];
            if ($this->getFixtureTaxRules()) {
                $taxClassIds = array_merge(
                    (array)$this->getFixtureTaxRules()[0]->getCustomerTaxClasses(),
                    (array)$this->getFixtureTaxRules()[0]->getProductTaxClasses()
                );
                foreach ($taxClassIds as $taxClassId) {
                    /** @var \Magento\Tax\Model\ClassModel $taxClass */
                    $taxClass = Bootstrap::getObjectManager()->create(\Magento\Tax\Model\ClassModel::class);
                    $this->fixtureTaxClasses[] = $taxClass->load($taxClassId);
                }
            }
        }
        return $this->fixtureTaxClasses;
    }

    /**
     * Get tax rule created in Magento\Tax\_files\tax_classes.php
     *
     * @return \Magento\Tax\Model\Calculation\Rule[]
     */
    private function getFixtureTaxRules()
    {
        if ($this->fixtureTaxRules === null) {
            $this->fixtureTaxRules = [];
            $taxRuleCodes = ['Test Rule Duplicate', 'Test Rule'];
            foreach ($taxRuleCodes as $taxRuleCode) {
                /** @var \Magento\Tax\Model\Calculation\Rule $taxRule */
                $taxRule = Bootstrap::getObjectManager()->create(\Magento\Tax\Model\Calculation\Rule::class);
                $taxRule->load($taxRuleCode, 'code');
                if ($taxRule->getId()) {
                    $this->fixtureTaxRules[] = $taxRule;
                }
            }
        }
        return $this->fixtureTaxRules;
    }
}
