<?php

declare(strict_types=1);

namespace Drupal\Tests\acquia_dam\Unit\Client;

use Drupal\acquia_dam\Client\AcquiaDamClient;
use Drupal\Component\Datetime\Time;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Tests\UnitTestCase;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\RequestInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Test the Acquia DAM client connection and assets.
 *
 * @group acquia_dam_unit
 */
final class AcquiaDamClientTest extends UnitTestCase {

  /**
   * Tests getting an asset from the client leverages caching.
   */
  public function testGetAsset(): void {
    $mock_data = file_get_contents(__DIR__ . '/../../../fixtures/a56fb261-8ad5-4e0d-8323-0e8a3659ed39.json');
    self::assertNotFalse($mock_data);
    $backend = $this->createMock(CacheBackendInterface::class);
    $backend->expects($this->exactly(2))
      ->method('get')
      ->with('asset:foo')
      ->willReturn(FALSE, (object) [
        'data' => $mock_data,
      ]);
    $backend->expects($this->once())
      ->method('set')
      ->with('asset:foo', $mock_data, 1649256864, [
        'acquia-dam-asset',
        'acquia-dam-asset:foo',
        'acquia-dam-asset:foo:bar',
      ]);

    $time = new Time(new RequestStack());
    $config = $this->createMock(ImmutableConfig::class);
    $logger = $this->createMock(LoggerInterface::class);
    $client = new AcquiaDamClient(
      $backend,
      $time,
      $config,
      [
        'handler' => new MockHandler([
          new Response(200, [], $mock_data),
        ]),
      ],
      $logger,
      $this->createMock(MessengerInterface::class),
      $this->createMock(DateFormatterInterface::class),
      'laser.widencollective.com',
    );
    $first = $client->getAsset('foo', 'bar');
    self::assertEquals(Json::decode($mock_data), $first);
    $second = $client->getAsset('foo', 'bar');
    self::assertEquals(Json::decode($mock_data), $second);
  }

  /**
   * Builds a test client pre-wired with one or more mocked HTTP responses.
   *
   * @param \GuzzleHttp\Psr7\Response ...$responses
   *   Responses the MockHandler will return in order.
   *
   * @return \Drupal\acquia_dam\Client\AcquiaDamClient
   *   Ready-to-use client instance.
   */
  private function createClient(Response ...$responses): AcquiaDamClient {
    return new AcquiaDamClient(
      $this->createMock(CacheBackendInterface::class),
      new Time(new RequestStack()),
      $this->createMock(ImmutableConfig::class),
      ['handler' => new MockHandler($responses)],
      $this->createMock(LoggerInterface::class),
      $this->createMock(MessengerInterface::class),
      $this->createMock(DateFormatterInterface::class),
      'laser.widencollective.com',
    );
  }

  /**
   * Tests getDisplayKeysV1() normalises a V1 API response correctly.
   */
  public function testGetDisplayKeysV1NormalizesV1Response(): void {
    $fixture = file_get_contents(__DIR__ . '/../../../fixtures/metadata/metadata-fields-v1.json');
    self::assertNotFalse($fixture);

    $client = $this->createClient(new Response(200, [], $fixture));
    $result  = $client->getDisplayKeysV1();

    self::assertIsArray($result);
    self::assertArrayHasKey('fields', $result);

    // Index the result by display_key for reliable assertions.
    $by_key = array_column($result['fields'], NULL, 'display_key');

    // Confirmed discriminators from live V1 fixture:
    self::assertEquals('selection_list',       $by_key['asset_type']['field_type']);
    self::assertEquals('selection_list_multi', $by_key['image_type']['field_type']);
    self::assertEquals('selection_list',       $by_key['year']['field_type']);
    self::assertEquals('selection_list',       $by_key['usage_rights']['field_type']);
    self::assertEquals('text_multi_line',      $by_key['description']['field_type']);
    self::assertEquals('text_multi_line',      $by_key['fileinformation']['field_type']);
    self::assertEquals('text',                 $by_key['keywords']['field_type']);
    self::assertEquals('text',                 $by_key['altText']['field_type']);
    self::assertEquals('text',                 $by_key['widen_original_filepath']['field_type']);

    // Labels are preserved.
    self::assertEquals('Asset Type',       $by_key['asset_type']['display_name']);
    self::assertEquals('Description',      $by_key['description']['display_name']);
    self::assertEquals('File information.', $by_key['fileinformation']['display_name']);
  }

