<?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 unified input merging.
 *
 * Tests the mergeUnifiedInput functionality for the reserved "input" port.
 *
 * @coversDefaultClass \Drupal\flowdrop_runtime\Service\ParameterResolver
 * @group flowdrop_runtime
 */
class ParameterResolverUnifiedInputTest extends TestCase {

  use ProphecyTrait;

  /**
   * The service under test.
   *
   * @var \Drupal\flowdrop_runtime\Service\ParameterResolver
   */
  protected ParameterResolver $resolver;

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

  /**
   * Tests that unified input is decomposed and merged with individual inputs.
   *
   * @covers ::resolve
   */
  public function testUnifiedInputDecomposition(): void {
    $parameterSchema = [
      "type" => "object",
      "properties" => [
        "text" => ["type" => "string", "default" => ""],
        "count" => ["type" => "integer", "default" => 0],
      ],
    ];

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

    $runtimeInputs = [
      "input" => [
        "text" => "hello from unified",
        "count" => 42,
      ],
    ];

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

    // Values from unified input should be decomposed.
    $this->assertEquals("hello from unified", $result->getString("text"));
    $this->assertEquals(42, $result->getInt("count"));
  }

  /**
   * Tests that individual port values take precedence over unified input.
   *
   * @covers ::resolve
   */
  public function testIndividualPortPrecedence(): void {
    $parameterSchema = [
      "type" => "object",
      "properties" => [
        "text" => ["type" => "string", "default" => ""],
        "count" => ["type" => "integer", "default" => 0],
      ],
    ];

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

    $runtimeInputs = [
      "input" => [
        "text" => "from unified input",
        "count" => 10,
      ],
      // Individual port should override unified input.
      "text" => "from individual port",
    ];

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

    // Individual port value takes precedence.
    $this->assertEquals("from individual port", $result->getString("text"));
    // Unified input value is used for count.
    $this->assertEquals(10, $result->getInt("count"));
  }

  /**
   * Tests that non-connectable parameters are filtered from unified input.
   *
   * @covers ::resolve
   */
  public function testNonConnectableFilteredFromUnifiedInput(): void {
    $parameterSchema = [
      "type" => "object",
      "properties" => [
        "public_param" => ["type" => "string", "default" => "default_public"],
        "hidden_param" => ["type" => "string", "default" => "default_hidden"],
      ],
    ];

    $entityConfig = [
      "public_param" => ["connectable" => TRUE, "configurable" => FALSE],
      // Not connectable - should be filtered.
      "hidden_param" => ["connectable" => FALSE, "configurable" => TRUE],
    ];

    $runtimeInputs = [
      "input" => [
        "public_param" => "injected_public",
        // Should be filtered out because not connectable.
        "hidden_param" => "injected_hidden",
      ],
    ];

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

    // Public param should be injected from unified input.
    $this->assertEquals("injected_public", $result->getString("public_param"));
    // Hidden param should use default (filtered from unified input).
    $this->assertEquals("default_hidden", $result->getString("hidden_param"));
  }

  /**
   * Tests that internal parameters bypass connectable check.
   *
   * @covers ::resolve
   */
  public function testInternalParametersBypassConnectableCheck(): void {
    $parameterSchema = [
      "type" => "object",
      "properties" => [
        "__state__" => ["type" => "object", "default" => []],
        "regular" => ["type" => "string", "default" => "default"],
      ],
    ];

    $entityConfig = [
      // Internal params don't need connectable flag.
      "__state__" => [],
      // Not connectable, should be filtered.
      "regular" => ["connectable" => FALSE],
    ];

    $runtimeInputs = [
      "input" => [
        "__state__" => ["key" => "value"],
        "regular" => "should_be_filtered",
      ],
    ];

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

    // Internal parameter should be accepted (bypasses connectable check).
    $this->assertEquals(["key" => "value"], $result->getArray("__state__"));
    // Regular non-connectable param should use default.
    $this->assertEquals("default", $result->getString("regular"));
  }

  /**
   * Tests behavior when unified input is not an array.
   *
   * @covers ::resolve
   */
  public function testNonArrayUnifiedInputSkipped(): void {
    $parameterSchema = [
      "type" => "object",
      "properties" => [
        "text" => ["type" => "string", "default" => "default_text"],
      ],
    ];

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

    // Unified input is a string instead of array - should be skipped.
    $runtimeInputs = [
      "input" => "not an array",
    ];

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

    // Should use default since unified input is invalid.
    $this->assertEquals("default_text", $result->getString("text"));
  }

  /**
   * Tests behavior when no unified input is provided.
   *
   * @covers ::resolve
   */
  public function testNoUnifiedInputBehavior(): void {
    $parameterSchema = [
      "type" => "object",
      "properties" => [
        "text" => ["type" => "string", "default" => "default_text"],
      ],
    ];

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

    // No unified input, only individual port.
    $runtimeInputs = [
      "text" => "direct value",
    ];

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

    // Should use the direct value.
    $this->assertEquals("direct value", $result->getString("text"));
  }

  /**
   * Tests complete merge scenario with all precedence rules.
   *
   * @covers ::resolve
   */
  public function testCompleteMergeScenario(): void {
    $parameterSchema = [
      "type" => "object",
      "properties" => [
        "from_unified" => ["type" => "string", "default" => ""],
        "from_individual" => ["type" => "string", "default" => ""],
        "from_workflow" => ["type" => "string", "default" => ""],
        "from_default" => ["type" => "string", "default" => "default_value"],
        "hidden_param" => ["type" => "string", "default" => "hidden_default"],
      ],
    ];

    $entityConfig = [
      "from_unified" => ["connectable" => TRUE],
      "from_individual" => ["connectable" => TRUE],
      "from_workflow" => ["connectable" => TRUE, "configurable" => TRUE],
      "from_default" => ["connectable" => TRUE],
      "hidden_param" => ["connectable" => FALSE, "configurable" => TRUE],
    ];

    $workflowValues = [
      "from_workflow" => "workflow_value",
    ];

    $runtimeInputs = [
      "input" => [
        "from_unified" => "unified_value",
        "from_individual" => "will_be_overwritten",
        "hidden_param" => "should_be_filtered",
      ],
      "from_individual" => "individual_value",
    ];

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

    // From unified input (connectable, not overwritten).
    $this->assertEquals("unified_value", $result->getString("from_unified"));
    // Individual port takes precedence over unified.
    $this->assertEquals("individual_value", $result->getString("from_individual"));
    // Runtime input takes precedence over workflow value.
    // (Workflow value is only used if no runtime input exists).
    $this->assertEquals("workflow_value", $result->getString("from_workflow"));
    // Uses schema default.
    $this->assertEquals("default_value", $result->getString("from_default"));
    // Filtered from unified input (not connectable), uses default.
    $this->assertEquals("hidden_default", $result->getString("hidden_param"));
  }

}
