<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_interrupt\Unit\Plugin\FlowDropNodeProcessor;

use Drupal\flowdrop\DTO\ParameterBagInterface;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\ResumableNodeProcessorInterface;
use Drupal\flowdrop_interrupt\Exception\InterruptRequiredException;
use Drupal\flowdrop_interrupt\Message\TimerInterruptMessage;
use Drupal\flowdrop_interrupt\Plugin\FlowDropNodeProcessor\DelayNode;
use Drupal\flowdrop_orchestration\DTO\ResolvedInterruptInterface;
use Drupal\Tests\UnitTestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
 * Tests the DelayNode process()/resume() machine.
 *
 * @coversDefaultClass \Drupal\flowdrop_interrupt\Plugin\FlowDropNodeProcessor\DelayNode
 * @group flowdrop_interrupt
 */
class DelayNodeTest extends UnitTestCase {

  use ProphecyTrait;

  /**
   * Builds a DelayNode with a stubbed config factory.
   */
  protected function createNode(int $maxDelay = 31536000): DelayNode {
    $container = new ContainerBuilder();
    $container->set('config.factory', $this->getConfigFactoryStub([
      'flowdrop_interrupt.settings' => ['timer_max_delay' => $maxDelay],
    ]));
    return DelayNode::create($container, [], 'delay', []);
  }

  /**
   * Builds a parameter bag prophecy for value/unit.
   */
  protected function params(float $value, string $unit): ParameterBagInterface {
    $params = $this->prophesize(ParameterBagInterface::class);
    $params->getFloat('value', 0.0)->willReturn($value);
    $params->getString('unit', 'seconds')->willReturn($unit);
    return $params->reveal();
  }

  /**
   * The node opts into the resume hook.
   */
  public function testImplementsResumableInterface(): void {
    $this->assertInstanceOf(ResumableNodeProcessorInterface::class, $this->createNode());
  }

  /**
   * A positive delay throws a TimerInterruptMessage with fireAt ≈ now+delay.
   */
  public function testProcessThrowsTimerMessage(): void {
    $node = $this->createNode();

    try {
      $node->process($this->params(5.0, 'minutes'));
      $this->fail('Expected InterruptRequiredException');
    }
    catch (InterruptRequiredException $e) {
      $message = $e->getInterruptMessage();
      $this->assertInstanceOf(TimerInterruptMessage::class, $message);
      $this->assertEqualsWithDelta(time() + 300, $message->fireAt, 2);
      $this->assertSame($message->fireAt, $message->getContext()['scheduled_at']);
    }
  }

  /**
   * A zero delay passes straight through without pausing.
   */
  public function testZeroDelayReturnsImmediately(): void {
    $node = $this->createNode();
    $output = $node->process($this->params(0.0, 'seconds'));

    $this->assertTrue($output['resolved_by_timer']);
    $this->assertNull($output['user_id']);
    $this->assertEqualsWithDelta(time(), $output['resumed_at'], 2);
    $this->assertSame(0, $output['drift_seconds']);
  }

  /**
   * Unit factors: each supported unit maps to the right second count.
   *
   * @dataProvider unitProvider
   */
  public function testUnitFactors(string $unit, int $factor): void {
    $node = $this->createNode();

    try {
      $node->process($this->params(2.0, $unit));
      $this->fail('Expected InterruptRequiredException');
    }
    catch (InterruptRequiredException $e) {
      $message = $e->getInterruptMessage();
      $this->assertInstanceOf(TimerInterruptMessage::class, $message);
      $this->assertEqualsWithDelta(time() + 2 * $factor, $message->fireAt, 2);
    }
  }

  /**
   * Data provider for unit factors.
   */
  public static function unitProvider(): array {
    return [
      'seconds' => ['seconds', 1],
      'minutes' => ['minutes', 60],
      'hours' => ['hours', 3600],
      'days' => ['days', 86400],
    ];
  }

  /**
   * Validation rejects non-numeric, negative, and over-cap values.
   */
  public function testValidateParams(): void {
    $node = $this->createNode(maxDelay: 3600);

    $this->assertFalse($node->validateParams(['value' => 'abc'])->isValid());
    $this->assertFalse($node->validateParams(['value' => -5])->isValid());
    $this->assertFalse($node->validateParams(['value' => 5, 'unit' => 'fortnights'])->isValid());
    // 2 hours > 1 hour cap.
    $this->assertFalse($node->validateParams(['value' => 2, 'unit' => 'hours'])->isValid());
    $this->assertTrue($node->validateParams(['value' => 30, 'unit' => 'minutes'])->isValid());
    $this->assertTrue($node->validateParams(['value' => 0])->isValid());
  }

  /**
   * An unlimited cap (0) accepts any delay length.
   */
  public function testValidateParamsUnlimitedCap(): void {
    $node = $this->createNode(maxDelay: 0);
    $this->assertTrue($node->validateParams(['value' => 9000, 'unit' => 'days'])->isValid());
  }

  /**
   * Resume() on the timer path reports resolved_by_timer and drift.
   */
  public function testResumeTimerPath(): void {
    $node = $this->createNode();
    $params = $this->prophesize(ParameterBagInterface::class);

    $interrupt = $this->prophesize(ResolvedInterruptInterface::class);
    $interrupt->getResponseData()->willReturn([
      'resolved_by_timer' => TRUE,
      'fired_at' => 1700000060,
      'scheduled_at' => 1700000000,
    ]);
    $interrupt->getResolvedAt()->willReturn(1700000060);
    $interrupt->getResolvedBy()->willReturn(NULL);

    $output = $node->resume($interrupt->reveal(), $params->reveal());

    $this->assertTrue($output['resolved_by_timer']);
    $this->assertSame(1700000000, $output['scheduled_at']);
    $this->assertSame(1700000060, $output['resumed_at']);
    $this->assertSame(60, $output['drift_seconds']);
    $this->assertNull($output['user_id']);
  }

  /**
   * Resume() on the human-skip path reports the user and negative drift.
   */
  public function testResumeHumanSkipPath(): void {
    $node = $this->createNode();
    $params = $this->prophesize(ParameterBagInterface::class);

    $interrupt = $this->prophesize(ResolvedInterruptInterface::class);
    $interrupt->getResponseData()->willReturn([
      'resolved_by_timer' => FALSE,
      'skipped' => TRUE,
      'scheduled_at' => 1700000600,
    ]);
    $interrupt->getResolvedAt()->willReturn(1700000000);
    $interrupt->getResolvedBy()->willReturn(42);

    $output = $node->resume($interrupt->reveal(), $params->reveal());

    $this->assertFalse($output['resolved_by_timer']);
    $this->assertSame(42, $output['user_id']);
    $this->assertSame(-600, $output['drift_seconds']);
  }

}
