<?php

declare(strict_types=1);

namespace Drupal\Tests\acquia_dam\Kernel;

use Drupal\acquia_dam\Client\AcquiaDamClient;
use Drupal\acquia_dam\Exception\DamClientException;

/**
 * Kernel tests for AcquiaDamClient scroll search methods.
 *
 * Exercises scrollSearch() and searchWithScroll() through the mock HTTP
 * middleware, using real fixture files:
 *   - search/scroll/search.json          → page 1 (5 items, scroll_id set)
 *   - search/scroll/search_next_page.json → page 2 (2 items, scroll_id set)
 *   - search/scroll/search_complete.json  → terminator (items: [], ends loop)
 *
 * State keys used by MockedResponseMiddleware:
 *   acquia_dam_test.scroll_search_mode        'success' | 'error' | 'empty'
 *   acquia_dam_test.scroll_continuation_call_count  managed by middleware
 *
 * @coversDefaultClass \Drupal\acquia_dam\Client\AcquiaDamClient
 * @group acquia_dam_kernel
 */
final class ScrollSearchTest extends AcquiaDamKernelTestBase {

  /**
   * Scroll ID returned by the initial search fixture (search.json).
   *
   * Used as the argument when calling searchWithScroll() directly in tests
   * that bypass scrollSearch(). The middleware does not validate the value —
   * routing is purely counter-based — so any consistent non-empty string works.
   */
  private const SCROLL_ID = 'FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFlloMkxfT2tTU0xlQzF2ck5xMUpvTWcAAAAABCVnMhZOTWp5U2NSZ1MydXVkRXVXOW5oN2Fn';

  /**
   * Page 1 asset IDs — from search/scroll/search.json (5 items).
   */
  private const PAGE_1_IDS = [
    'efb03f75-3c42-497b-baa9-5ec79d1f56af',
    '4f656c07-6a08-47b3-9403-16082d2fcda2',
    'abab96ac-c2ed-40b1-aaf7-56a52f898230',
    'eec6d92b-6452-4ab6-894a-b4d0826e65ba',
    'f2a9c03d-3664-477c-8013-e84504ed5adc',
  ];

  /**
   * Page 2 asset IDs — from search/scroll/search_next_page.json (2 items).
   */
  private const PAGE_2_IDS = [
    'fcc13282-d569-44bc-a992-2ff2b405ce59',
    '0048cb0c-d174-4234-b220-8719a6f7f6bf',
  ];

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();
    $this->drupalSetUpCurrentUser();
  }

  /**
   * Activates the given scroll test mode in Drupal state.
   *
   * @param string $mode
   *   One of 'success', 'error', or 'empty'.
   */
  private function setScrollMode(string $mode): void {
    $this->container->get('state')
      ->set('acquia_dam_test.scroll_search_mode', $mode);
  }

  /**
   * Returns the site-level DAM client from the factory.
   *
   * @return \Drupal\acquia_dam\Client\AcquiaDamClient
   *   The client instance.
   */
  private function getClient(): AcquiaDamClient {
    return $this->container->get('acquia_dam.client.factory')->getSiteClient();
  }

  /**
   * Tests that scrollSearch() yields all pages and returns every item.
   *
   * Fixture call sequence:
   *   1. search(..., scroll=true) → search.json         (5 items, scroll_id)
   *   2. searchWithScroll(scroll_id) → search_next_page.json (2 items, scroll_id)
   *   3. searchWithScroll(scroll_id) → search_complete.json  (items: [] → loop breaks)
   *
   * Expected: 2 page yields, 7 total items with the correct UUIDs.
   * Also verifies the raw first-page response carries the expected total_count
   * and a non-empty scroll_id so callers can rely on those fields.
   *
   * @covers ::scrollSearch
   * @covers ::searchWithScroll
   */
  public function testScrollSearchYieldsAllPagesWithCorrectIds(): void {
    $this->setScrollMode('success');

    $client = $this->getClient();
    $pages = [];
    foreach ($client->scrollSearch('cat:({Testing})', '-created_date', 5, ['file_properties']) as $page_items) {
      $pages[] = $page_items;
    }

    // Two pages must have been yielded.
    $this->assertCount(2, $pages, 'scrollSearch() must yield exactly 2 pages.');

    // Page 1 — 5 items.
    $this->assertCount(5, $pages[0], 'Page 1 must contain 5 items.');
    $page1_ids = array_column($pages[0], 'id');
    foreach (self::PAGE_1_IDS as $expected_id) {
      $this->assertContains($expected_id, $page1_ids, "Page 1 must contain asset {$expected_id}.");
    }

    // Page 2 — 2 items.
    $this->assertCount(2, $pages[1], 'Page 2 must contain 2 items.');
    $page2_ids = array_column($pages[1], 'id');
    foreach (self::PAGE_2_IDS as $expected_id) {
      $this->assertContains($expected_id, $page2_ids, "Page 2 must contain asset {$expected_id}.");
    }

    // Grand total across all pages matches fixture total_count.
    $all_items = array_merge(...$pages);
    $this->assertCount(7, $all_items, 'Total items across all pages must be 7.');

    // Each item on every page must carry the minimum fields that callers
    // depend on (id, filename, released_and_not_expired, file_properties).
    foreach ($all_items as $item) {
      $this->assertArrayHasKey('id', $item);
      $this->assertArrayHasKey('filename', $item);
      $this->assertArrayHasKey('released_and_not_expired', $item);
      $this->assertArrayHasKey('file_properties', $item);
    }
  }

  /**
   * Tests that scrollSearch() yields nothing when the first page is empty.
   *
   * Fixture: search_complete.json is returned for the initial search
   * (mode='empty'). scrollSearch() detects empty items and returns immediately
   * without yielding.
   *
   * @covers ::scrollSearch
   */
  public function testScrollSearchEmptyFirstPageYieldsNothing(): void {
    $this->setScrollMode('empty');

    $client = $this->getClient();
    $pages = [];
    foreach ($client->scrollSearch('cat:({Testing})', '-created_date', 5, ['file_properties']) as $page_items) {
      $pages[] = $page_items;
    }

    $this->assertCount(0, $pages, 'scrollSearch() must yield nothing when the first page has no items.');
  }

  /**
   * Tests that scrollSearch() throws DamClientException on an API error body.
   *
   * Fixture call sequence:
   *   1. search(..., scroll=true) → search.json          (5 items, scroll_id)
   *   2. searchWithScroll(scroll_id) → {"error":true,"message":"Scroll ID expired."}
   *
   * scrollSearch() reads the error flag and throws DamClientException before
   * yielding a second page. The exception propagates to the caller.
   *
   * @covers ::scrollSearch
   * @covers ::searchWithScroll
   */
  public function testScrollSearchThrowsOnApiErrorInContinuation(): void {
    $this->setScrollMode('error');

    $this->expectException(DamClientException::class);
    $this->expectExceptionMessage('Scroll ID expired.');

    $client = $this->getClient();
    // iterator_to_array() exhausts the generator; the exception is thrown
    // when the generator requests the second (error) page.
    iterator_to_array(
      $client->scrollSearch('cat:({Testing})', '-created_date', 5, ['file_properties'])
    );
  }

}

