<?php

/**
 * Created by PhpStorm.
 * User: filip
 * Date: 5/17/16
 * Time: 9:31 AM.
 */

namespace QueryTest;

use KupShop\CatalogBundle\ProductList\FilterParams;
use KupShop\KupShopBundle\Context\UserContext;
use KupShop\KupShopBundle\PriceType\PriceForDiscountPriceType;
use KupShop\KupShopBundle\PriceType\PriceIfOnlyDiscountPriceType;
use KupShop\KupShopBundle\PriceType\PriceTypeInterface;
use KupShop\KupShopBundle\PriceType\PriceWithoutDiscountPriceType;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\PricelistBundle\Context\PricelistContext;
use Query\Filter;
use Query\Operator;
use Query\Product;

class FilterTest extends \DatabaseTestCase
{
    /** @dataProvider data_testVisibilityFilter */
    public function testVisibilityFilter(int $expectedCount, callable $figureSpec, string $type = FilterParams::ENTITY_PRODUCT): void
    {
        $qb = sqlQueryBuilder()
            ->select('p.id')
            ->joinVariationsOnProducts()
            ->from('products', 'p')
            ->where($figureSpec);

        if ($type === FilterParams::ENTITY_VARIATION) {
            $qb->groupBy('p.id, pv.id');
        } else {
            $qb->groupBy('p.id');
        }

        $count = $qb->execute()->rowCount();

        $this->assertEquals($expectedCount, $count);
    }

    public function data_testVisibilityFilter(): iterable
    {
        yield 'Test load visible products' => [8, Filter::isVisible()];
        yield 'Test load hidden products' => [2, Operator::not(Filter::isVisible())];
        yield 'Test load sold out products' => [0, Filter::byFigure('O')];
        yield 'Test load visible and hidden products' => [9, Operator::orX(Filter::isVisible(), Operator::not(Filter::isVisible()))];

        // Variations list - 1 visible variation + 6 visible products = 7
        yield 'Test load visible variation product list' => [8, Filter::isVisible(), FilterParams::ENTITY_VARIATION];
        // Variation list - 1 hidden variation and 1 hidden product
        yield 'Test load hidden variation product list' => [2, Operator::not(Filter::isVisible()), FilterParams::ENTITY_VARIATION];
        // Variation list - 6 products + 3 variation products
        yield 'Test load visible and hidden variation product list' => [10, Operator::orX(Filter::isVisible(), Operator::not(Filter::isVisible())), FilterParams::ENTITY_VARIATION];
    }

    /** @dataProvider dataSet_testByDiscountRange */
    public function testByDiscountRange(\Range $range, ?int $priceListId, ?PriceTypeInterface $originalPriceType, array $expectedIDs): void
    {
        if ($priceListId) {
            Contexts::get(PricelistContext::class)->activate($priceListId);
        }

        if ($originalPriceType) {
            Contexts::clear();
            $this->withMockedUserContext($originalPriceType);
        }

        $IDs = sqlQueryBuilder()
            ->select('p.id')
            ->from('products', 'p')
            ->where(Filter::byDiscountRange($range))
            ->groupBy('p.id')
            ->execute()
            ->fetchFirstColumn();

        $this->assertEquals($expectedIDs, $IDs);
    }

    public function dataSet_testByDiscountRange(): iterable
    {
        yield 'Filter discount range 1-5' => [new \Range(1, 5), null, null, []];
        yield 'Filter discount range 5-12' => [new \Range(5, 12), null, null, [6]];
        yield 'Filter discount range 10-15' => [new \Range(10, 15), null, null, [6]];
        yield 'Filter discount range 10-20' => [new \Range(10, 20), null, null, [6, 7]];
        yield 'Filter discount range 20-35' => [new \Range(20, 35), null, null, [7]];

        yield 'Filter with price list range 1-10' => [new \Range(1, 10), 1, null, []];
        yield 'Filter with price list range 20-30' => [new \Range(20, 30), 1, null, [1]];
        yield 'Filter with price list range 15-20 - discount calculated using price_for_discount' => [new \Range(15, 20), 1, null, [2]];
        yield 'Filter with price list range 15-20 - with explicit discount field' => [new \Range(15, 20), 1, new PriceIfOnlyDiscountPriceType(new PriceForDiscountPriceType()), []];
        yield 'Filter with price list range 30-40 - with explicit discount field' => [new \Range(30, 40), 1, new PriceIfOnlyDiscountPriceType(new PriceForDiscountPriceType()), [3]];

        yield 'Filter not discounted products' => [new \Range(null, 0), null, null, [1, 2, 3, 4, 5, 8]];
        yield 'Filter not discounted products for price list' => [new \Range(null, 0), 1, null, [4, 5, 6, 7, 8]];
        yield 'Filter not discounted products for price list - without price history' => [new \Range(null, 0), 1, new PriceWithoutDiscountPriceType(), [2, 4, 5, 6, 7, 8, 9]];
        yield 'Filter not discounted products for price list - with explicit discount field' => [new \Range(null, 0), 1, new PriceIfOnlyDiscountPriceType(new PriceForDiscountPriceType()), [2, 4, 5, 6, 7, 8, 9]];
    }

