<?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\Plugin\FlowDropNodeProcessor\ChoiceNode;
use Drupal\flowdrop_interrupt\Plugin\FlowDropNodeProcessor\FormInputNode;
use Drupal\flowdrop_interrupt\Plugin\FlowDropNodeProcessor\TextInputNode;
use Drupal\flowdrop_orchestration\DTO\ResolvedInterruptInterface;
use Drupal\Tests\UnitTestCase;
use Prophecy\PhpUnit\ProphecyTrait;

/**
 * Tests resume() output shape across the built-in HITL nodes.
 *
 * Verifies the Phase 2 contract for ChoiceNode, TextInputNode, FormInputNode:
 *  - Each opts into the ResumableNodeProcessorInterface contract.
 *  - resume() reads response_data via getResponseData() and produces the
 *    documented output keys.
 *
 * ConfirmationNode has its own test for branch-output coverage.
 *
 * @group flowdrop_interrupt
 */
class HitlNodeResumeContractTest extends UnitTestCase {

  use ProphecyTrait;

  /**
   * All four built-in HITL nodes implement ResumableNodeProcessorInterface.
   */
  public function testAllNodesImplementResumable(): void {
    $this->assertInstanceOf(ResumableNodeProcessorInterface::class, new ChoiceNode([], 'choice_input', []));
    $this->assertInstanceOf(ResumableNodeProcessorInterface::class, new TextInputNode([], 'text_input_interrupt', []));
    $this->assertInstanceOf(ResumableNodeProcessorInterface::class, new FormInputNode([], 'form_input', []));
  }

  /**
   * ChoiceNode single-select: scalar response, count=1.
   */
  public function testChoiceNodeResumeSingleSelection(): void {
    $node = new ChoiceNode([], 'choice_input', []);
    $params = $this->prophesize(ParameterBagInterface::class);
    $params->getBool('multiple', FALSE)->willReturn(FALSE);

    $interrupt = $this->prophesize(ResolvedInterruptInterface::class);
    $interrupt->getResponseData()->willReturn('option_b');
    $interrupt->getResolvedAt()->willReturn(1700000000);
    $interrupt->getResolvedBy()->willReturn(7);
    $interrupt->getInterruptId()->willReturn('uuid-choice');

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

    $this->assertSame('option_b', $output['selected']);
    $this->assertSame(1, $output['selection_count']);
    $this->assertSame(1700000000, $output['response_time']);
    $this->assertSame(7, $output['user_id']);
    $this->assertSame('uuid-choice', $output['interrupt_id']);
  }

  /**
   * ChoiceNode multi-select: array response, count matches.
   */
  public function testChoiceNodeResumeMultipleSelection(): void {
    $node = new ChoiceNode([], 'choice_input', []);
    $params = $this->prophesize(ParameterBagInterface::class);
    $params->getBool('multiple', FALSE)->willReturn(TRUE);

    $interrupt = $this->prophesize(ResolvedInterruptInterface::class);
    $interrupt->getResponseData()->willReturn(['a', 'b', 'c']);
    $interrupt->getResolvedAt()->willReturn(NULL);
    $interrupt->getResolvedBy()->willReturn(NULL);
    $interrupt->getInterruptId()->willReturn('uuid-multi');

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

    $this->assertSame(['a', 'b', 'c'], $output['selected']);
    $this->assertSame(3, $output['selection_count']);
  }

  /**
   * ChoiceNode multi-select with scalar response: promoted to array.
   *
   * Defensive normalization — if upstream resolution wrote a scalar where
   * an array was expected, wrap it so downstream consumers don't trip.
   */
  public function testChoiceNodeResumeMultipleScalarPromotedToArray(): void {
    $node = new ChoiceNode([], 'choice_input', []);
    $params = $this->prophesize(ParameterBagInterface::class);
    $params->getBool('multiple', FALSE)->willReturn(TRUE);

    $interrupt = $this->prophesize(ResolvedInterruptInterface::class);
    $interrupt->getResponseData()->willReturn('only_one');
    $interrupt->getResolvedAt()->willReturn(NULL);
    $interrupt->getResolvedBy()->willReturn(NULL);
    $interrupt->getInterruptId()->willReturn('uuid-promote');

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

    $this->assertSame(['only_one'], $output['selected']);
    $this->assertSame(1, $output['selection_count']);
  }

  /**
   * TextInputNode: string response → text/length outputs.
   */
  public function testTextInputNodeResumeWithStringResponse(): void {
    $node = new TextInputNode([], 'text_input_interrupt', []);
    $params = $this->prophesize(ParameterBagInterface::class);

    $interrupt = $this->prophesize(ResolvedInterruptInterface::class);
    $interrupt->getResponseData()->willReturn('Hello, world');
    $interrupt->getResolvedAt()->willReturn(1700000000);
    $interrupt->getResolvedBy()->willReturn(11);
    $interrupt->getInterruptId()->willReturn('uuid-text');

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

    $this->assertSame('Hello, world', $output['text']);
    $this->assertSame(12, $output['length']);
    $this->assertSame(11, $output['user_id']);
    $this->assertSame('uuid-text', $output['interrupt_id']);
  }

  /**
   * TextInputNode: wrapped response `{value: "..."}` is unwrapped.
   */
  public function testTextInputNodeResumeWithWrappedValue(): void {
    $node = new TextInputNode([], 'text_input_interrupt', []);
    $params = $this->prophesize(ParameterBagInterface::class);

    $interrupt = $this->prophesize(ResolvedInterruptInterface::class);
    $interrupt->getResponseData()->willReturn(['value' => 'wrapped']);
    $interrupt->getResolvedAt()->willReturn(NULL);
    $interrupt->getResolvedBy()->willReturn(NULL);
    $interrupt->getInterruptId()->willReturn('uuid-wrap');

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

    $this->assertSame('wrapped', $output['text']);
  }

  /**
   * FormInputNode: array response passed through as data.
   */
  public function testFormInputNodeResume(): void {
    $node = new FormInputNode([], 'form_input', []);
    $params = $this->prophesize(ParameterBagInterface::class);

    $interrupt = $this->prophesize(ResolvedInterruptInterface::class);
    $interrupt->getResponseData()->willReturn(['name' => 'Alice', 'age' => 30]);
    $interrupt->getResolvedAt()->willReturn(1700000000);
    $interrupt->getResolvedBy()->willReturn(5);
    $interrupt->getInterruptId()->willReturn('uuid-form');

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

    $this->assertSame(['name' => 'Alice', 'age' => 30], $output['data']);
    $this->assertSame(1700000000, $output['response_time']);
    $this->assertSame(5, $output['user_id']);
    $this->assertSame('uuid-form', $output['interrupt_id']);
  }

  /**
   * FormInputNode: non-array response collapses to empty data.
   */
  public function testFormInputNodeResumeWithNonArrayResponse(): void {
    $node = new FormInputNode([], 'form_input', []);
    $params = $this->prophesize(ParameterBagInterface::class);

    $interrupt = $this->prophesize(ResolvedInterruptInterface::class);
    $interrupt->getResponseData()->willReturn('not an array');
    $interrupt->getResolvedAt()->willReturn(NULL);
    $interrupt->getResolvedBy()->willReturn(NULL);
    $interrupt->getInterruptId()->willReturn('uuid-nonform');

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

    $this->assertSame([], $output['data']);
  }

}
