<?php
declare(strict_types=1);

namespace Autogedal\BlackFriday\Test\Unit\Model;

use Autogedal\BlackFriday\Model\Config;
use Autogedal\BlackFriday\Model\ProductProvider;
use Autogedal\BlackFriday\Model\SkuList;
use Magento\Catalog\Helper\Image as ImageHelper;
use Magento\Catalog\Model\Product\Visibility;
use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory;
use Magento\Eav\Model\Config as EavConfig;
use Magento\Framework\App\CacheInterface;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\Pricing\PriceCurrencyInterface;
use Magento\Framework\Serialize\SerializerInterface;
use Magento\InventoryIndexer\Model\StockIndexTableNameResolverInterface;
use Magento\InventorySalesApi\Api\StockResolverInterface;
use Magento\Store\Model\StoreManagerInterface;
use PHPUnit\Framework\TestCase;

/**
 * Selection rules of the "Toate" view (no category chosen): the row of a category and the order of "load more".
 */
class ProductProviderOverviewTest extends TestCase
{
    private ProductProvider $provider;

    protected function setUp(): void
    {
        $this->provider = new ProductProvider(
            $this->createMock(ResourceConnection::class),
            $this->createMock(Config::class),
            $this->createMock(SkuList::class),
            $this->createMock(StoreManagerInterface::class),
            $this->createMock(StockResolverInterface::class),
            $this->createMock(StockIndexTableNameResolverInterface::class),
            $this->createMock(EavConfig::class),
            $this->createMock(ProductCollectionFactory::class),
            $this->createMock(CategoryCollectionFactory::class),
            $this->createMock(Visibility::class),
            $this->createMock(ImageHelper::class),
            $this->createMock(PriceCurrencyInterface::class),
            $this->createMock(CacheInterface::class),
            $this->createMock(SerializerInterface::class)
        );
    }

    public function testAfterTheStartARowHasTheTwoBestsellersThenTheTwoBiggestDiscounts(): void
    {
        $products = $this->products([
            1 => ['rank' => 50, 'pct' => 5],
            2 => ['rank' => 90, 'pct' => 0],
            3 => ['rank' => 70, 'pct' => 10],
            4 => ['rank' => 0, 'pct' => 40],
            5 => ['rank' => 0, 'pct' => 25],
            6 => ['rank' => 0, 'pct' => 30],
        ]);

        // bestsellers by rank: 2 (90), 3 (70); then discounts: 4 (40%), 6 (30%)
        self::assertSame([2, 3, 4, 6], $this->rowIds($products, true));
    }

    public function testAProductAlreadyChosenAsBestsellerIsNotTakenAgainForTheDiscounts(): void
    {
        $products = $this->products([
            1 => ['rank' => 90, 'pct' => 50],   // bestseller with the biggest discount
            2 => ['rank' => 80, 'pct' => 0],
            3 => ['rank' => 0, 'pct' => 20],
            4 => ['rank' => 0, 'pct' => 15],
            5 => ['rank' => 0, 'pct' => 10],
        ]);

        self::assertSame([1, 2, 3, 4], $this->rowIds($products, true));
    }

    public function testFewerThanTwoBestsellersLeaveMorePlacesToTheDiscounts(): void
    {
        $products = $this->products([
            1 => ['rank' => 10, 'pct' => 0],
            2 => ['rank' => 0, 'pct' => 30],
            3 => ['rank' => 0, 'pct' => 20],
            4 => ['rank' => 0, 'pct' => 40],
            5 => ['rank' => 0, 'pct' => 5],
        ]);

        self::assertSame([1, 4, 2, 3], $this->rowIds($products, true));
    }

    public function testWithoutDiscountsTheRowIsFilledWithTheNextBestsellers(): void
    {
        $products = $this->products([
            1 => ['rank' => 10, 'pct' => 0],
            2 => ['rank' => 40, 'pct' => 0],
            3 => ['rank' => 30, 'pct' => 0],
            4 => ['rank' => 20, 'pct' => 0],
            5 => ['rank' => 0, 'pct' => 0],
        ]);

        self::assertSame([2, 3, 4, 1], $this->rowIds($products, true));
    }

    public function testBeforeTheStartARowHasTheFourBestsellersWhateverTheDiscount(): void
    {
        $products = $this->products([
            1 => ['rank' => 10, 'pct' => 60],
            2 => ['rank' => 50, 'pct' => 0],
            3 => ['rank' => 40, 'pct' => 0],
            4 => ['rank' => 30, 'pct' => 0],
            5 => ['rank' => 20, 'pct' => 0],
            6 => ['rank' => 0, 'pct' => 70],
        ]);

        self::assertSame([2, 3, 4, 5], $this->rowIds($products, false));
    }

