<?php

namespace Drupal\Tests\commerce_product\Kernel\Entity;

use Drupal\Tests\commerce\Kernel\CommerceKernelTestBase;
use Drupal\commerce_price\Price;
use Drupal\commerce_product\Entity\Product;
use Drupal\commerce_product\Entity\ProductAttribute;
use Drupal\commerce_product\Entity\ProductAttributeValue;
use Drupal\commerce_product\Entity\ProductVariation;
use Drupal\user\UserInterface;

/**
 * Tests the Product variation entity.
 *
 * @coversDefaultClass \Drupal\commerce_product\Entity\ProductVariation
 *
 * @group commerce
 */
class ProductVariationTest extends CommerceKernelTestBase {

  /**
   * Modules to enable.
   *
   * @var array
   */
  protected static $modules = [
    'path',
    'commerce_product',
    // Needed to confirm that url generation doesn't cause a crash when
    // deleting a product variation without a referenced product.
    'menu_link_content',
  ];

  /**
   * A sample user.
   *
   * @var \Drupal\user\UserInterface
   */
  protected $user;

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();

    $this->installEntitySchema('commerce_product_variation');
    $this->installEntitySchema('commerce_product');
    $this->installEntitySchema('commerce_product_attribute_value');
    $this->installConfig(['commerce_product']);

