<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_interrupt\Kernel;

use Drupal\Core\Session\AccountInterface;
use Drupal\Tests\flowdrop\Kernel\WorkflowExecutionTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\flowdrop_interrupt\Entity\FlowDropInterruptInterface;
use Drupal\flowdrop_interrupt\Enum\InterruptStatus;
use Drupal\flowdrop_interrupt\Enum\InterruptType;

/**
 * Pins the interrupt entity access matrix.
 *
 * Interrupts are consent gates: who may see, answer, or dismiss a pending
 * question is the HITL security boundary. This pins the own/any split for
 * every operation the handler knows (view / resolve / cancel), that one
 * operation's permission never grants another, and the admin bypasses.
 *
 * @group flowdrop_interrupt
 */
class InterruptAccessControlTest extends WorkflowExecutionTestBase {

  use UserCreationTrait;

  /**
   * The interrupt owner.
   */
  protected AccountInterface $owner;

  /**
   * An interrupt owned by $owner.
   */
  protected FlowDropInterruptInterface $interrupt;

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

    // Burn uid 1: the superuser bypasses all access checks.
    $this->createUser();

    $this->owner = $this->createUser([]);
    $this->interrupt = $this->createInterruptOwnedBy($this->owner);
  }

  /**
   * Creates a pending confirmation interrupt owned by the given account.
   */
  protected function createInterruptOwnedBy(AccountInterface $owner): FlowDropInterruptInterface {
    $storage = $this->container->get('entity_type.manager')
      ->getStorage('flowdrop_interrupt');
    /** @var \Drupal\flowdrop_interrupt\Entity\FlowDropInterruptInterface $interrupt */
    $interrupt = $storage->create([
      'type' => InterruptType::Confirmation->value,
      'workflow_id' => 'access_fixture',
      'node_id' => 'confirm.1',
      'message' => 'Proceed?',
      'status' => InterruptStatus::Pending->value,
      'uid' => $owner->id(),
    ]);
    $interrupt->save();
    return $interrupt;
  }

  /**
   * The own/any split holds for every operation.
   *
   * Operations are independent: holding one operation's permission grants
   * nothing else.
   */
  public function testOperationPermissionMatrix(): void {
    $operations = ['view', 'resolve', 'cancel'];

    foreach ($operations as $operation) {
      $ownPermission = sprintf('%s own flowdrop interrupts', $operation);
      $anyPermission = sprintf('%s any flowdrop interrupts', $operation);

      // Owner holding the "own" permission: allowed.
      $owner = $this->createUser([$ownPermission]);
      $owned = $this->createInterruptOwnedBy($owner);
      $this->assertTrue(
        $owned->access($operation, $owner),
        sprintf('Owner with "%s" may %s their interrupt.', $ownPermission, $operation),
      );

      // Non-owner holding only the "own" permission: denied.
      $this->assertFalse(
        $this->interrupt->access($operation, $owner),
        sprintf('Non-owner with "%s" may not %s a foreign interrupt.', $ownPermission, $operation),
      );

      // Non-owner holding the "any" permission: allowed.
      $any = $this->createUser([$anyPermission]);
      $this->assertTrue(
        $this->interrupt->access($operation, $any),
        sprintf('"%s" grants %s on a foreign interrupt.', $anyPermission, $operation),
      );

      // The permission grants ONLY its own operation — resolve-own must
      // not view, view-any must not cancel, and so on.
      foreach ($operations as $other) {
        if ($other === $operation) {
          continue;
        }
        $this->assertFalse(
          $owned->access($other, $owner),
          sprintf('"%s" must not grant %s.', $ownPermission, $other),
        );
        $this->assertFalse(
          $this->interrupt->access($other, $any),
          sprintf('"%s" must not grant %s.', $anyPermission, $other),
        );
      }
    }

    // No permissions at all: every operation denied, own or foreign.
    $nobody = $this->createUser([]);
    $ownedByNobody = $this->createInterruptOwnedBy($nobody);
    foreach ($operations as $operation) {
      $this->assertFalse(
        $ownedByNobody->access($operation, $nobody),
        sprintf('Ownership without permission must not grant %s.', $operation),
      );
      $this->assertFalse(
        $this->interrupt->access($operation, $nobody),
        sprintf('A user without permissions may not %s a foreign interrupt.', $operation),
      );
    }
  }

  /**
   * Admin bypasses: global and entity-type admin allow every operation.
   */
  public function testAdminBypasses(): void {
    foreach (['administer flowdrop', 'administer flowdrop interrupts'] as $permission) {
      $admin = $this->createUser([$permission]);
      foreach (['view', 'resolve', 'cancel'] as $operation) {
        $this->assertTrue(
          $this->interrupt->access($operation, $admin),
          sprintf('"%s" grants %s on any interrupt.', $permission, $operation),
        );
      }
    }
  }

  /**
   * Interrupt creation is reserved to administrators.
   *
   * Interrupts are raised by the runtime on behalf of paused executions;
   * ordinary users must not be able to fabricate pending consent gates.
   */
  public function testCreateAccessIsAdminOnly(): void {
    $accessHandler = $this->container->get('entity_type.manager')
      ->getAccessControlHandler('flowdrop_interrupt');

    $regular = $this->createUser([
      'view own flowdrop interrupts',
      'resolve own flowdrop interrupts',
      'cancel own flowdrop interrupts',
    ]);
    $this->assertFalse(
      $accessHandler->createAccess(NULL, $regular),
      'Own-scoped interrupt permissions must not grant create.',
    );

    $admin = $this->createUser(['administer flowdrop interrupts']);
    $this->assertTrue(
      $accessHandler->createAccess(NULL, $admin),
      'The entity-type admin may create interrupts.',
    );
  }

}
