<?php

declare(strict_types=1);

namespace Drupal\Tests\jsonapi\Functional;

use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\file\Entity\File;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
use Drupal\Tests\image\Kernel\ImageFieldCreationTrait;
use Drupal\Tests\jsonapi\Traits\GetDocumentFromResponseTrait;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException;
use Psr\Http\Message\ResponseInterface;

/**
 * Provides helper methods for the JSON:API module's functional tests.
 *
 * @internal
 */
abstract class JsonApiFunctionalTestBase extends BrowserTestBase {

  use EntityReferenceFieldCreationTrait;
  use GetDocumentFromResponseTrait;
  use ImageFieldCreationTrait;

  const IS_MULTILINGUAL = TRUE;
  const IS_NOT_MULTILINGUAL = FALSE;

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'jsonapi',
    'serialization',
    'node',
    'image',
    'taxonomy',
    'link',
  ];

  /**
   * {@inheritdoc}
   */
  protected $defaultTheme = 'stark';

  /**
   * Test user.
   *
   * @var \Drupal\user\Entity\User
   */
  protected $user;

  /**
   * Test admin user.
   *
   * @var \Drupal\user\Entity\User
   */
  protected $adminUser;

  /**
   * Test user with access to view profiles.
   *
   * @var \Drupal\user\Entity\User
   */
  protected $userCanViewProfiles;

  /**
   * Test nodes.
   *
   * @var \Drupal\node\Entity\Node[]
   */
  protected $nodes = [];

  /**
   * Test taxonomy terms.
   *
   * @var \Drupal\taxonomy\Entity\Term[]
   */
  protected $tags = [];

  /**
   * Test files.
   *
   * @var \Drupal\file\Entity\File[]
   */
  protected $files = [];

  /**
   * The HTTP client.
   *
   * @var \GuzzleHttp\ClientInterface
   */
  protected $httpClient;

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

    // Set up a HTTP client that accepts relative URLs.
    $this->httpClient = $this->container->get('http_client_factory')
      ->fromOptions(['base_uri' => $this->baseUrl]);

    // Create Basic page and Article node types.
    if ($this->profile != 'standard') {
      $this->drupalCreateContentType([
        'type' => 'article',
        'name' => 'Article',
      ]);

      // Setup vocabulary.
      Vocabulary::create([
        'vid' => 'tags',
        'name' => 'Tags',
      ])->save();

      // Add tags and field_image to the article.
      $this->createEntityReferenceField(
        'node',
        'article',
        'field_tags',
        'Tags',
        'taxonomy_term',
        'default',
        [
          'target_bundles' => [
            'tags' => 'tags',
          ],
          'auto_create' => TRUE,
        ],
        FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
      );
      $this->createImageField('field_image', 'node', 'article');
      $this->createImageField('field_no_hero', 'node', 'article');
    }

    FieldStorageConfig::create([
      'field_name' => 'field_link',
      'entity_type' => 'node',
      'type' => 'link',
      'settings' => [],
      'cardinality' => 1,
    ])->save();

    $field_config = FieldConfig::create([
      'field_name' => 'field_link',
      'label' => 'Link',
      'entity_type' => 'node',
      'bundle' => 'article',
      'required' => FALSE,
      'settings' => [],
      'description' => '',
    ]);
    $field_config->save();

    // Field for testing sorting.
    FieldStorageConfig::create([
      'field_name' => 'field_sort1',
      'entity_type' => 'node',
      'type' => 'integer',
    ])->save();
    FieldConfig::create([
      'field_name' => 'field_sort1',
      'entity_type' => 'node',
      'bundle' => 'article',
    ])->save();

    // Another field for testing sorting.
    FieldStorageConfig::create([
      'field_name' => 'field_sort2',
      'entity_type' => 'node',
      'type' => 'integer',
    ])->save();
    FieldConfig::create([
      'field_name' => 'field_sort2',
      'entity_type' => 'node',
      'bundle' => 'article',
    ])->save();

    $this->user = $this->drupalCreateUser([
      'create article content',
      'edit any article content',
      'delete any article content',
    ]);
    $this->adminUser = $this->drupalCreateUser([
      'create article content',
      'edit any article content',
      'delete any article content',
    ],
      'jsonapi_admin_user',
      TRUE,
    );

    // Create a user that can.
    $this->userCanViewProfiles = $this->drupalCreateUser([
      'access user profiles',
    ]);

    $this->grantPermissions(Role::load(RoleInterface::ANONYMOUS_ID), [
      'access user profiles',
      'administer taxonomy',
    ]);

    \Drupal::service('router.builder')->rebuild();
  }

  /**
   * Performs a HTTP request. Wraps the Guzzle HTTP client.
   *
   * Why wrap the Guzzle HTTP client? Because any error response is returned via
   * an exception, which would make the tests unnecessarily complex to read.
   *
   * @param string $method
   *   HTTP method.
   * @param \Drupal\Core\Url $url
   *   URL to request.
   * @param array $request_options
   *   Request options to apply.
   *
   * @return \Psr\Http\Message\ResponseInterface
   *   The request response.
   *
   * @throws \Psr\Http\Client\ClientExceptionInterface
   *
   * @see \GuzzleHttp\ClientInterface::request
   */
  protected function request($method, Url $url, array $request_options): ResponseInterface {
    try {
      $response = $this->httpClient->request($method, $url->toString(), $request_options);
    }
    catch (ClientException $e) {
      $response = $e->getResponse();
    }
    catch (ServerException $e) {
      $response = $e->getResponse();
    }

    return $response;
  }

  /**
   * Creates default content to test the API.
   *
   * @param int $num_articles
   *   Number of articles to create.
   * @param int $num_tags
   *   Number of tags to create.
   * @param bool $article_has_image
   *   Set to TRUE if you want to add an image to the generated articles.
   * @param bool $article_has_link
   *   Set to TRUE if you want to add a link to the generated articles.
   * @param bool $is_multilingual
   *   (optional) Set to TRUE if you want to enable multilingual content.
   * @param bool $referencing_twice
   *   (optional) Set to TRUE if you want articles to reference the same tag
   *   twice.
   */
  protected function createDefaultContent($num_articles, $num_tags, $article_has_image, $article_has_link, $is_multilingual, $referencing_twice = FALSE) {
    $random = $this->getRandomGenerator();
    for ($created_tags = 0; $created_tags < $num_tags; $created_tags++) {
      $term = Term::create([
        'vid' => 'tags',
        'name' => $random->name(),
      ]);

      if ($is_multilingual) {
        $term->addTranslation('ca', ['name' => $term->getName() . ' (ca)']);
      }

      $term->save();
      $this->tags[] = $term;
    }
    for ($created_nodes = 0; $created_nodes < $num_articles; $created_nodes++) {
      $values = [
        'uid' => ['target_id' => $this->user->id()],
        'type' => 'article',
        'promote' => TRUE,
      ];

      if ($referencing_twice) {
        $values['field_tags'] = [
          ['target_id' => 1],
          ['target_id' => 1],
        ];
      }
      else {
        // Get N random tags.
        $selected_tags = mt_rand(1, $num_tags);
        $tags = [];
        while (count($tags) < $selected_tags) {
          $tags[] = mt_rand(1, $num_tags);
          $tags = array_unique($tags);
        }
        $values['field_tags'] = array_map(function ($tag) {
          return ['target_id' => $tag];
        }, $tags);
      }
      if ($article_has_image) {
        $file = File::create([
          'uri' => 'public://' . $random->name() . '.png',
        ]);
        $file->setPermanent();
        $file->save();
        $this->files[] = $file;
        $values['field_image'] = ['target_id' => $file->id(), 'alt' => 'alt text'];
      }
      if ($article_has_link) {
        $values['field_link'] = [
          'title' => $this->getRandomGenerator()->name(),
          'uri' => sprintf(
            '%s://%s.%s',
            'http' . (mt_rand(0, 2) > 1 ? '' : 's'),
            $this->getRandomGenerator()->name(),
            'org'
          ),
        ];
      }

      // Create values for the sort fields, to allow for testing complex
      // sorting:
      // - field_sort1 increments every 5 articles, starting at zero
      // - field_sort2 decreases every article, ending at zero.
      $values['field_sort1'] = ['value' => floor($created_nodes / 5)];
      $values['field_sort2'] = ['value' => $num_articles - $created_nodes];

      $node = $this->createNode($values);

      if ($is_multilingual === static::IS_MULTILINGUAL) {
        $values['title'] = $node->getTitle() . ' (ca)';
        $values['field_image']['alt'] = 'alt text (ca)';
        $node->addTranslation('ca', $values);
      }
      $node->save();

      $this->nodes[] = $node;
    }
    if ($article_has_link) {
      // Make sure that there is at least 1 https link for ::testRead() #19.
      $this->nodes[0]->field_link = [
        'title' => 'Drupal',
        'uri' => 'https://example.com',
      ];
      $this->nodes[0]->save();
    }
  }

}
