<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_stategraph\Kernel;

use Drupal\Core\Session\AccountInterface;
use Drupal\Tests\flowdrop\Kernel\WorkflowExecutionTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\flowdrop_stategraph\Entity\FlowDropStateCheckpointInterface;

/**
 * Pins checkpoint ownership and the entity access matrix.
 *
 * Checkpoints persist workflow state — channel values, intermediate node
 * outputs — so the own/any split is the read boundary between users'
 * paused runs. The ownership stamp is also what makes the split real:
 * a checkpoint written during a user's run must carry that user as
 * owner, or every "own" permission silently degrades to nothing.
 *
 * @group flowdrop_stategraph
 */
class StateCheckpointAccessControlTest extends WorkflowExecutionTestBase {

  use UserCreationTrait;

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();
    // Burn uid 1: the superuser bypasses all access checks.
    $this->createUser();
  }

  /**
   * Runs an entity-checkpointed HITL workflow to the pause as the user.
   *
   * @return \Drupal\flowdrop_stategraph\Entity\FlowDropStateCheckpointInterface[]
   *   The checkpoints written by the run.
   */
  protected function runToPauseAs(AccountInterface $user, string $workflowId): array {
    $this->setCurrentUser($user);
    $workflow = $this->createStateGraphWorkflow($workflowId, [
      $this->loggerNode('src', 'ready'),
      $this->confirmationNode('confirm', 'Proceed?'),
      $this->loggerNode('body', 'approved'),
    ], [
      $this->edge('src', 'message', 'confirm', 'message'),
      $this->edge('confirm', 'True', 'body', 'trigger'),
    ], [
      'stategraph' => [
        'checkpointer_type' => 'entity',
        'create_checkpoints' => TRUE,
      ],
    ]);
    $this->launchAndWait($workflow);

    // Each test runs one workflow against a fresh database, so every
    // persisted checkpoint belongs to this run.
    $storage = $this->container->get('entity_type.manager')
      ->getStorage('state_checkpoint');
    $storage->resetCache();
    $checkpoints = [];
    foreach ($storage->loadMultiple() as $id => $checkpoint) {
      $this->assertInstanceOf(FlowDropStateCheckpointInterface::class, $checkpoint);
      $checkpoints[$id] = $checkpoint;
    }
    $this->assertNotEmpty($checkpoints, 'The paused run persisted checkpoints.');
    return $checkpoints;
  }

  /**
   * Checkpoints written during a run are owned by the launching user.
   */
  public function testCheckpointsAreOwnedByTheLaunchingUser(): void {
    $owner = $this->createUser([]);
    $checkpoints = $this->runToPauseAs($owner, 'checkpoint_owner');

    foreach ($checkpoints as $checkpoint) {
      $this->assertSame(
        (int) $owner->id(),
        (int) $checkpoint->getOwnerId(),
        'Every checkpoint of the run is owned by the launching user.',
      );
    }
  }

  /**
   * The own/any split holds for view/update/delete, with admin bypasses.
   */
  public function testAccessMatrix(): void {
    $owner = $this->createUser([
      'view own state_checkpoint',
      'edit own state_checkpoint',
      'delete own state_checkpoint',
    ]);
    $checkpoints = $this->runToPauseAs($owner, 'checkpoint_matrix');
    $checkpoint = reset($checkpoints);
    $this->assertInstanceOf(FlowDropStateCheckpointInterface::class, $checkpoint);

    $operations = ['view' => 'view', 'update' => 'edit', 'delete' => 'delete'];

    // The owner with own-permissions may do all three.
    foreach (array_keys($operations) as $operation) {
      $this->assertTrue(
        $checkpoint->access($operation, $owner),
        sprintf('The owner with own-permissions may %s their checkpoint.', $operation),
      );
    }

    foreach ($operations as $operation => $verb) {
      // Non-owner with the "own" permission: denied.
      $ownScoped = $this->createUser([sprintf('%s own state_checkpoint', $verb)]);
      $this->assertFalse(
        $checkpoint->access($operation, $ownScoped),
        sprintf('Non-owner with "%s own" may not %s a foreign checkpoint.', $verb, $operation),
      );

      // Non-owner with the "any" permission: allowed, and only for its
      // own operation.
      $anyScoped = $this->createUser([sprintf('%s any state_checkpoint', $verb)]);
      $this->assertTrue(
        $checkpoint->access($operation, $anyScoped),
        sprintf('"%s any" grants %s on a foreign checkpoint.', $verb, $operation),
      );
      foreach (array_keys($operations) as $other) {
        if ($other !== $operation) {
          $this->assertFalse(
            $checkpoint->access($other, $anyScoped),
            sprintf('"%s any" must not grant %s.', $verb, $other),
          );
        }
      }
    }

    // No permissions: denied across the board.
    $nobody = $this->createUser([]);
    foreach (array_keys($operations) as $operation) {
      $this->assertFalse($checkpoint->access($operation, $nobody));
    }

    // Admin bypasses.
    foreach (['administer flowdrop', 'administer flowdrop_stategraph'] as $permission) {
      $admin = $this->createUser([$permission]);
      foreach (array_keys($operations) as $operation) {
        $this->assertTrue(
          $checkpoint->access($operation, $admin),
          sprintf('"%s" grants %s on any checkpoint.', $permission, $operation),
        );
      }
    }
  }

}
