<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_interrupt\Kernel;

use Drupal\flowdrop_interrupt\Entity\FlowDropInterruptInterface;
use Drupal\flowdrop_interrupt\Enum\InterruptStatus;
use Drupal\flowdrop_interrupt\Enum\InterruptType;
use Drupal\flowdrop_pipeline\Constants\PipelineStatus;
use Drupal\Tests\flowdrop\Kernel\WorkflowExecutionTestBase;

/**
 * Pins the RateLimit gate end-to-end: shared bucket, pause, re-arm, pass.
 *
 * Two runs through a limit-1 bucket: the first passes and registers the
 * flood event; the second pauses on a paced timer. When the sweep resumes
 * it, `resume()` re-checks the still-full bucket and re-arms a FRESH
 * timer (new interrupt entity, job re-stamped) — proving early resolution
 * cannot bypass the limit. Once the bucket clears, the next sweep lets
 * the run acquire the slot and complete as throttled.
 *
 * @group flowdrop_interrupt
 */
class RateLimitGateTest extends WorkflowExecutionTestBase {

  /**
   * Full lifecycle: pass → block → re-arm on a full bucket → acquire.
   */
  public function testLimitOneBucketBlocksReArmsAndEventuallyPasses(): void {
    $workflow = $this->createStateGraphWorkflow('rate_limit_gate', [
      $this->node('gate', 'rate_limit', [
        'limit' => 1,
        'window' => 3600,
        'identifier' => 'shared-bucket',
      ], ['name' => 'Rate Limit', 'category' => 'timing']),
      $this->loggerNode('sink', 'passed'),
    ], [
      $this->edge('gate', 'trigger', 'sink', 'trigger'),
    ]);

    // Run 1: empty bucket — straight through, not throttled.
    $first = $this->launchAndWait($workflow);
    $firstPipeline = $this->loadPipeline($first->pipelineId);
    $this->assertSame(PipelineStatus::COMPLETED, $firstPipeline->getStatus(), $this->jobTrail($firstPipeline));
    $this->assertFalse($this->jobOutputForNode($firstPipeline, 'gate')['throttled']);

    // Run 2: bucket full — pauses on a timer paced at window/limit.
    $second = $this->launchAndWait($workflow);
    $secondPipeline = $this->loadPipeline($second->pipelineId);
    $this->assertSame(PipelineStatus::PAUSED, $secondPipeline->getStatus(), $this->jobTrail($secondPipeline));

    $timer = $this->singlePendingTimer();
    $this->assertEqualsWithDelta(time() + 3600, $timer->getScheduledAt(), 5);
    $firstTimerUuid = $timer->uuid();

    // Sweep a due timer while the bucket is STILL full: resume() must
    // re-check and re-arm a fresh timer instead of passing through.
    $timer->setScheduledAt(time() - 10);
    $timer->save();
    $sweeper = $this->container->get('flowdrop_interrupt.scheduled_sweeper');
    $this->assertSame(1, $sweeper->sweep());

    $secondPipeline = $this->loadPipeline($second->pipelineId);
    $this->assertSame(PipelineStatus::PAUSED, $secondPipeline->getStatus(), 'A full bucket must keep the run paused — early resolution cannot bypass the limit.');

    $reArmed = $this->singlePendingTimer();
    $this->assertNotSame($firstTimerUuid, $reArmed->uuid(), 'Re-arming must create a fresh interrupt entity.');

    // Clear the bucket, sweep the re-armed timer: the run acquires the
    // slot and completes as throttled.
    $this->container->get('flood')->clear('flowdrop_interrupt.rate_limit', 'shared-bucket');
    $reArmed->setScheduledAt(time() - 10);
    $reArmed->save();
    $this->assertSame(1, $sweeper->sweep());

    $secondPipeline = $this->loadPipeline($second->pipelineId);
    $this->assertSame(PipelineStatus::COMPLETED, $secondPipeline->getStatus(), $this->jobTrail($secondPipeline));

    $output = $this->jobOutputForNode($secondPipeline, 'gate');
    $this->assertTrue($output['throttled']);
    $this->assertTrue($output['resolved_by_timer']);
    $this->assertSame('completed', $this->jobStatusForNode($secondPipeline, 'sink'));
  }

  /**
   * Loads the single pending Timer interrupt the test expects to exist.
   */
  protected function singlePendingTimer(): FlowDropInterruptInterface {
    $storage = $this->container->get('entity_type.manager')->getStorage('flowdrop_interrupt');
    $ids = $storage->getQuery()
      ->accessCheck(FALSE)
      ->condition('type', InterruptType::Timer->value)
      ->condition('status', InterruptStatus::Pending->value)
      ->execute();
    $storage->resetCache($ids);
    $interrupts = array_values($storage->loadMultiple($ids));
    $this->assertCount(1, $interrupts, 'Exactly one pending Timer interrupt must exist.');
    $timer = $interrupts[0];
    $this->assertInstanceOf(FlowDropInterruptInterface::class, $timer);
    return $timer;
  }

}
