<?php

declare(strict_types=1);

namespace Drupal\Tests\canvas\Kernel\Entity;

use Drupal\canvas\Audit\ComponentAudit;
use Drupal\canvas\Audit\RevisionAuditEnum;
use Drupal\canvas\AutoSave\AutoSaveManager;
use Drupal\canvas\Entity\Page;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\canvas\Entity\Component;
use Drupal\canvas\Entity\JavaScriptComponent;
use Drupal\canvas\Entity\Pattern;
use Drupal\canvas\Plugin\Canvas\ComponentSource\JsComponent;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\user\UserInterface;

/**
 * Tests JavascriptComponent access.
 *
 * @group canvas
 * @covers \Drupal\canvas\Entity\JavaScriptComponent
 * @covers \Drupal\canvas\EntityHandlers\CanvasConfigEntityAccessControlHandler
 */
final class JavascriptComponentAccessTest extends KernelTestBase {

  use UserCreationTrait;

  protected static $modules = [
    'canvas',
    'user',
    'system',
    'datetime',
    'file',
    'image',
    'options',
    'path',
    'path_alias',
    'link',
    'media',
  ];

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();
    $this->installEntitySchema('user');
    $this->installEntitySchema(Page::ENTITY_TYPE_ID);
    $this->installEntitySchema('path_alias');
    $this->installConfig(['system']);
  }

  public function testAccess(): void {
    $js_component_id = 'something_silly';
    $slots = [
      'slot1' => [
        'title' => 'Slot 1',
        'description' => 'Slot 1 innit.',
      ],
    ];
    $js_component = JavaScriptComponent::create([
      'machineName' => $js_component_id,
      'name' => $this->getRandomGenerator()->sentences(5),
      'status' => FALSE,
      'props' => [],
      'required' => [],
      'slots' => $slots,
      'js' => [
        'original' => 'console.log("hey");',
        'compiled' => 'console.log("hey");',
      ],
      'css' => [
        'original' => '.test { display: none; }',
        'compiled' => '.test { display: none; }',
      ],
      'dataDependencies' => [],
    ]);
    self::assertCount(0, $js_component->getTypedData()->validate());
    $js_component->save();
    $code_component_maintainer = $this->createUser([JavaScriptComponent::ADMIN_PERMISSION]);
    \assert($code_component_maintainer instanceof UserInterface);
    self::assertEquals(
      AccessResult::allowed()->addCacheContexts(['user.permissions']),
      $js_component->access('delete', $code_component_maintainer, TRUE),
    );

    // Now enable the component.
    $js_component->enable()->save();
    // And reset the access cache.
    $entity_type_manager = $this->container->get(EntityTypeManagerInterface::class);
    $entity_type_manager->getAccessControlHandler(JavaScriptComponent::ENTITY_TYPE_ID)->resetCache();
    $component_id = JsComponent::componentIdFromJavascriptComponentId($js_component_id);
    $component = Component::load($component_id);
    self::assertNotNull($component);
    self::assertTrue($component->status());
    self::assertContains($js_component->getConfigDependencyName(), $component->getDependencies()['config'] ?? []);
    self::assertCount(0, $js_component->getTypedData()->validate());
    // User should still have access to delete.
    self::assertEquals(
      AccessResult::allowed()->addCacheContexts(['user.permissions']),
      $js_component->access('delete', $code_component_maintainer, TRUE),
    );

    // Ensure >=1 component instance exists in a *content* entity.
    // @see \Drupal\canvas\Audit\ComponentAudit
    $page = Page::create([
      'title' => 'Test page',
      'components' => [
        [
          'uuid' => '2c6e91ae-23ac-433d-9bb8-687144464b34',
          'component_id' => $component_id,
          'inputs' => [],
        ],
      ],
      // Enforce a consistent `data_hash` generated by the auto-save manager.
      // @see \Drupal\canvas\AutoSave\AutoSaveManager::generateHash()
      'uuid' => '81a3d6c4-10e0-422f-ba71-92d76393c008',
      'created' => 0,
      'revision_created' => 0,
    ]);
    self::assertCount(0, $page->validate());
    $page->save();
    // Create some identical revisions.
    for ($i = 0; $i < 2; $i++) {
      $page->setNewRevision(TRUE);
      $page->save();
    }
    // And reset the access cache.
    $entity_type_manager->getAccessControlHandler(JavaScriptComponent::ENTITY_TYPE_ID)->resetCache();
    // User should no longer have access to delete.
    self::assertEquals(
      AccessResult::forbidden('This code component is in use in a default revision and cannot be deleted.')->addCacheContexts(['user.permissions']),
      $js_component->access('delete', $code_component_maintainer, TRUE),
    );

    // Removing all content entity default revision usages restores the ability
    // to delete.
    $audit = $this->container->get(ComponentAudit::class);
    self::assertSame(3, (int) $page->getRevisionId());
    self::assertSame([Page::ENTITY_TYPE_ID => [3 => '1']], $audit->getContentRevisionIdsUsingComponentIds([$component_id], which_revisions: RevisionAuditEnum::Default));
    self::assertSame([Page::ENTITY_TYPE_ID => [3 => '1']], $audit->getContentRevisionIdsUsingComponentIds([$component_id], which_revisions: RevisionAuditEnum::Latest));
    self::assertSame([Page::ENTITY_TYPE_ID => [1 => '1', 2 => '1', 3 => '1']], $audit->getContentRevisionIdsUsingComponentIds([$component_id], which_revisions: RevisionAuditEnum::All));
    $page->setNewRevision(TRUE);
    $page->set('components', [])->save();
    self::assertSame(4, (int) $page->getRevisionId());
    self::assertSame([Page::ENTITY_TYPE_ID => []], $audit->getContentRevisionIdsUsingComponentIds([$component_id], which_revisions: RevisionAuditEnum::Default));
    self::assertSame([Page::ENTITY_TYPE_ID => []], $audit->getContentRevisionIdsUsingComponentIds([$component_id], which_revisions: RevisionAuditEnum::Latest));
    self::assertSame([Page::ENTITY_TYPE_ID => [1 => '1', 2 => '1', 3 => '1']], $audit->getContentRevisionIdsUsingComponentIds([$component_id], which_revisions: RevisionAuditEnum::All));
    // @todo When Canvas adds support for Content Moderation and/or Workspaces in https://www.drupal.org/i/3439664, expand this test coverage to account for a default revision that is NOT the latest.
    // And reset the access cache.
    $entity_type_manager->getAccessControlHandler(JavaScriptComponent::ENTITY_TYPE_ID)->resetCache();
    self::assertEquals(
      AccessResult::allowed()->addCacheContexts(['user.permissions']),
      $js_component->access('delete', $code_component_maintainer, TRUE),
    );

    // Generate an auto-save entry for this content entity.
    $auto_save_manager = $this->container->get(AutoSaveManager::class);
    $page->setComponentTree([
      [
        'uuid' => '2c6e91ae-aaaa-bbbb-9bb8-687144464b34',
        'component_id' => $component_id,
        'inputs' => [],
      ],
    ]);
    $auto_save_manager->saveEntity($page);
    self::assertSame([Page::ENTITY_TYPE_ID => ['auto-save-8f48528246bf1d24' => '1']], $audit->getAutoSavesUsingComponentIds([$component_id]));
    // And reset the access cache.
    $entity_type_manager->getAccessControlHandler(JavaScriptComponent::ENTITY_TYPE_ID)->resetCache();
    self::assertEquals(
      AccessResult::forbidden('This code component is in use in a Canvas auto-save and cannot be deleted.')->addCacheContexts(['user.permissions']),
      $js_component->access('delete', $code_component_maintainer, TRUE),
    );

    // Ensure >=1 component instance exists in a *config* entity.
    // @see \Drupal\Core\Config\Entity\ConfigEntityBase::getDependencies()
    $auto_save_manager->delete($page);
    $pattern = Pattern::create([
      'label' => $this->randomMachineName(),
      'component_tree' => [
        ['uuid' => 'uuid-in-root', 'component_id' => $component_id, 'inputs' => []],
      ],
    ]);
    $pattern->save();
    // And reset the access cache.
    $entity_type_manager->getAccessControlHandler(JavaScriptComponent::ENTITY_TYPE_ID)->resetCache();
    // User should no longer have access to delete.
    self::assertEquals(
      AccessResult::forbidden('There is other configuration depending on this code component.'),
      $js_component->access('delete', $code_component_maintainer, TRUE),
    );

    // If the config entity is updated, deletion is again allowed.
    $pattern->setComponentTree([])->save();
    $entity_type_manager->getAccessControlHandler(JavaScriptComponent::ENTITY_TYPE_ID)->resetCache();
    self::assertEquals(
      AccessResult::allowed()->addCacheContexts(['user.permissions']),
      $js_component->access('delete', $code_component_maintainer, TRUE),
    );

    // If >=1 component instance exists in the auto-save for a config entity,
    // deletion is again forbidden.
    $pattern->setComponentTree([['uuid' => 'uuid-in-root', 'component_id' => $component_id, 'inputs' => []]]);
    $auto_save_manager->saveEntity($pattern);
    $entity_type_manager->getAccessControlHandler(JavaScriptComponent::ENTITY_TYPE_ID)->resetCache();
    self::assertEquals(
      AccessResult::forbidden('This code component is in use in a Canvas auto-save and cannot be deleted.')->addCacheContexts(['user.permissions']),
      $js_component->access('delete', $code_component_maintainer, TRUE),
    );

    // Finally: if another code component imports this code component, deletion
    // should also be forbidden.
    $auto_save_manager->delete($pattern);
    JavaScriptComponent::create([
      'machineName' => 'dependent',
      'dependencies' => [
        'enforced' => [
          'config' => [
            $js_component->getConfigDependencyName(),
          ],
        ],
      ],
    ] + array_diff_key($js_component->toArray(), array_flip(['uuid'])))->save();
    $entity_type_manager->getAccessControlHandler(JavaScriptComponent::ENTITY_TYPE_ID)->resetCache();
    self::assertEquals(
      AccessResult::forbidden('There is other configuration depending on this code component.'),
      $js_component->access('delete', $code_component_maintainer, TRUE),
    );
  }

}