  /**
   * Tests that an unknown discriminator falls back to 'text'.
   */
  public function testGetDisplayKeysV1UnknownDiscriminatorFallsBackToText(): void {
    $v1_body = Json::encode([
      'types' => [[
        'fields' => [
          ['displayKey' => 'mystery_field', 'displayName' => 'Mystery', 'discriminator' => 'UnknownFutureType'],
        ],
      ]],
    ]);

    $client = $this->createClient(new Response(200, [], $v1_body));
    $result  = $client->getDisplayKeysV1();

    self::assertEquals('text', $result['fields'][0]['field_type']);
  }

  /**
   * Tests that an empty 'types' array returns an empty fields list.
   */
  public function testGetDisplayKeysV1EmptyTypesReturnsEmptyFields(): void {
    $client = $this->createClient(new Response(200, [], Json::encode(['types' => []])));
    $result  = $client->getDisplayKeysV1();

    self::assertEquals(['fields' => []], $result);
  }

  /**
   * Creates an AcquiaDamClient with a capture handler for search tests.
   *
   * @param \Psr\Http\Message\RequestInterface|null $captured_request
   *   Set by reference to the request the client sent.
   *
   * @return \Drupal\acquia_dam\Client\AcquiaDamClient
   *   Ready-to-use client instance.
   */
  private function buildSearchClient(?RequestInterface &$captured_request = NULL): AcquiaDamClient {
    $handler = new MockHandler([
      function (RequestInterface $request, array $options) use (&$captured_request) {
        $captured_request = $request;
        return new Response(200, [], Json::encode([
          'items' => [],
          'total_count' => 0,
        ]));
      },
    ]);

    return new AcquiaDamClient(
      $this->createMock(CacheBackendInterface::class),
      new Time(new RequestStack()),
      $this->createMock(ImmutableConfig::class),
      ['handler' => $handler],
      $this->createMock(LoggerInterface::class),
      $this->createMock(MessengerInterface::class),
      $this->createMock(DateFormatterInterface::class),
      'laser.widencollective.com',
    );
  }

  /**
   * Tests that search() with released=TRUE appends the active condition.
   *
   * Captures the HTTP request sent by the client and verifies the query
   * parameter contains the release-date and expiration-date conditions.
   */
  public function testSearchWithReleasedAppendsConditionToQuery(): void {
    $client = $this->buildSearchClient($captured_request);
    $client->search('ft:(image)', '-created_date', 10, 0, ['file_properties'], FALSE, FALSE, TRUE);

    self::assertNotNull($captured_request);
    parse_str($captured_request->getUri()->getQuery(), $params);
    self::assertArrayHasKey('query', $params);
    self::assertStringContainsString('rd:([before', $params['query']);
    self::assertStringContainsString('ed:((isEmpty) OR [after', $params['query']);
  }

  /**
   * Tests that search() with released=FALSE does NOT append the condition.
   *
   * Verifies the query parameter is sent unchanged, confirming the flag
   * default does not mutate the query.
   */
  public function testSearchWithoutReleasedDoesNotAppendCondition(): void {
    $client = $this->buildSearchClient($captured_request);
    $client->search('ft:(image)', '-created_date', 10, 0, ['file_properties']);

    self::assertNotNull($captured_request);
    parse_str($captured_request->getUri()->getQuery(), $params);
    self::assertArrayHasKey('query', $params);
    self::assertSame('ft:(image)', $params['query']);
  }

}