    $user = $this->createUser();
    $this->user = $this->reloadEntity($user);
  }

  /**
   * @covers ::getOrderItemTypeId
   * @covers ::getOrderItemTitle
   * @covers ::getProduct
   * @covers ::getProductId
   * @covers ::getSku
   * @covers ::setSku
   * @covers ::getTitle
   * @covers ::setTitle
   * @covers ::getListPrice
   * @covers ::setListPrice
   * @covers ::getPrice
   * @covers ::setPrice
   * @covers ::getCreatedTime
   * @covers ::setCreatedTime
   * @covers ::getOwner
   * @covers ::setOwner
   * @covers ::getOwnerId
   * @covers ::setOwnerId
   * @covers ::getStores
   */
  public function testProductVariation() {
    /** @var \Drupal\commerce_product\Entity\ProductInterface $product */
    $product = Product::create([
      'type' => 'default',
      'title' => 'My Product Title',
    ]);
    $product->save();
    /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $variation */
    $variation = ProductVariation::create([
      'type' => 'default',
      'product_id' => $product->id(),
    ]);
    $variation->save();
    $product = $this->reloadEntity($product);
    $variation = $this->reloadEntity($variation);

    // Confirm that postSave() added the reference on the parent product.
    $this->assertTrue($product->hasVariation($variation));

    $this->assertEquals('default', $variation->getOrderItemTypeId());
    $this->assertEquals('My Product Title', $variation->getOrderItemTitle());

    $this->assertEquals($product, $variation->getProduct());
    $this->assertEquals($product->id(), $variation->getProductId());

    $variation->setSku('1001');
    $this->assertEquals('1001', $variation->getSku());

    $variation->setTitle('My title');
    $this->assertEquals('My title', $variation->getTitle());

    $this->assertNull($variation->getListPrice());

    $list_price = new Price('19.99', 'USD');
    $variation->setListPrice($list_price);
    $this->assertEquals($list_price, $variation->getListPrice());

    $this->assertNull($variation->getPrice());

    $price = new Price('9.99', 'USD');
    $variation->setPrice($price);
    $this->assertEquals($price, $variation->getPrice());

    $variation->setPublished();
    $this->assertEquals(TRUE, $variation->isPublished());

    $variation->setCreatedTime(635879700);
    $this->assertEquals(635879700, $variation->getCreatedTime());

    $variation->setOwner($this->user);
    $this->assertEquals($this->user, $variation->getOwner());
    $this->assertEquals($this->user->id(), $variation->getOwnerId());
    $variation->setOwnerId(0);
    $this->assertInstanceOf(UserInterface::class, $variation->getOwner());
    $this->assertTrue($variation->getOwner()->isAnonymous());
    // Non-existent/deleted user ID.
    $variation->setOwnerId(892);
    $this->assertInstanceOf(UserInterface::class, $variation->getOwner());
    $this->assertTrue($variation->getOwner()->isAnonymous());
    $this->assertEquals(892, $variation->getOwnerId());
    $variation->setOwnerId($this->user->id());
    $this->assertEquals($this->user, $variation->getOwner());
    $this->assertEquals($this->user->id(), $variation->getOwnerId());

    $this->assertEquals($product->getStores(), $variation->getStores());

    // Confirm that deleting the variation deletes the reference.
    $variation->delete();
    $product = $this->reloadEntity($product);
    $this->assertFalse($product->hasVariation($variation));

    // Confirm that the attribute methods return nothing by default.
    $this->assertEmpty($variation->getAttributeFieldNames());
    $this->assertEmpty($variation->getAttributeValueIds());
    $this->assertEmpty($variation->getAttributeValues());

    $this->assertEquals([
      'url.query_args:v',
      'store',
    ], $variation->getCacheContexts());
  }

  /**
   * @covers ::getAttributeFieldNames
   * @covers ::getAttributeValueIds
   * @covers ::getAttributeValueId
   * @covers ::getAttributeValues
   * @covers ::getAttributeValue
   */
  public function testAttributes() {
    $color_attribute = ProductAttribute::create([
      'id' => 'color',
      'label' => 'Color',
    ]);
    $color_attribute->save();

    $size_attribute = ProductAttribute::create([
      'id' => 'size',
      'label' => 'Size',
    ]);
    $size_attribute->save();

    $attribute_field_manager = $this->container->get('commerce_product.attribute_field_manager');
    $attribute_field_manager->createField($color_attribute, 'default');
    $attribute_field_manager->createField($size_attribute, 'default');

    $color_attribute_value = ProductAttributeValue::create([
      'attribute' => 'color',
      'name' => 'Blue',
      'weight' => 0,
    ]);
    $color_attribute_value->save();
    $color_attribute_value = $this->reloadEntity($color_attribute_value);

    $size_attribute_value = ProductAttributeValue::create([
      'attribute' => 'size',
      'name' => 'Medium',
      'weight' => 0,
    ]);
    $size_attribute_value->save();
    $size_attribute_value = $this->reloadEntity($size_attribute_value);

    $product = Product::create([
      'type' => 'default',
      'title' => 'My Product Title',
    ]);
    $product->save();
    $product = $this->reloadEntity($product);

    /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $variation */
    $variation = ProductVariation::create([
      'type' => 'default',
      'product_id' => $product->id(),
      'attribute_color' => $color_attribute_value->id(),
      'attribute_size' => $size_attribute_value->id(),
    ]);
    $variation->save();
    $variation = $this->reloadEntity($variation);

    $this->assertEquals(['attribute_color', 'attribute_size'], $variation->getAttributeFieldNames());
    $this->assertEquals([
      'attribute_color' => $color_attribute_value->id(),
      'attribute_size' => $size_attribute_value->id(),
    ], $variation->getAttributeValueIds());
    $this->assertEquals($color_attribute_value->id(), $variation->getAttributeValueId('attribute_color'));
    $this->assertEquals($size_attribute_value->id(), $variation->getAttributeValueId('attribute_size'));

    $this->assertEquals([
      'attribute_color' => $color_attribute_value,
      'attribute_size' => $size_attribute_value,
    ], $variation->getAttributeValues());
    $this->assertEquals($color_attribute_value, $variation->getAttributeValue('attribute_color'));
    $this->assertEquals($size_attribute_value, $variation->getAttributeValue('attribute_size'));

    // Confirm that empty fields are excluded properly.
    $variation->set('attribute_size', NULL);
    $variation->save();
    $variation = $this->reloadEntity($variation);

    $this->assertEquals([
      'attribute_color' => $color_attribute_value->id(),
    ], $variation->getAttributeValueIds());
    $this->assertNull($variation->getAttributeValueId('attribute_size'));

    $this->assertEquals([
      'attribute_color' => $color_attribute_value,
    ], $variation->getAttributeValues());
    $this->assertNull($variation->getAttributeValue('attribute_size'));

    // Confirm that deleted attribute values are handled properly.
    $variation->set('attribute_size', $size_attribute_value->id());
    $variation->save();
    $variation = $this->reloadEntity($variation);
    $size_attribute_value->delete();

    $this->assertEquals([
      'attribute_color' => $color_attribute_value->id(),
      'attribute_size' => $size_attribute_value->id(),
    ], $variation->getAttributeValueIds());

    $this->assertEquals([
      'attribute_color' => $color_attribute_value,
    ], $variation->getAttributeValues());
    $this->assertNull($variation->getAttributeValue('attribute_size'));
  }

  /**
   * @covers ::toUrl
   */
  public function testDeleteIncomplete() {
    // Confirm that a variation can be deleted even if it has no product.
    $variation = ProductVariation::create([
      'type' => 'default',
      'sku' => strtolower($this->randomMachineName()),
      'title' => $this->randomString(),
    ]);
    $variation->save();
    $variation->delete();
  }

  /**
   * Tests that preSave doesn't throw exception on missing bundle entity.
   *
   * E.g.: during migration 'migrate_lookup' plugin can create stub entity for
   * future reference. This stub entity can have bundle that is the same as
   * entity type ID - 'commerce_product_variation'. In that case, entity should
   * gracefully skip title generation.
   *
   * @see https://www.drupal.org/project/commerce/issues/3342331
   *
   * @covers ::preSave
   */
  public function testPreSaveWithMissingBundleEntity(): void {
    $variation = ProductVariation::create([
      // This is 'missing' bundle which is used as fallback by Drupal core and
      // Migrations.
      'type' => 'commerce_product_variation',
      'sku' => strtolower($this->randomMachineName()),
      'title' => $this->randomString(),
    ])->save();

    self::assertSame($variation, SAVED_NEW);
  }

  /**
   * @covers ::createDuplicate
   */
  public function testDuplicate() {
    $time = time() - 3600;
    /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $variation */
    $variation = ProductVariation::create([
      'type' => 'default',
      'sku' => strtolower($this->randomMachineName()),
      'title' => $this->randomString(),
    ]);
    $variation->setCreatedTime($time);
    $variation->setChangedTime($time);
    $variation->save();
    /** @var \Drupal\commerce_product\Entity\ProductInterface $product */
    $product = Product::create([
      'type' => 'default',
      'title' => 'My Product Title',
      'variations' => [$variation],
    ]);
    $product->save();

    // Create variation duplicate.
    $duplicate_variation = $variation->createDuplicate();
    $duplicate_variation->setSKU(strtolower($this->randomMachineName()));
    $duplicate_variation->save();

    // Confirm that the duplicated variation created with the proper timestamps.
    $this->assertEquals($variation->label(), $duplicate_variation->label());
    $this->assertNotEquals($variation->id(), $duplicate_variation->id());
    $this->assertNotEquals($variation->getSku(), $duplicate_variation->getSku());
    $this->assertNotNull($duplicate_variation->getCreatedTime());
    $this->assertTrue($variation->getCreatedTime() < $duplicate_variation->getCreatedTime());
    $this->assertNotNull($duplicate_variation->getChangedTime());
    $this->assertTrue($variation->getChangedTime() < $duplicate_variation->getChangedTime());
    $product = $this->reloadEntity($product);
    $this->assertTrue($product->hasVariation($duplicate_variation));
  }

}