    /** @dataProvider dataSet_testDiscountFieldDefinition */
    public function testDiscountFieldDefinition(int $productId, ?int $priceListId, ?PriceTypeInterface $originalPriceType, float $expectedDiscount): void
    {
        if ($priceListId) {
            Contexts::get(PricelistContext::class)->activate($priceListId);
        }

        if ($originalPriceType) {
            $this->withMockedUserContext($originalPriceType);
        }

        $discountFieldDefinition = $this->get(UserContext::class)->getOriginalPriceType()->getDiscountFieldDefinition();

        $discount = sqlQueryBuilder()->select($discountFieldDefinition->getField())
            ->fromProducts()
            ->joinVariationsOnProducts()
            ->where(Operator::equals(['p.id' => $productId]))
            ->andWhere($discountFieldDefinition->getSpec())
            ->execute()
            ->fetchOne();

        $this->assertEqualsWithDelta($expectedDiscount, $discount, 0.1);
    }

    public function dataSet_testDiscountFieldDefinition(): iterable
    {
        yield 'Product with zero price' => [1, null, null, 0];
        yield 'Product with zero discount' => [2, null, null, 0];
        yield 'Product with 10% discount' => [6, null, null, 10];
        yield 'Product with price for discount' => [9, null, null, 100 - (((120 * 0.8) * 100) / 200)];
        yield 'Product with discount from pricelist' => [1, 1, null, 25];
        yield 'Product with price for discount from pricelist' => [2, 1, null, 100 - ((41.3223 * 100) / 49.5868)];
    }

    /**
     * @dataProvider dataSet_testByProducers
     */
    public function testByProducers(array $producerIds, int $expectedCount, bool $negative): void
    {
        $this->assertSame($expectedCount, sqlQueryBuilder()
            ->select('p.id')
            ->from('products', 'p')
            ->andWhere($negative
                ? Operator::not(Filter::byProducers($producerIds))
                : Filter::byProducers($producerIds)
            )
            ->execute()
            ->rowCount()
        );
    }

    public function dataSet_testByProducers(): array
    {
        return [
            [[1], 4, false],
            [[2], 1, false],
            [[3], 0, false],
            [[1, 2, 3], 5, false],
            [[1337], 9, true],
            [[1337], 0, false],
            [[1, 2, 3], 4, true],
        ];
    }

    /**
     * @dataProvider dataSet_testByPriceRange
     */
    public function testByPriceRange(\Range $range, $expectedCount)
    {
        $count = sqlQueryBuilder()
            ->select('p.id')
            ->from('products', 'p')
            ->where(Filter::byPriceRange($range))
            ->execute()
            ->rowCount();

        $this->assertSame($expectedCount, $count);
    }

    public function dataSet_testByPriceRange()
    {
        return [
            [new \Range(5 * 1.21, 5 * 1.21), 0],
            [new \Range(10 * 1.21, 10 * 1.21), 1],
            [new \Range(10 * 1.21, 30 * 1.21), 3],
        ];
    }

    /**
     * @dataProvider dataSet_testByProductPriceRange
     */
    public function testByProductPriceRange(\Range $range, $expectedCount)
    {
        $qb = sqlQueryBuilder()
            ->select('p.id')
            ->from('products', 'p')
            ->where(Filter::byProductPriceRange($range))
            ->groupBy('p.id')
            ->execute();

        $count = $qb->rowCount();
        $this->assertSame($expectedCount, $count);
    }

    public function dataSet_testByProductPriceRange()
    {
        return [
            [new \Range(5 * 1.21, 5 * 1.21), 0],
            [new \Range(30 * 1.21, 30 * 1.21), 1],
            [new \Range(30 * 1.21, 40 * 1.21), 2],
        ];
    }

    public function getDataSet()
    {
        return $this->getJsonDataSetFromFile();
    }

    private function withMockedUserContext(PriceTypeInterface $originalPriceType): void
    {
        $mockedUserContext = $this->getMockBuilder(UserContext::class)
            ->onlyMethods(['loadOriginalPriceType'])
            ->getMock();

        $mockedUserContext->expects(self::once())
            ->method('loadOriginalPriceType')
            ->willReturn($originalPriceType);

        $this->set(UserContext::class, $mockedUserContext);
    }
}
