<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_pipeline\Kernel;

use Drupal\Tests\flowdrop\Kernel\WorkflowExecutionTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\flowdrop_job\FlowDropJobInterface;
use Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface;

/**
 * Pins the pipeline and job entity access matrices.
 *
 * Pipelines and their jobs carry execution data — inputs, outputs, error
 * messages — so their own/any permission splits are the read boundary
 * between users' runs. Fixtures come from a real launch so ownership is
 * stamped exactly the way production stamps it.
 *
 * Also pins permission independence across the two entity types: holding
 * pipeline permissions must never grant access to job entities directly
 * (the job API and lists check job permissions, not pipeline ones).
 *
 * @group flowdrop_pipeline
 */
class PipelineJobAccessControlTest extends WorkflowExecutionTestBase {

  use UserCreationTrait;

  /**
   * A pipeline launched by the owning user.
   */
  protected FlowDropPipelineInterface $pipeline;

  /**
   * A job from that pipeline.
   */
  protected FlowDropJobInterface $job;

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

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

    // Launch as a regular owner so the pipeline and its jobs get real
    // production-path ownership stamps.
    $owner = $this->createUser([]);
    $this->setCurrentUser($owner);
    $workflow = $this->createStateGraphWorkflow('pipeline_access', [
      $this->loggerNode('sink', 'ran'),
    ], []);
    $result = $this->launchAndWait($workflow);
    $pipeline = $this->loadPipeline($result->pipelineId);
    // This test exercises entity-level access (getOwnerId()/access()), which
    // live on the full entity interface rather than the narrow runtime view
    // returned by loadPipeline().
    $this->assertInstanceOf(FlowDropPipelineInterface::class, $pipeline);
    $this->pipeline = $pipeline;

    $jobs = $this->pipeline->getJobs();
    $this->assertNotEmpty($jobs, 'The launch produced at least one job.');
    $this->job = reset($jobs);

    // Sanity: the fixtures are owned by the launching user, not anonymous
    // or admin — otherwise every "own" assertion below is vacuous.
    $this->assertSame((int) $owner->id(), (int) $this->pipeline->getOwnerId());
  }

  /**
   * The own/any split holds for both entity types and every operation.
   */
  public function testOwnAnyMatrix(): void {
    $cases = [
      'flowdrop_pipeline' => $this->pipeline,
      'flowdrop_job' => $this->job,
    ];
    $operations = ['view' => 'view', 'update' => 'edit', 'delete' => 'delete'];

    foreach ($cases as $entityTypeId => $entity) {
      foreach ($operations as $operation => $verb) {
        $ownPermission = sprintf('%s own %s', $verb, $entityTypeId);
        $anyPermission = sprintf('%s any %s', $verb, $entityTypeId);

        // Non-owner with the "own" permission: denied.
        $ownScoped = $this->createUser([$ownPermission]);
        $this->assertFalse(
          $entity->access($operation, $ownScoped),
          sprintf('Non-owner with "%s" may not %s a foreign %s.', $ownPermission, $operation, $entityTypeId),
        );

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

        // The "any" permission grants only its own operation.
        foreach (array_keys($operations) as $other) {
          if ($other !== $operation) {
            $this->assertFalse(
              $entity->access($other, $anyScoped),
              sprintf('"%s" must not grant %s.', $anyPermission, $other),
            );
          }
        }
      }

      // No permissions: every operation denied.
      $nobody = $this->createUser([]);
      foreach (array_keys($operations) as $operation) {
        $this->assertFalse(
          $entity->access($operation, $nobody),
          sprintf('A user without permissions may not %s a %s.', $operation, $entityTypeId),
        );
      }
    }
  }

  /**
   * The owner with "own" permissions can reach their run and its jobs.
   */
  public function testOwnerWithOwnPermissions(): void {
    $owner = $this->createUser([
      'view own flowdrop_pipeline',
      'view own flowdrop_job',
    ]);
    $this->setCurrentUser($owner);
    $workflow = $this->createStateGraphWorkflow('pipeline_owned', [
      $this->loggerNode('sink', 'ran'),
    ], []);
    $result = $this->launchAndWait($workflow);
    $pipeline = $this->loadPipeline($result->pipelineId);
    $this->assertInstanceOf(FlowDropPipelineInterface::class, $pipeline);
    $jobs = $pipeline->getJobs();

    $this->assertTrue(
      $pipeline->access('view', $owner),
      'The owner with "view own" can view their pipeline.',
    );
    $this->assertTrue(
      reset($jobs)->access('view', $owner),
      'The owner with "view own" can view their job.',
    );

    // ...but not someone else's.
    $this->assertFalse($this->pipeline->access('view', $owner));
    $this->assertFalse($this->job->access('view', $owner));
  }

  /**
   * Pipeline permissions never grant job access, and vice versa.
   *
   * Jobs carry the per-node input/output payloads — the most sensitive
   * execution data — so pipeline-level read access must not implicitly
   * unlock them at the entity level.
   */
  public function testPipelineAndJobPermissionsAreIndependent(): void {
    $pipelineViewer = $this->createUser(['view any flowdrop_pipeline']);
    $this->assertTrue($this->pipeline->access('view', $pipelineViewer));
    $this->assertFalse(
      $this->job->access('view', $pipelineViewer),
      'Pipeline view permissions must not grant direct job access.',
    );

    $jobViewer = $this->createUser(['view any flowdrop_job']);
    $this->assertTrue($this->job->access('view', $jobViewer));
    $this->assertFalse(
      $this->pipeline->access('view', $jobViewer),
      'Job view permissions must not grant pipeline access.',
    );
  }

  /**
   * Admin bypasses hold for both entity types.
   */
  public function testAdminBypasses(): void {
    $admin = $this->createUser(['administer flowdrop']);
    foreach (['view', 'update', 'delete'] as $operation) {
      $this->assertTrue($this->pipeline->access($operation, $admin));
      $this->assertTrue($this->job->access($operation, $admin));
    }
  }

}
