<?php
namespace Tests\Unit\Domain\Product;

use PHPUnit\Framework\TestCase;
use Domain\Product\ProductRepository;

/**
 * Testy jednostkowe dla ProductRepository
 *
 * Testujemy izolowaną logikę repozytorium z mockami bazy danych
 */
class ProductRepositoryTest extends TestCase
{
    /**
     * Test pobierania ilości produktu - przypadek sukcesu
     */
    public function testGetQuantityReturnsCorrectValue()
    {
        // Arrange (Przygotowanie)
        // Tworzymy mock bazy danych
        $mockDb = $this->createMock(\medoo::class);

        // Konfigurujemy mock - metoda get() zwróci 42
        $mockDb->expects($this->once())
            ->method('get')
            ->with(
                $this->equalTo('pp_shop_products'),
                $this->equalTo('quantity'),
                $this->equalTo(['id' => 123])
            )
            ->willReturn(42);

        $repository = new ProductRepository($mockDb);

        // Act (Działanie)
        $quantity = $repository->getQuantity(123);

        // Assert (Asercja)
        $this->assertEquals(42, $quantity);
        $this->assertIsInt($quantity);
    }

    /**
     * Test pobierania ilości - produkt nie istnieje
     */
    public function testGetQuantityReturnsNullWhenProductNotFound()
    {
        // Arrange
        $mockDb = $this->createMock(\medoo::class);

        // Medoo zwraca false gdy nie znajdzie rekordu
        $mockDb->method('get')->willReturn(false);

        $repository = new ProductRepository($mockDb);

        // Act
        $quantity = $repository->getQuantity(999);

        // Assert
        $this->assertNull($quantity);
    }

    /**
     * Test pobierania produktu po ID
     */
    public function testFindReturnsProductData()
    {
        // Arrange
        $mockDb = $this->createMock(\medoo::class);

        $expectedProduct = [
            'id' => 123,
            'name' => 'Test Product',
            'quantity' => 10,
            'price_brutto' => '99.99'
        ];

        $mockDb->method('get')->willReturn($expectedProduct);

        $repository = new ProductRepository($mockDb);

        // Act
        $product = $repository->find(123);

        // Assert
        $this->assertEquals($expectedProduct, $product);
        $this->assertIsArray($product);
        $this->assertEquals(123, $product['id']);
    }

    /**
     * Test aktualizacji ilości produktu
     */
    public function testUpdateQuantitySuccess()
    {
        // Arrange
        $mockDb = $this->createMock(\medoo::class);

        // Medoo zwraca PDOStatement w przypadku sukcesu
        $mockDb->expects($this->once())
            ->method('update')
            ->with(
                $this->equalTo('pp_shop_products'),
                $this->equalTo(['quantity' => 50]),
                $this->equalTo(['id' => 123])
            )
            ->willReturn($this->createMock(\PDOStatement::class));

        $repository = new ProductRepository($mockDb);

        // Act
        $result = $repository->updateQuantity(123, 50);

        // Assert
        $this->assertTrue($result);
    }

    /**
     * Test pobierania ceny - zwraca cenę regularną gdy brak promocji
     */
    public function testGetPriceReturnsRegularPrice()
    {
        // Arrange
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->method('get')->willReturn([
            'price_brutto' => '99.99',
            'price_brutto_promo' => ''
        ]);

        $repository = new ProductRepository($mockDb);

        // Act
        $price = $repository->getPrice(123);

        // Assert
        $this->assertEquals(99.99, $price);
        $this->assertIsFloat($price);
    }

    /**
     * Test pobierania ceny - zwraca cenę promocyjną gdy jest niższa
     */
    public function testGetPriceReturnsPromoPrice()
    {
        // Arrange
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->method('get')->willReturn([
            'price_brutto' => '99.99',
            'price_brutto_promo' => '79.99'
        ]);

        $repository = new ProductRepository($mockDb);

        // Act
        $price = $repository->getPrice(123);

        // Assert
        $this->assertEquals(79.99, $price);
    }

    /**
     * Test pobierania ceny - zwraca regularną gdy promo jest wyższa
     */
    public function testGetPriceReturnsRegularWhenPromoIsHigher()
    {
        // Arrange
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->method('get')->willReturn([
            'price_brutto' => '49.99',
            'price_brutto_promo' => '79.99'
        ]);

        $repository = new ProductRepository($mockDb);

        // Act
        $price = $repository->getPrice(123);

        // Assert
        $this->assertEquals(49.99, $price);
    }

    /**
     * Test pobierania ceny - zwraca null gdy produkt nie istnieje
     */
    public function testGetPriceReturnsNullWhenNotFound()
    {
        // Arrange
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->method('get')->willReturn(false);

        $repository = new ProductRepository($mockDb);

        // Act
        $price = $repository->getPrice(999);

        // Assert
        $this->assertNull($price);
    }