    public function testOutOfStockProductsAreUsedOnlyWhenTheCategoryHasNothingElse(): void
    {
        $products = $this->products([
            1 => ['rank' => 99, 'pct' => 50, 'salable' => false],   // best on both criteria, but out of stock
            2 => ['rank' => 10, 'pct' => 0],
            3 => ['rank' => 0, 'pct' => 5],
        ]);

        self::assertSame([2, 3, 1], $this->rowIds($products, true));
        self::assertSame([1], $this->rowIds($this->products([1 => ['rank' => 5, 'pct' => 0, 'salable' => false]]), true));
    }

    public function testACategoryWithFewerThanFourProductsShowsThemAll(): void
    {
        self::assertSame([2, 1], $this->rowIds($this->products([1 => ['rank' => 0, 'pct' => 0], 2 => ['rank' => 3, 'pct' => 0]]), true));
        self::assertSame([], $this->rowIds([], true));
    }

    public function testLoadMoreShowsTheBestsellersAndDiscountedFirstThenTheOthersThenOutOfStock(): void
    {
        $products = $this->products([
            1 => ['rank' => 0, 'pct' => 0],
            2 => ['rank' => 10, 'pct' => 0],                        // bestseller
            3 => ['rank' => 0, 'pct' => 15],                        // discounted
            4 => ['rank' => 20, 'pct' => 30, 'salable' => false],   // out of stock: last whatever the criteria
            5 => ['rank' => 0, 'pct' => 0],
            6 => ['rank' => 5, 'pct' => 10],
        ]);

        foreach ([1, 2, 3, 77, 123456] as $seed) {
            $ids = $this->mixedIds($products, true, $seed);
            self::assertEqualsCanonicalizing([2, 3, 6], array_slice($ids, 0, 3), "seed $seed");
            self::assertEqualsCanonicalizing([1, 5], array_slice($ids, 3, 2), "seed $seed");
            self::assertSame(4, $ids[5], "seed $seed");
        }
    }

    public function testBeforeTheStartOnlyTheBestsellersComeFirstInLoadMore(): void
    {
        $products = $this->products([
            1 => ['rank' => 0, 'pct' => 40],   // the discount is not known on the page before the start
            2 => ['rank' => 10, 'pct' => 0],
            3 => ['rank' => 0, 'pct' => 0],
        ]);

        self::assertSame(2, $this->mixedIds($products, false, 9)[0]);
    }

    public function testTheRandomOrderStaysTheSameForAVisitorAndChangesBetweenVisitors(): void
    {
        $products = $this->products(array_fill_keys(range(1, 40), ['rank' => 0, 'pct' => 0]));

        self::assertSame($this->mixedIds($products, true, 4242), $this->mixedIds($products, true, 4242));
        self::assertNotSame($this->mixedIds($products, true, 4242), $this->mixedIds($products, true, 4243));
        self::assertEqualsCanonicalizing(range(1, 40), $this->mixedIds($products, true, 4242));
    }

    public function testTheDiscountUsesTheRoundingOfTheSiteLabel(): void
    {
        $percent = new \ReflectionMethod($this->provider, 'percent');

        self::assertSame(36, $percent->invoke($this->provider, 779.0, 499.0));   // 35.94 -> 36, like the card
        self::assertSame(10, $percent->invoke($this->provider, 189.0, 170.1));
        self::assertSame(0, $percent->invoke($this->provider, 100.0, 100.0));
        self::assertSame(0, $percent->invoke($this->provider, 0.0, 0.0));
    }

    /**
     * @param array<int, array{rank: float|int, pct: int, salable?: bool}> $definitions
     * @return array<int, array<string, mixed>> as in the dataset
     */
    private function products(array $definitions): array
    {
        $products = [];
        foreach ($definitions as $id => $definition) {
            $products[$id] = [
                'id' => $id,
                'tabs' => [59],
                'brand' => 0,
                'model' => 0,
                'attrs' => [],
                'salable' => $definition['salable'] ?? true,
                'rank' => (float)$definition['rank'],
                'pct' => $definition['pct'],
            ];
        }
        return $products;
    }

    /** @return int[] */
    private function rowIds(array $products, bool $live): array
    {
        return (new \ReflectionMethod($this->provider, 'rowIds'))->invoke($this->provider, $products, $live);
    }

    /** @return int[] */
    private function mixedIds(array $products, bool $live, int $seed): array
    {
        return (new \ReflectionMethod($this->provider, 'mixedIds'))->invoke($this->provider, $products, $live, $seed);
    }
}
