<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_stategraph\Unit\Routing;

use Prophecy\Argument;
use Drupal\Tests\UnitTestCase;
use Drupal\flowdrop_orchestration\DTO\Snapshot\IteratorState;
use Drupal\flowdrop_stategraph\Routing\ConditionalEdgeEvaluator;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\LoggerInterface;

/**
 * Tests the ConditionalEdgeEvaluator.
 *
 * @coversDefaultClass \Drupal\flowdrop_stategraph\Routing\ConditionalEdgeEvaluator
 * @group flowdrop_stategraph
 */
class ConditionalEdgeEvaluatorTest extends UnitTestCase {

  use ProphecyTrait;

  /**
   * The evaluator under test.
   *
   * @var \Drupal\flowdrop_stategraph\Routing\ConditionalEdgeEvaluator
   */
  private ConditionalEdgeEvaluator $evaluator;

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();

    // Note: For void methods, just stub without willReturn() or
    // shouldBeCalled().
    $logger = $this->prophesize(LoggerInterface::class);
    $logger->debug(Argument::cetera());
    $logger->warning(Argument::cetera());

    $this->evaluator = new ConditionalEdgeEvaluator($logger->reveal());
  }

  /**
   * Tests equality check (==) evaluates correctly.
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionEquals(): void {
    $state = ["status" => "approved"];

    $this->assertTrue($this->evaluator->evaluateCondition("status == 'approved'", $state));
    $this->assertFalse($this->evaluator->evaluateCondition("status == 'rejected'", $state));
  }

  /**
   * Tests strict equality check (===) evaluates correctly.
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionStrictEquals(): void {
    $state = ["count" => 5];

    $this->assertTrue($this->evaluator->evaluateCondition("count === 5", $state));
    $this->assertFalse($this->evaluator->evaluateCondition("count === '5'", $state));
  }

  /**
   * Tests inequality check (!=) evaluates correctly.
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionNotEquals(): void {
    $state = ["status" => "pending"];

    $this->assertTrue($this->evaluator->evaluateCondition("status != 'approved'", $state));
    $this->assertFalse($this->evaluator->evaluateCondition("status != 'pending'", $state));
  }

  /**
   * Tests strict inequality check (!==) evaluates correctly.
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionStrictNotEquals(): void {
    $state = ["value" => 0];

    $this->assertTrue($this->evaluator->evaluateCondition("value !== false", $state));
    $this->assertFalse($this->evaluator->evaluateCondition("value !== 0", $state));
  }

  /**
   * Tests less than check (<).
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionLessThan(): void {
    $state = ["iterationCount" => 5];

    $this->assertTrue($this->evaluator->evaluateCondition("iterationCount < 10", $state));
    $this->assertFalse($this->evaluator->evaluateCondition("iterationCount < 5", $state));
    $this->assertFalse($this->evaluator->evaluateCondition("iterationCount < 3", $state));
  }

  /**
   * Tests greater than check (>).
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionGreaterThan(): void {
    $state = ["score" => 75];

    $this->assertTrue($this->evaluator->evaluateCondition("score > 50", $state));
    $this->assertFalse($this->evaluator->evaluateCondition("score > 75", $state));
    $this->assertFalse($this->evaluator->evaluateCondition("score > 100", $state));
  }

  /**
   * Tests less than or equal check (<=).
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionLessOrEqual(): void {
    $state = ["count" => 10];

    $this->assertTrue($this->evaluator->evaluateCondition("count <= 10", $state));
    $this->assertTrue($this->evaluator->evaluateCondition("count <= 15", $state));
    $this->assertFalse($this->evaluator->evaluateCondition("count <= 5", $state));
  }

  /**
   * Tests greater than or equal check (>=).
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionGreaterOrEqual(): void {
    $state = ["level" => 5];

    $this->assertTrue($this->evaluator->evaluateCondition("level >= 5", $state));
    $this->assertTrue($this->evaluator->evaluateCondition("level >= 3", $state));
    $this->assertFalse($this->evaluator->evaluateCondition("level >= 10", $state));
  }

  /**
   * Tests simple truthy check.
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionTruthy(): void {
    $state = [
      "hasData" => TRUE,
      "isEmpty" => FALSE,
      "count" => 5,
      "zero" => 0,
      "emptyString" => "",
      "nonEmptyString" => "hello",
    ];

    $this->assertTrue($this->evaluator->evaluateCondition("hasData", $state));
    $this->assertFalse($this->evaluator->evaluateCondition("isEmpty", $state));
    $this->assertTrue($this->evaluator->evaluateCondition("count", $state));
    $this->assertFalse($this->evaluator->evaluateCondition("zero", $state));
    $this->assertFalse($this->evaluator->evaluateCondition("emptyString", $state));
    $this->assertTrue($this->evaluator->evaluateCondition("nonEmptyString", $state));
  }

  /**
   * Tests nested state path (dot notation).
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionNestedPath(): void {
    $state = [
      "data" => [
        "status" => "active",
        "user" => [
          "role" => "admin",
        ],
      ],
    ];

    $this->assertTrue($this->evaluator->evaluateCondition("data.status == 'active'", $state));
    $this->assertTrue($this->evaluator->evaluateCondition("data.user.role == 'admin'", $state));
    $this->assertFalse($this->evaluator->evaluateCondition("data.status == 'inactive'", $state));
  }

  /**
   * Tests state. prefix is optional.
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionStatePrefix(): void {
    $state = ["isComplete" => TRUE];

    // Both with and without "state." prefix should work.
    $this->assertTrue($this->evaluator->evaluateCondition("state.isComplete == true", $state));
    $this->assertTrue($this->evaluator->evaluateCondition("isComplete == true", $state));
  }

  /**
   * Tests comparison with boolean literals.
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionBooleanLiteral(): void {
    $state = ["isComplete" => TRUE, "hasError" => FALSE];

    $this->assertTrue($this->evaluator->evaluateCondition("isComplete == true", $state));
    $this->assertFalse($this->evaluator->evaluateCondition("isComplete == false", $state));
    $this->assertTrue($this->evaluator->evaluateCondition("hasError == false", $state));
    $this->assertFalse($this->evaluator->evaluateCondition("hasError == true", $state));
  }

  /**
   * Tests comparison with numeric values.
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionNumericValue(): void {
    $state = ["count" => 42, "ratio" => 3.14];

    $this->assertTrue($this->evaluator->evaluateCondition("count == 42", $state));
    $this->assertFalse($this->evaluator->evaluateCondition("count == 43", $state));
    $this->assertTrue($this->evaluator->evaluateCondition("ratio == 3.14", $state));
  }

  /**
   * Tests comparison with quoted strings (single and double quotes).
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionQuotedString(): void {
    $state = ["name" => "test"];

    // Double quotes.
    $this->assertTrue($this->evaluator->evaluateCondition('name == "test"', $state));

    // Single quotes.
    $this->assertTrue($this->evaluator->evaluateCondition("name == 'test'", $state));
  }

  /**
   * Tests comparison with null literal.
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionNullLiteral(): void {
    $state = ["error" => NULL, "message" => "hello"];

    $this->assertTrue($this->evaluator->evaluateCondition("error == null", $state));
    $this->assertFalse($this->evaluator->evaluateCondition("message == null", $state));
  }

  /**
   * Tests iterator state checks.
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionIteratorHasMore(): void {
    $iterator = IteratorState::fromItems(["a", "b", "c"]);

    $state = [
      "iterator" => $iterator->toArray(),
    ];

    $this->assertTrue($this->evaluator->evaluateCondition("iterator.hasMore == true", $state));
    $this->assertFalse($this->evaluator->evaluateCondition("iterator.hasMore == false", $state));

    // Test with exhausted iterator.
    $exhausted = $iterator->advance()->advance();
    $state["iterator"] = $exhausted->toArray();

    $this->assertFalse($this->evaluator->evaluateCondition("iterator.hasMore == true", $state));
  }

  /**
   * Tests condition returns false on error.
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionReturnsFalseOnError(): void {
    $state = ["key" => "value"];

    // Missing key should return false, not throw.
    $this->assertFalse($this->evaluator->evaluateCondition("nonexistent.path == 'value'", $state));
  }

  /**
   * Tests condition with whitespace.
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionWithWhitespace(): void {
    $state = ["status" => "active"];

    // Various whitespace patterns.
    $this->assertTrue($this->evaluator->evaluateCondition("  status  ==  'active'  ", $state));
    $this->assertTrue($this->evaluator->evaluateCondition("status=='active'", $state));
  }

  /**
   * Tests comparison between two state paths.
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionTwoStatePaths(): void {
    $state = [
      "currentIndex" => 2,
      "maxIndex" => 5,
      "sameValue" => 10,
      "otherValue" => 10,
    ];

    $this->assertTrue($this->evaluator->evaluateCondition("currentIndex < maxIndex", $state));
    $this->assertTrue($this->evaluator->evaluateCondition("sameValue == otherValue", $state));
  }

  /**
   * Tests empty condition evaluates as falsy.
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionEmpty(): void {
    $state = ["key" => "value"];

    // Empty condition should be falsy.
    $this->assertFalse($this->evaluator->evaluateCondition("", $state));
  }

  /**
   * Tests condition with array index access.
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionArrayIndex(): void {
    $state = [
      "items" => ["first", "second", "third"],
    ];

    // Note: This tests if we can access array with dot notation.
    // If items.0 doesn't work, we'd need different syntax.
    // Based on the implementation, this might need adjustment.
    $this->assertTrue($this->evaluator->evaluateCondition("items", $state));
  }

  /**
   * Tests condition with deeply nested path.
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionDeeplyNested(): void {
    $state = [
      "level1" => [
        "level2" => [
          "level3" => [
            "level4" => "deep_value",
          ],
        ],
      ],
    ];

    $this->assertTrue($this->evaluator->evaluateCondition(
      "level1.level2.level3.level4 == 'deep_value'",
      $state
    ));
  }

  /**
   * Tests condition with special characters in string values.
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionSpecialCharsInString(): void {
    $state = ["path" => "/api/v1/users"];

    $this->assertTrue($this->evaluator->evaluateCondition("path == '/api/v1/users'", $state));
  }

  /**
   * Tests condition type coercion behavior.
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionTypeCoercion(): void {
    $state = [
      "stringNum" => "5",
      "intNum" => 5,
    ];

    // Strict equals should differentiate types.
    $this->assertFalse($this->evaluator->evaluateCondition("stringNum === intNum", $state));

    // Loose equals should match with type coercion ("5" == 5 is true).
    $this->assertTrue($this->evaluator->evaluateCondition("stringNum == intNum", $state));
  }

  /**
   * Tests negative number comparison.
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionNegativeNumber(): void {
    $state = ["temperature" => -5];

    // Note: Negative numbers might need special handling.
    $this->assertTrue($this->evaluator->evaluateCondition("temperature < 0", $state));
  }

  /**
   * Tests condition with float comparison.
   *
   * @covers ::evaluateCondition
   */
  public function testEvaluateConditionFloatComparison(): void {
    $state = ["price" => 19.99];

    $this->assertTrue($this->evaluator->evaluateCondition("price < 20.0", $state));
    $this->assertTrue($this->evaluator->evaluateCondition("price > 19.0", $state));
  }

}