    /**
     * Test pobierania nazwy produktu - zwraca nazwę
     */
    public function testGetNameReturnsProductName()
    {
        // Arrange
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('get')
            ->with(
                $this->equalTo('pp_shop_products_langs'),
                $this->equalTo('name'),
                $this->equalTo(['AND' => ['product_id' => 123, 'lang_id' => 'pl']])
            )
            ->willReturn('Testowy produkt');

        $repository = new ProductRepository($mockDb);

        // Act
        $name = $repository->getName(123, 'pl');

        // Assert
        $this->assertEquals('Testowy produkt', $name);
        $this->assertIsString($name);
    }

    /**
     * Test pobierania nazwy - produkt nie istnieje
     */
    public function testGetNameReturnsNullWhenNotFound()
    {
        // Arrange
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->method('get')->willReturn(false);

        $repository = new ProductRepository($mockDb);

        // Act
        $name = $repository->getName(999, 'pl');

        // Assert
        $this->assertNull($name);
    }

    /**
     * Test typu zwracanej wartości
     */
    public function testGetQuantityReturnsInteger()
    {
        // Arrange
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->method('get')->willReturn('25'); // Baza może zwrócić string

        $repository = new ProductRepository($mockDb);

        // Act
        $quantity = $repository->getQuantity(123);

        // Assert
        $this->assertIsInt($quantity); // Sprawdzamy czy konwersja na int zadziałała
        $this->assertEquals(25, $quantity);
    }

    /**
     * Test przywracania produktu z archiwum
     */
    public function testUnarchiveUpdatesProductAndChildren()
    {
        // Arrange
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->expects($this->exactly(2))
            ->method('update')
            ->withConsecutive(
                [
                    $this->equalTo('pp_shop_products'),
                    $this->equalTo(['status' => 1, 'archive' => 0]),
                    $this->equalTo(['id' => 123])
                ],
                [
                    $this->equalTo('pp_shop_products'),
                    $this->equalTo(['status' => 1, 'archive' => 0]),
                    $this->equalTo(['parent_id' => 123])
                ]
            );

        $repository = new ProductRepository($mockDb);

        // Act
        $result = $repository->unarchive(123);

        // Assert
        $this->assertTrue($result);
    }

    /**
     * Test przenoszenia produktu do archiwum
     */
    public function testArchiveUpdatesProductAndChildren()
    {
        // Arrange
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->expects($this->exactly(2))
            ->method('update')
            ->withConsecutive(
                [
                    $this->equalTo('pp_shop_products'),
                    $this->equalTo(['status' => 0, 'archive' => 1]),
                    $this->equalTo(['id' => 456])
                ],
                [
                    $this->equalTo('pp_shop_products'),
                    $this->equalTo(['status' => 0, 'archive' => 1]),
                    $this->equalTo(['parent_id' => 456])
                ]
            );

        $repository = new ProductRepository($mockDb);

        // Act
        $result = $repository->archive(456);

        // Assert
        $this->assertTrue($result);
    }

    /**
     * Test że unarchive zwraca bool
     */
    public function testUnarchiveReturnsBool()
    {
        $mockDb = $this->createMock(\medoo::class);
        $repository = new ProductRepository($mockDb);

        $result = $repository->unarchive(1);
        $this->assertIsBool($result);
    }

    /**
     * Test że archive zwraca bool
     */
    public function testArchiveReturnsBool()
    {
        $mockDb = $this->createMock(\medoo::class);
        $repository = new ProductRepository($mockDb);

        $result = $repository->archive(1);
        $this->assertIsBool($result);
    }

    /**
     * Test allProductsForMassEdit - zwraca mapę id => name
     */
    public function testAllProductsForMassEditReturnsMap()
    {
        $mockDb = $this->createMock(\medoo::class);

        $callIndex = 0;
        $mockDb->method('get')
            ->willReturnCallback(function($table, $column, $where) use (&$callIndex) {
                $callIndex++;
                // 1. domyślny język
                if ($table === 'pp_langs') return 'pl';
                // 2. nazwa produktu 1
                if ($table === 'pp_shop_products_langs' && $where['AND']['product_id'] === 1) return 'Produkt A';
                // 3. nazwa produktu 2
                if ($table === 'pp_shop_products_langs' && $where['AND']['product_id'] === 2) return 'Produkt B';
                return false;
            });

        $mockDb->method('select')
            ->with('pp_shop_products', 'id', ['parent_id' => null])
            ->willReturn([1, 2]);

        $repository = new ProductRepository($mockDb);

        $result = $repository->allProductsForMassEdit();

        $this->assertIsArray($result);
        $this->assertCount(2, $result);
        $this->assertEquals('Produkt A', $result[1]);
        $this->assertEquals('Produkt B', $result[2]);
    }

