<?php

namespace Drupal\Tests\acquia_contenthub_publisher\Traits;

use PHPUnit\Framework\Assert;
use PHPUnit\Framework\Constraint\Callback;
use PHPUnit\Framework\Constraint\Constraint;

/**
 * A class returning expected arguments for a sequence of mocked method calls.
 *
 * @internal
 */
class ConsecutiveCallsIterator {

  /**
   * Expected arguments for subsequent mocked method's calls.
   *
   * Each value of the array is an array of arguments for a single method call.
   *
   * @var list<array>
   */
  private array $expectedArguments = [];

  /**
   * Constructor.
   *
   * @param list<array<mixed>> $expectedArgumentsSequence
   *   An array of expected arguments for a stack of calls to the method. Each
   *   array item contains the expected call arguments, either concrete values
   *   or Constraint objects (could be a partial list, in which case the actual
   *   arguments in excess of the expected ones are ignored).
   */
  public function __construct(array $expectedArgumentsSequence) {
    // Find maximum count of parameters in expected sequence.
    $countParameters = 0;
    foreach ($expectedArgumentsSequence as $args) {
      $countParameters = max($countParameters, count($args));
    }

    // Check expected calls arguments count.
    for ($i = 0; $i < count($expectedArgumentsSequence); $i++) {
      $this->expectedArguments[$i] = $expectedArgumentsSequence[$i];
      if (count($this->expectedArguments[$i]) < $countParameters) {
        // Fill missing argument expectations with an 'anything' constraint.
        for ($m = count($this->expectedArguments[$i]); $m < $countParameters; $m++) {
          $this->expectedArguments[$i][] = Assert::anything();
        }
      }
    }
  }

  /**
   * Generator.
   *
   * @return iterable<\PHPUnit\Framework\Constraint\Callback<mixed>>
   *   Each item yields a method's call parameter.
   */
  public function generator(): iterable {
    $expectedArgumentsSequence = $this->expectedArguments;
    $countParameters = count($expectedArgumentsSequence[0] ?? []);
    $callOffset = 0;
    $callbackExecutionsCounter = 0;
    for ($argumentOffset = 0; $argumentOffset < $countParameters; $argumentOffset++) {
      yield new Callback(
        static function (mixed $actual) use ($expectedArgumentsSequence, &$callOffset, &$callbackExecutionsCounter, $argumentOffset, $countParameters): bool {
          $expected = $expectedArgumentsSequence[$callOffset][$argumentOffset] ?? Assert::anything();

          if ($expected instanceof Constraint) {
            Assert::assertThat($actual, $expected, "Failed for call set #{$callOffset}, argument #{$argumentOffset}");
          }
          else {
            Assert::assertEquals($expected, $actual, "Failed for call set #{$callOffset}, argument #{$argumentOffset}");
          }

          $callbackExecutionsCounter++;
          $callOffset = (int) ($callbackExecutionsCounter / $countParameters);

          return TRUE;
        },
      );
    }
  }

}
