<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_runtime\Unit\Service;

use Drupal\flowdrop_runtime\Service\ParameterResolver;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\LoggerInterface;

/**
 * Unit tests for ParameterResolver dynamic port forwarding.
 *
 * Tests that dynamic port values are forwarded through the
 * ParameterResolver to the ParameterBag.
 *
 * @coversDefaultClass \Drupal\flowdrop_runtime\Service\ParameterResolver
 * @group flowdrop_runtime
 */
class ParameterResolverDynamicPortsTest extends TestCase {

  use ProphecyTrait;

  /**
   * The service under test.
   */
  protected ParameterResolver $resolver;

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();
    $logger = $this->prophesize(LoggerInterface::class);
    $this->resolver = new ParameterResolver($logger->reveal());
  }

  /**
   * Tests that dynamic input values are forwarded from runtime inputs.
   *
   * @covers ::resolve
   */
  public function testDynamicInputValuesForwarded(): void {
    // Schema declares dynamicInputs/dynamicOutputs (Option B pattern).
    $parameterSchema = [
      "type" => "object",
      "properties" => [
        "dynamicInputs" => [
          "type" => "array",
          "default" => [],
        ],
        "dynamicOutputs" => [
          "type" => "array",
          "default" => [],
        ],
      ],
    ];

    $entityConfig = [
      "dynamicInputs" => ["configurable" => TRUE],
      "dynamicOutputs" => ["configurable" => TRUE],
    ];

    $workflowValues = [
      "dynamicInputs" => [
        ["name" => "first_name", "label" => "First Name", "dataType" => "string"],
        ["name" => "age", "label" => "Age", "dataType" => "number"],
      ],
      "dynamicOutputs" => [
        ["name" => "greeting", "value" => "Hello {{ first_name }}"],
      ],
    ];

    $runtimeInputs = [
      "first_name" => "Alice",
      "age" => 30,
    ];

    $result = $this->resolver->resolve(
      $parameterSchema,
      $entityConfig,
      $workflowValues,
      $runtimeInputs
    );

    // Dynamic input values should be in the ParameterBag.
    $this->assertEquals("Alice", $result->get("first_name"));
    $this->assertEquals(30, $result->get("age"));

    // Dynamic definitions should be resolved from workflowValues.
    $this->assertIsArray($result->get("dynamicInputs"));
    $this->assertCount(2, $result->get("dynamicInputs"));
    $this->assertIsArray($result->get("dynamicOutputs"));
    $this->assertCount(1, $result->get("dynamicOutputs"));
  }

  /**
   * Tests that static params take precedence over dynamic port names.
   *
   * @covers ::resolve
   */
  public function testStaticParamsPrecedeOverDynamic(): void {
    $parameterSchema = [
      "type" => "object",
      "properties" => [
        "data" => [
          "type" => "string",
          "default" => "static_default",
        ],
        "dynamicInputs" => [
          "type" => "array",
          "default" => [],
        ],
      ],
    ];

    $entityConfig = [
      "data" => ["connectable" => TRUE, "configurable" => FALSE],
      "dynamicInputs" => ["configurable" => TRUE],
    ];

    $workflowValues = [
      "dynamicInputs" => [
        ["name" => "data", "label" => "Data", "dataType" => "string"],
      ],
    ];

    $runtimeInputs = [
      "data" => "from_connection",
    ];

    $result = $this->resolver->resolve(
      $parameterSchema,
      $entityConfig,
      $workflowValues,
      $runtimeInputs
    );

    // Static param "data" should get the value from runtime input
    // via the normal resolution path (connectable=TRUE), NOT from
    // dynamic port forwarding.
    $this->assertEquals("from_connection", $result->get("data"));
  }

  /**
   * Tests that missing runtime values for dynamic inputs are skipped.
   *
   * @covers ::resolve
   */
  public function testMissingDynamicInputSkipped(): void {
    $parameterSchema = [
      "type" => "object",
      "properties" => [
        "dynamicInputs" => [
          "type" => "array",
          "default" => [],
        ],
      ],
    ];

    $entityConfig = [
      "dynamicInputs" => ["configurable" => TRUE],
    ];

    $workflowValues = [
      "dynamicInputs" => [
        ["name" => "connected_port", "label" => "Connected", "dataType" => "string"],
        ["name" => "unconnected_port", "label" => "Unconnected", "dataType" => "string"],
      ],
    ];

    $runtimeInputs = [
      "connected_port" => "hello",
    ];

    $result = $this->resolver->resolve(
      $parameterSchema,
      $entityConfig,
      $workflowValues,
      $runtimeInputs
    );

    $this->assertEquals("hello", $result->get("connected_port"));
    $this->assertNull($result->get("unconnected_port"));
  }

  /**
   * Tests that empty dynamic port arrays are handled safely.
   *
   * @covers ::resolve
   */
  public function testEmptyDynamicPorts(): void {
    $parameterSchema = [
      "type" => "object",
      "properties" => [
        "text" => ["type" => "string", "default" => "hello"],
      ],
    ];

    $entityConfig = [
      "text" => ["configurable" => TRUE],
    ];

    $workflowValues = [
      "text" => "world",
    ];

    $result = $this->resolver->resolve(
      $parameterSchema,
      $entityConfig,
      $workflowValues,
      []
    );

    // Normal resolution should work without dynamic ports.
    $this->assertEquals("world", $result->get("text"));
  }

  /**
   * Tests that dynamic port definitions with empty names are skipped.
   *
   * @covers ::resolve
   */
  public function testEmptyNameSkipped(): void {
    $parameterSchema = [
      "type" => "object",
      "properties" => [
        "dynamicInputs" => [
          "type" => "array",
          "default" => [],
        ],
      ],
    ];

    $entityConfig = [
      "dynamicInputs" => ["configurable" => TRUE],
    ];

    $workflowValues = [
      "dynamicInputs" => [
        ["name" => "", "label" => "Empty Name"],
        ["name" => "valid", "label" => "Valid"],
      ],
    ];

    $runtimeInputs = [
      "" => "should_be_ignored",
      "valid" => "accepted",
    ];

    $result = $this->resolver->resolve(
      $parameterSchema,
      $entityConfig,
      $workflowValues,
      $runtimeInputs
    );

    $this->assertEquals("accepted", $result->get("valid"));
    $this->assertFalse($result->has(""));
  }

  /**
   * Tests that non-array items in dynamicInputs are safely skipped.
   *
   * @covers ::resolve
   */
  public function testMalformedDynamicInputDefsSkipped(): void {
    $parameterSchema = [
      "type" => "object",
      "properties" => [
        "dynamicInputs" => [
          "type" => "array",
          "default" => [],
        ],
      ],
    ];

    $entityConfig = [
      "dynamicInputs" => ["configurable" => TRUE],
    ];

    $workflowValues = [
      "dynamicInputs" => [
        "not_an_array",
        ["name" => "valid", "label" => "Valid"],
      ],
    ];

    $runtimeInputs = [
      "valid" => "value",
    ];

    $result = $this->resolver->resolve(
      $parameterSchema,
      $entityConfig,
      $workflowValues,
      $runtimeInputs
    );

    // Should skip the non-array item and still forward the valid one.
    $this->assertEquals("value", $result->get("valid"));
  }

  /**
   * Tests that dynamic port values arrive via the unified "input" port.
   *
   * When unified input is enabled and the upstream sends a JSON object
   * via the "input" port, keys matching dynamic port names must pass
   * through the mergeUnifiedInput filter — even though they're not
   * in entityConfig (they're instance-specific).
   *
   * @covers ::resolve
   */
  public function testDynamicPortsFromUnifiedInput(): void {
    $parameterSchema = [
      "type" => "object",
      "properties" => [
        "dynamicInputs" => [
          "type" => "array",
          "default" => [],
        ],
      ],
    ];

    $entityConfig = [
      "dynamicInputs" => ["configurable" => TRUE],
    ];

    $workflowValues = [
      "dynamicInputs" => [
        ["name" => "first_name", "label" => "First Name", "dataType" => "string"],
        ["name" => "age", "label" => "Age", "dataType" => "number"],
      ],
    ];

    // Values arrive via the unified "input" port (a single JSON object),
    // NOT as individual port connections.
    $runtimeInputs = [
      "input" => [
        "first_name" => "Alice",
        "age" => 30,
      ],
    ];

    $result = $this->resolver->resolve(
      $parameterSchema,
      $entityConfig,
      $workflowValues,
      $runtimeInputs
    );

    // Dynamic port values from unified input should be forwarded.
    $this->assertEquals("Alice", $result->get("first_name"));
    $this->assertEquals(30, $result->get("age"));
  }

  /**
   * Tests individual port connections override unified input for dynamic ports.
   *
   * @covers ::resolve
   */
  public function testDynamicPortIndividualOverridesUnified(): void {
    $parameterSchema = [
      "type" => "object",
      "properties" => [
        "dynamicInputs" => [
          "type" => "array",
          "default" => [],
        ],
      ],
    ];

    $entityConfig = [
      "dynamicInputs" => ["configurable" => TRUE],
    ];

    $workflowValues = [
      "dynamicInputs" => [
        ["name" => "name", "label" => "Name", "dataType" => "string"],
      ],
    ];

    // Both unified and individual provide "name".
    $runtimeInputs = [
      "input" => [
        "name" => "from_unified",
      ],
      "name" => "from_individual",
    ];

    $result = $this->resolver->resolve(
      $parameterSchema,
      $entityConfig,
      $workflowValues,
      $runtimeInputs
    );

    // Individual port value should take precedence.
    $this->assertEquals("from_individual", $result->get("name"));
  }

}