    /**
     * Test allProductsForMassEdit - pusta lista gdy brak produktów
     */
    public function testAllProductsForMassEditEmptyWhenNoProducts()
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->method('get')->willReturn('pl');
        $mockDb->method('select')->willReturn([]);

        $repository = new ProductRepository($mockDb);

        $result = $repository->allProductsForMassEdit();

        $this->assertIsArray($result);
        $this->assertEmpty($result);
    }

    /**
     * Test getProductsByCategory - zwraca listę ID
     */
    public function testGetProductsByCategoryReturnsList()
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->method('select')
            ->with('pp_shop_products_categories', 'product_id', ['category_id' => 5])
            ->willReturn([10, 20, 30]);

        $repository = new ProductRepository($mockDb);

        $result = $repository->getProductsByCategory(5);

        $this->assertIsArray($result);
        $this->assertCount(3, $result);
        $this->assertEquals([10, 20, 30], $result);
    }

    /**
     * Test getProductsByCategory - pusta lista gdy brak produktów w kategorii
     */
    public function testGetProductsByCategoryReturnsEmptyArray()
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->method('select')->willReturn([]);

        $repository = new ProductRepository($mockDb);

        $result = $repository->getProductsByCategory(999);

        $this->assertIsArray($result);
        $this->assertEmpty($result);
    }

    /**
     * Test applyDiscountPercent - zwraca null gdy produkt nie istnieje
     */
    public function testApplyDiscountPercentReturnsNullForInvalidProduct()
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->method('get')->willReturn(false);

        $repository = new ProductRepository($mockDb);

        $result = $repository->applyDiscountPercent(999, 10.0);

        $this->assertNull($result);
    }

    /**
     * Test applyDiscountPercent - poprawny wynik z rabatem
     */
    public function testApplyDiscountPercentReturnsCorrectPrices()
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->method('get')
            ->willReturn([
                'vat' => 23,
                'price_brutto' => '100.00',
                'price_netto' => '81.30',
            ]);

        $mockDb->method('update')
            ->willReturn($this->createMock(\PDOStatement::class));

        // query zwracający puste kombinacje
        $mockStmt = $this->createMock(\PDOStatement::class);
        $mockStmt->method('fetchAll')->willReturn([]);
        $mockDb->method('query')->willReturn($mockStmt);

        $repository = new ProductRepository($mockDb);

        $result = $repository->applyDiscountPercent(1, 10.0);

        $this->assertIsArray($result);
        $this->assertArrayHasKey('price_brutto', $result);
        $this->assertArrayHasKey('price_brutto_promo', $result);
        $this->assertEquals(100.00, $result['price_brutto']);
        $this->assertEquals(90.00, $result['price_brutto_promo']);
    }

    /**
     * Test applyDiscountPercent - rabat 0% nie tworzy ceny promocyjnej
     */
    public function testApplyDiscountPercentZeroPercentNullsPromo()
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->method('get')
            ->willReturn([
                'vat' => 23,
                'price_brutto' => '100.00',
                'price_netto' => '81.30',
            ]);

        $mockDb->method('update')
            ->willReturn($this->createMock(\PDOStatement::class));

        $mockStmt = $this->createMock(\PDOStatement::class);
        $mockStmt->method('fetchAll')->willReturn([]);
        $mockDb->method('query')->willReturn($mockStmt);

        $repository = new ProductRepository($mockDb);

        $result = $repository->applyDiscountPercent(1, 0.0);

        $this->assertIsArray($result);
        $this->assertNull($result['price_brutto_promo']);
    }

    // =========================================================================
    // Frontend methods (migrated from front\factory\ShopProduct)
    // =========================================================================

    /**
     * Test getSkuWithFallback - zwraca SKU bezpośrednio
     */
    public function testGetSkuWithFallbackReturnsSku()
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('get')
            ->with('pp_shop_products', 'sku', ['id' => 10])
            ->willReturn('SKU-ABC');

        $repository = new ProductRepository($mockDb);
        $result = $repository->getSkuWithFallback(10);

        $this->assertEquals('SKU-ABC', $result);
    }

    /**
     * Test getSkuWithFallback - fallback na parent_id
     */
    public function testGetSkuWithFallbackFromParent()
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->method('get')
            ->willReturnCallback(function ($table, $column, $where) {
                if ($column === 'sku' && $where['id'] === 10) return null;
                if ($column === 'parent_id' && $where['id'] === 10) return 5;
                if ($column === 'sku' && $where['id'] === 5) return 'SKU-PARENT';
                return null;
            });

        $repository = new ProductRepository($mockDb);
        $result = $repository->getSkuWithFallback(10, true);

        $this->assertEquals('SKU-PARENT', $result);
    }

    /**
     * Test getSkuWithFallback - zwraca null dla nieprawidłowego ID
     */
    public function testGetSkuWithFallbackReturnsNullForInvalidId()
    {
        $mockDb = $this->createMock(\medoo::class);
        $repository = new ProductRepository($mockDb);

        $this->assertNull($repository->getSkuWithFallback(0));
        $this->assertNull($repository->getSkuWithFallback(-1));
    }

    /**
     * Test getEanWithFallback - zwraca EAN bezpośrednio
     */
    public function testGetEanWithFallbackReturnsEan()
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('get')
            ->with('pp_shop_products', 'ean', ['id' => 10])
            ->willReturn('1234567890123');

        $repository = new ProductRepository($mockDb);
        $result = $repository->getEanWithFallback(10);

        $this->assertEquals('1234567890123', $result);
    }

    /**
     * Test getEanWithFallback - fallback na parent_id
     */
    public function testGetEanWithFallbackFromParent()
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->method('get')
            ->willReturnCallback(function ($table, $column, $where) {
                if ($column === 'ean' && $where['id'] === 10) return null;
                if ($column === 'parent_id' && $where['id'] === 10) return 5;
                if ($column === 'ean' && $where['id'] === 5) return 'EAN-PARENT';
                return null;
            });

        $repository = new ProductRepository($mockDb);
        $result = $repository->getEanWithFallback(10, true);

        $this->assertEquals('EAN-PARENT', $result);
    }

    /**
     * Test isProductActiveCached - zwraca 1 dla aktywnego produktu
     */
    public function testIsProductActiveCachedReturnsOneForActive()
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('get')
            ->with('pp_shop_products', 'status', ['id' => 10])
            ->willReturn(1);

        $repository = new ProductRepository($mockDb);

        $this->assertEquals(1, $repository->isProductActiveCached(10));
    }

    /**
     * Test isProductActiveCached - zwraca 0 dla nieaktywnego produktu
     */
    public function testIsProductActiveCachedReturnsZeroForInactive()
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->method('get')->willReturn(0);

        $repository = new ProductRepository($mockDb);

        $this->assertEquals(0, $repository->isProductActiveCached(10));
    }

    /**
     * Test isProductActiveCached - zwraca 0 dla nieprawidłowego ID
     */
    public function testIsProductActiveCachedReturnsZeroForInvalidId()
    {
        $mockDb = $this->createMock(\medoo::class);
        $repository = new ProductRepository($mockDb);

        $this->assertEquals(0, $repository->isProductActiveCached(0));
        $this->assertEquals(0, $repository->isProductActiveCached(-1));
    }

    /**
     * Test productCategoriesFront - zwraca kategorie
     */
    public function testProductCategoriesFrontReturnsCategories()
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->method('get')
            ->with('pp_shop_products', 'parent_id', ['id' => 10])
            ->willReturn(null);

        $mockStmt = $this->createMock(\PDOStatement::class);
        $mockStmt->method('fetchAll')->willReturn([
            ['category_id' => 1],
            ['category_id' => 5],
        ]);
        $mockDb->method('query')->willReturn($mockStmt);

        $repository = new ProductRepository($mockDb);
        $result = $repository->productCategoriesFront(10);

        $this->assertIsArray($result);
        $this->assertCount(2, $result);
        $this->assertEquals(1, $result[0]['category_id']);
        $this->assertEquals(5, $result[1]['category_id']);
    }

    /**
     * Test productCategoriesFront - fallback na parent_id
     */
    public function testProductCategoriesFrontUsesParentId()
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->method('get')
            ->with('pp_shop_products', 'parent_id', ['id' => 10])
            ->willReturn(5);

        $mockStmt = $this->createMock(\PDOStatement::class);
        $mockStmt->method('fetchAll')->willReturn([['category_id' => 3]]);
        $mockDb->expects($this->once())
            ->method('query')
            ->with(
                $this->equalTo('SELECT category_id FROM pp_shop_products_categories WHERE product_id = :pid'),
                $this->equalTo([':pid' => 5])
            )
            ->willReturn($mockStmt);

        $repository = new ProductRepository($mockDb);
        $result = $repository->productCategoriesFront(10);

        $this->assertCount(1, $result);
        $this->assertEquals(3, $result[0]['category_id']);
    }

    /**
     * Test productCategoriesFront - pusta tablica dla nieprawidłowego ID
     */
    public function testProductCategoriesFrontReturnsEmptyForInvalidId()
    {
        $mockDb = $this->createMock(\medoo::class);
        $repository = new ProductRepository($mockDb);

        $this->assertEquals([], $repository->productCategoriesFront(0));
        $this->assertEquals([], $repository->productCategoriesFront(-1));
    }

    /**
     * Test getWarehouseMessageZero - zwraca wiadomość
     */
    public function testGetWarehouseMessageZeroReturnsMessage()
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('get')
            ->with(
                'pp_shop_products_langs',
                'warehouse_message_zero',
                ['AND' => ['product_id' => 10, 'lang_id' => 'pl']]
            )
            ->willReturn('Produkt niedostępny');

        $repository = new ProductRepository($mockDb);

        $this->assertEquals('Produkt niedostępny', $repository->getWarehouseMessageZero(10, 'pl'));
    }

    /**
     * Test getWarehouseMessageZero - null dla nieprawidłowego ID
     */
    public function testGetWarehouseMessageZeroReturnsNullForInvalidId()
    {
        $mockDb = $this->createMock(\medoo::class);
        $repository = new ProductRepository($mockDb);

        $this->assertNull($repository->getWarehouseMessageZero(0, 'pl'));
    }

    /**
     * Test getWarehouseMessageNonzero - zwraca wiadomość
     */
    public function testGetWarehouseMessageNonzeroReturnsMessage()
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('get')
            ->with(
                'pp_shop_products_langs',
                'warehouse_message_nonzero',
                ['AND' => ['product_id' => 10, 'lang_id' => 'pl']]
            )
            ->willReturn('Wysyłka w 24h');

        $repository = new ProductRepository($mockDb);

        $this->assertEquals('Wysyłka w 24h', $repository->getWarehouseMessageNonzero(10, 'pl'));
    }

    /**
     * Test getWarehouseMessageNonzero - null dla nieprawidłowego ID
     */
    public function testGetWarehouseMessageNonzeroReturnsNullForInvalidId()
    {
        $mockDb = $this->createMock(\medoo::class);
        $repository = new ProductRepository($mockDb);

        $this->assertNull($repository->getWarehouseMessageNonzero(0, 'pl'));
    }

    /**
     * Test topProductIds - zwraca ID aktywnych produktów
     */
    public function testTopProductIdsReturnsActiveProducts()
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockStmt = $this->createMock(\PDOStatement::class);
        $mockStmt->method('fetchAll')->willReturn([
            ['sell_count' => 10, 'parent_product_id' => 1],
            ['sell_count' => 5, 'parent_product_id' => 2],
            ['sell_count' => 3, 'parent_product_id' => 3],
        ]);

        $mockDb->method('query')->willReturn($mockStmt);

        // isProductActiveCached sprawdza status — produkt 2 nieaktywny
        $mockDb->method('get')
            ->willReturnCallback(function ($table, $column, $where) {
                if ($column === 'status') {
                    return $where['id'] === 2 ? 0 : 1;
                }
                return null;
            });

        $repository = new ProductRepository($mockDb);
        $result = $repository->topProductIds(6);

        $this->assertIsArray($result);
        $this->assertContains(1, $result);
        $this->assertNotContains(2, $result);
        $this->assertContains(3, $result);
    }

    /**
     * Test newProductIds - zwraca ID produktów
     */
    public function testNewProductIdsReturnsProductIds()
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockStmt = $this->createMock(\PDOStatement::class);
        $mockStmt->method('fetchAll')->willReturn([
            ['id' => 10],
            ['id' => 20],
            ['id' => 30],
        ]);
        $mockDb->method('query')->willReturn($mockStmt);

        $repository = new ProductRepository($mockDb);
        $result = $repository->newProductIds(3);

        $this->assertIsArray($result);
        $this->assertEquals([10, 20, 30], $result);
    }

    /**
     * Test newProductIds - pusta lista
     */
    public function testNewProductIdsReturnsEmptyWhenNoProducts()
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockStmt = $this->createMock(\PDOStatement::class);
        $mockStmt->method('fetchAll')->willReturn([]);
        $mockDb->method('query')->willReturn($mockStmt);

        $repository = new ProductRepository($mockDb);

        $this->assertEquals([], $repository->newProductIds(10));
    }

    /**
     * Test promotedProductIdsCached - zwraca promowane produkty
     */
    public function testPromotedProductIdsCachedReturnsIds()
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockStmt = $this->createMock(\PDOStatement::class);
        $mockStmt->method('fetchAll')->willReturn([
            ['id' => 5],
            ['id' => 8],
        ]);
        $mockDb->method('query')->willReturn($mockStmt);

        $repository = new ProductRepository($mockDb);
        $result = $repository->promotedProductIdsCached(2);

        $this->assertIsArray($result);
        $this->assertEquals([5, 8], $result);
    }

    /**
     * Test promotedProductIdsCached - pusta lista
     */
    public function testPromotedProductIdsCachedReturnsEmptyWhenNone()
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockStmt = $this->createMock(\PDOStatement::class);
        $mockStmt->method('fetchAll')->willReturn([]);
        $mockDb->method('query')->willReturn($mockStmt);

        $repository = new ProductRepository($mockDb);

        $this->assertEquals([], $repository->promotedProductIdsCached(6));
    }

    // --- findVariantsForApi ---

    public function testFindVariantsForApiReturnsVariants(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockStmt = new class {
            public function fetchAll($mode = null): array
            {
                return [
                    [
                        'id' => '101',
                        'permutation_hash' => '5-12|7-18',
                        'sku' => 'SKU-M-RED',
                        'ean' => null,
                        'price_brutto' => '109.99',
                        'price_brutto_promo' => null,
                        'price_netto' => '89.42',
                        'price_netto_promo' => null,
                        'quantity' => '5',
                        'stock_0_buy' => '0',
                        'weight' => null,
                        'status' => '1',
                    ],
                ];
            }
        };

        $mockDb->method('query')->willReturn($mockStmt);
        $mockDb->method('select')->willReturnCallback(function ($table, $columns, $where) {
            if ($table === 'pp_shop_products_attributes') {
                return [
                    ['product_id' => '101', 'attribute_id' => '5', 'value_id' => '12'],
                    ['product_id' => '101', 'attribute_id' => '7', 'value_id' => '18'],
                ];
            }
            if ($table === 'pp_shop_attributes_langs') {
                return [
                    ['attribute_id' => '5', 'lang_id' => 'pl', 'name' => 'Rozmiar'],
                    ['attribute_id' => '7', 'lang_id' => 'pl', 'name' => 'Kolor'],
                ];
            }
            if ($table === 'pp_shop_attributes_values_langs') {
                return [
                    ['value_id' => '12', 'lang_id' => 'pl', 'name' => 'M'],
                    ['value_id' => '18', 'lang_id' => 'pl', 'name' => 'Czerwony'],
                ];
            }
            return [];
        });
        $mockDb->method('get')->willReturn(0);

        $repository = new ProductRepository($mockDb);
        $result = $repository->findVariantsForApi(1);

        $this->assertCount(1, $result);
        $this->assertSame(101, $result[0]['id']);
        $this->assertSame('5-12|7-18', $result[0]['permutation_hash']);
        $this->assertSame('SKU-M-RED', $result[0]['sku']);
        $this->assertSame(109.99, $result[0]['price_brutto']);
        $this->assertSame(5, $result[0]['quantity']);
        $this->assertCount(2, $result[0]['attributes']);
    }

    public function testFindVariantsForApiReturnsEmptyWhenNoVariants(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockStmt = new class {
            public function fetchAll($mode = null): array
            {
                return [];
            }
        };

        $mockDb->method('query')->willReturn($mockStmt);

        $repository = new ProductRepository($mockDb);
        $result = $repository->findVariantsForApi(1);

        $this->assertSame([], $result);
    }

    // --- findVariantForApi ---

    public function testFindVariantForApiReturnsVariant(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->method('get')->willReturnCallback(function ($table, $columns, $where) {
            if ($table === 'pp_shop_products') {
                return [
                    'id' => '101',
                    'parent_id' => '1',
                    'permutation_hash' => '5-12',
                    'sku' => 'SKU-M',
                    'ean' => null,
                    'price_brutto' => '99.99',
                    'price_brutto_promo' => null,
                    'price_netto' => '81.29',
                    'price_netto_promo' => null,
                    'quantity' => '10',
                    'stock_0_buy' => '0',
                    'weight' => null,
                    'status' => '1',
                ];
            }
            return null;
        });
        $mockDb->method('select')->willReturnCallback(function ($table) {
            if ($table === 'pp_shop_products_attributes') {
                return [['attribute_id' => '5', 'value_id' => '12']];
            }
            if ($table === 'pp_shop_attributes_langs') {
                return [['attribute_id' => '5', 'lang_id' => 'pl', 'name' => 'Rozmiar']];
            }
            if ($table === 'pp_shop_attributes_values_langs') {
                return [['value_id' => '12', 'lang_id' => 'pl', 'name' => 'M']];
            }
            return [];
        });

        $repository = new ProductRepository($mockDb);
        $result = $repository->findVariantForApi(101);

        $this->assertNotNull($result);
        $this->assertSame(101, $result['id']);
        $this->assertSame(1, $result['parent_id']);
        $this->assertSame('5-12', $result['permutation_hash']);
        $this->assertSame(99.99, $result['price_brutto']);
        $this->assertCount(1, $result['attributes']);
    }

    public function testFindVariantForApiReturnsNullForNonVariant(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->method('get')->willReturn([
            'id' => '1',
            'parent_id' => null,
            'permutation_hash' => '',
        ]);

        $repository = new ProductRepository($mockDb);
        $result = $repository->findVariantForApi(1);

        $this->assertNull($result);
    }

    public function testFindVariantForApiReturnsNullForNonexistent(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->method('get')->willReturn(null);

        $repository = new ProductRepository($mockDb);
        $result = $repository->findVariantForApi(999);

        $this->assertNull($result);
    }

    // --- createVariantForApi ---

    public function testCreateVariantForApiSuccess(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $callCount = 0;
        $mockDb->method('get')->willReturnCallback(function ($table, $columns, $where) use (&$callCount) {
            $callCount++;
            if ($callCount === 1) {
                // Parent exists
                return ['id' => '1', 'archive' => '0', 'parent_id' => null, 'vat' => '23'];
            }
            return null;
        });

        $mockDb->method('count')->willReturn(0);
        $mockDb->method('insert')->willReturn(true);
        $mockDb->method('id')->willReturn(101);

        $repository = new ProductRepository($mockDb);
        $result = $repository->createVariantForApi(1, [
            'attributes' => [5 => 12, 7 => 18],
            'sku' => 'SKU-M-RED',
            'price_brutto' => 109.99,
            'quantity' => 5,
        ]);

        $this->assertNotNull($result);
        $this->assertSame(101, $result['id']);
        $this->assertSame('5-12|7-18', $result['permutation_hash']);
    }

    public function testCreateVariantForApiReturnsNullForArchivedParent(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->method('get')->willReturn([
            'id' => '1', 'archive' => '1', 'parent_id' => null, 'vat' => '23',
        ]);

        $repository = new ProductRepository($mockDb);
        $result = $repository->createVariantForApi(1, [
            'attributes' => [5 => 12],
        ]);

        $this->assertNull($result);
    }

    public function testCreateVariantForApiReturnsNullWhenParentIsVariant(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->method('get')->willReturn([
            'id' => '101', 'archive' => '0', 'parent_id' => '1', 'vat' => '23',
        ]);

        $repository = new ProductRepository($mockDb);
        $result = $repository->createVariantForApi(101, [
            'attributes' => [5 => 12],
        ]);

        $this->assertNull($result);
    }

    public function testCreateVariantForApiReturnsNullForEmptyAttributes(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->method('get')->willReturn([
            'id' => '1', 'archive' => '0', 'parent_id' => null, 'vat' => '23',
        ]);

        $repository = new ProductRepository($mockDb);
        $result = $repository->createVariantForApi(1, ['attributes' => []]);

        $this->assertNull($result);
    }

    public function testCreateVariantForApiReturnsNullForDuplicateHash(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->method('get')->willReturn([
            'id' => '1', 'archive' => '0', 'parent_id' => null, 'vat' => '23',
        ]);
        $mockDb->method('count')->willReturn(1);

        $repository = new ProductRepository($mockDb);
        $result = $repository->createVariantForApi(1, [
            'attributes' => [5 => 12],
        ]);

        $this->assertNull($result);
    }

    // --- updateVariantForApi ---

    public function testUpdateVariantForApiSuccess(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->method('get')->willReturn([
            'id' => '101', 'parent_id' => '1',
        ]);

        $mockDb->expects($this->once())->method('update');

        $repository = new ProductRepository($mockDb);
        $result = $repository->updateVariantForApi(101, [
            'sku' => 'NEW-SKU',
            'price_brutto' => 119.99,
        ]);

        $this->assertTrue($result);
    }

    public function testUpdateVariantForApiReturnsFalseForNonVariant(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->method('get')->willReturn([
            'id' => '1', 'parent_id' => null,
        ]);

        $repository = new ProductRepository($mockDb);
        $result = $repository->updateVariantForApi(1, ['sku' => 'NEW']);

        $this->assertFalse($result);
    }

    public function testUpdateVariantForApiReturnsFalseForNonexistent(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->method('get')->willReturn(null);

        $repository = new ProductRepository($mockDb);
        $result = $repository->updateVariantForApi(999, ['sku' => 'NEW']);

        $this->assertFalse($result);
    }

    public function testUpdateVariantForApiFiltersUnallowedFields(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->method('get')->willReturn([
            'id' => '101', 'parent_id' => '1',
        ]);

        $mockDb->expects($this->once())
            ->method('update')
            ->with(
                'pp_shop_products',
                $this->callback(function ($data) {
                    return isset($data['sku'])
                        && !isset($data['parent_id'])
                        && !isset($data['permutation_hash']);
                }),
                $this->anything()
            );

        $repository = new ProductRepository($mockDb);
        $repository->updateVariantForApi(101, [
            'sku' => 'NEW',
            'parent_id' => 999,
            'permutation_hash' => 'hacked',
        ]);
    }

    public function testUpdateVariantForApiCastsTypes(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->method('get')->willReturn([
            'id' => '101', 'parent_id' => '1',
        ]);

        $mockDb->expects($this->once())
            ->method('update')
            ->with(
                'pp_shop_products',
                $this->callback(function ($data) {
                    return $data['sku'] === '123'
                        && $data['price_brutto'] === 99.99
                        && $data['quantity'] === 5
                        && $data['weight'] === null
                        && $data['status'] === 1;
                }),
                $this->anything()
            );

        $repository = new ProductRepository($mockDb);
        $repository->updateVariantForApi(101, [
            'sku' => 123,
            'price_brutto' => '99.99',
            'quantity' => '5',
            'weight' => '',
            'status' => '1',
        ]);
    }

    // --- deleteVariantForApi ---

    public function testDeleteVariantForApiSuccess(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->method('get')->willReturn([
            'id' => '101', 'parent_id' => '1',
        ]);

        $deleteCalls = [];
        $mockDb->expects($this->exactly(3))
            ->method('delete')
            ->willReturnCallback(function ($table, $where) use (&$deleteCalls) {
                $deleteCalls[] = ['table' => $table, 'where' => $where];
                return true;
            });

        $repository = new ProductRepository($mockDb);
        $result = $repository->deleteVariantForApi(101);

        $this->assertTrue($result);
        $this->assertSame('pp_shop_products_langs', $deleteCalls[0]['table']);
        $this->assertSame(['product_id' => 101], $deleteCalls[0]['where']);
        $this->assertSame('pp_shop_products_attributes', $deleteCalls[1]['table']);
        $this->assertSame('pp_shop_products', $deleteCalls[2]['table']);
    }

    public function testDeleteVariantForApiReturnsFalseForNonVariant(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->method('get')->willReturn([
            'id' => '1', 'parent_id' => null,
        ]);

        $repository = new ProductRepository($mockDb);
        $result = $repository->deleteVariantForApi(1);

        $this->assertFalse($result);
    }

    public function testDeleteVariantForApiReturnsFalseForNonexistent(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->method('get')->willReturn(null);

        $repository = new ProductRepository($mockDb);
        $result = $repository->deleteVariantForApi(999);

        $this->assertFalse($result);
    }

    public function testSaveCustomFieldsDeletesAllWhenEmpty(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->expects($this->once())
            ->method('delete')
            ->with(
                $this->equalTo('pp_shop_products_custom_fields'),
                $this->equalTo(['id_product' => 55])
            );

        $mockDb->expects($this->never())->method('insert');
        $mockDb->expects($this->never())->method('update');

        $repository = new ProductRepository($mockDb);

        $method = new \ReflectionMethod(ProductRepository::class, 'saveCustomFields');
        $method->setAccessible(true);
        $method->invoke($repository, 55, [], [], []);
    }

    public function testCustomLabelNamesUsesDbSettingsWithFallback(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->expects($this->once())
            ->method('select')
            ->with(
                'pp_settings',
                ['param', 'value'],
                $this->callback(function ($where) {
                    return isset($where['param']) && is_array($where['param']) && count($where['param']) === 10;
                })
            )
            ->willReturn([
                ['param' => 'custom_label_0_name', 'value' => 'Sezon'],
                ['param' => 'google_custom_label_2_name', 'value' => 'Kampania'],
                ['param' => 'custom_label_4_name', 'value' => ''],
            ]);

        $repository = new ProductRepository($mockDb);
        $names = $repository->customLabelNames();

        $this->assertSame('Sezon', $names['custom_label_0']);
        $this->assertSame('Custom label 1', $names['custom_label_1']);
        $this->assertSame('Kampania', $names['custom_label_2']);
        $this->assertSame('Custom label 3', $names['custom_label_3']);
        $this->assertSame('Custom label 4', $names['custom_label_4']);
    }

    public function testCustomLabelSuggestionsReturnsEmptyForInvalidLabelType(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->never())->method('query');

        $repository = new ProductRepository($mockDb);
        $result = $repository->customLabelSuggestions('abc', 'custom_label_10');

        $this->assertSame([], $result);
    }

    public function testSaveCustomLabelReturnsFalseForInvalidLabelType(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->never())->method('update');

        $repository = new ProductRepository($mockDb);
        $result = $repository->saveCustomLabel(1, 'abc', 'custom_label_10');

        $this->assertFalse($result);
    }
}
