<?php

namespace Drupal\Tests\social_comment\Kernel\GraphQL;

use Drupal\comment\Entity\Comment;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\social_comment\Plugin\GraphQL\DataProducer\CommentsCreated;
use Drupal\Tests\node\Traits\NodeCreationTrait;
use Drupal\Tests\social_graphql\Kernel\SocialGraphQLTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\user\UserInterface;

/**
 * Tests the comments field on the Query type.
 *
 * @group social_graphql
 * @group social_comment
 */
class QueryCommentsTest extends SocialGraphQLTestBase {

  use CommentTestTrait;
  use NodeCreationTrait;
  use UserCreationTrait;

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    // For the comment functionality.
    'social_comment',
    'comment',
    // For checking access to a comment.
    'entity',
    // For the comment author and viewer.
    'social_user',
    'user',
    // User creation in social_user requires a service in role_delegation.
    "role_delegation",
    // social_comment configures comments for nodes.
    'node',
    // The default comment config contains a body text field.
    'field',
    'text',
    'filter',
    // Required modules.
    'views',
    'views_bulk_operations',
    'group',
    'variationcache',
    'flexible_permissions',
  ];

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

    $this->installEntitySchema('node');
    $this->installEntitySchema('user');
    $this->installEntitySchema('comment');
    $this->installSchema('comment', 'comment_entity_statistics');
    $this->installConfig(['filter', 'comment', 'social_comment']);
  }

  /**
   * Test that platform comments can be fetched using platform pagination.
   */
  public function testSupportsRelayPagination(): void {
    // Act as a user that can create and view published comments and contents.
    $this->setUpCurrentUser([], array_merge([
      'skip comment approval',
      'access comments',
    ], $this->userPermissions()));

    // Create a node to comment on.
    $node = $this->createNode();

    // Create a bunch of test comments for pagination testing.
    $comments = [];
    for ($i = 0; $i < 10; ++$i) {
      $comments[] = $this->createComment($node);
    }

    $comment_uuids = array_map(
      static fn($comment) => $comment->uuid(),
      $comments
    );

    $this->assertEndpointSupportsPagination(
      'comments',
      $comment_uuids
    );
  }

  /**
   * Test that the comments endpoint respects the access comments permission.
   */
  public function testUserRequiresAccessCommentsPermission() {
    // Create a published comment on a node.
    $node = $this->createNode();
    $this->setUpCurrentUser([], array_merge([
      'skip comment approval',
      'access comments',
    ], $this->userPermissions()));
    $this->createComment($node);

    // Create a user that is not allowed to view comments.
    $this->setUpCurrentUser([], $this->userPermissions());

    $this->assertResults('
        query {
          comments(first: 1) {
            nodes {
              id
            }
          }
        }
      ',
      [],
      [
        'comments' => [
          'nodes' => [],
        ],
      ],
      $this->defaultCacheMetaData()
        ->setCacheMaxAge(0)
        ->addCacheContexts(['languages:language_interface'])
    );
  }

  /**
   * Test that a user can only see comments they're allowed to see in the list.
   *
   * - Any published comment
   * - Their own unpublished comment.
   */
  public function testUserCanViewOnlyOwnOrOtherPublishedComments() {
    $node = $this->createNode();
    // A user to create some other comments with.
    $this->setUpCurrentUser([], array_merge(['access comments'], $this->userPermissions()));
    // Unpublished comment.
    $this->createComment($node);
    // Published comment.
    $published_comment = $this->createComment($node, NULL, ['status' => 1]);

    // Create another user that can view published comments.
    // Users in Open Social can't view their own unpublished comments as they
    // may be unpublished as moderation action (and LU in Open Social have the
    // `bypass moderation` permission).
    $this->setUpCurrentUser([], array_merge(['access comments'], $this->userPermissions()));
    $this->createComment($node);

    $this->assertResults('
        query {
          comments(last: 3) {
            pageInfo {
              hasNextPage
              hasPreviousPage
            }
            nodes {
              id
            }
          }
        }
      ',
      [],
      [
        'comments' => [
          'pageInfo' => [
            'hasNextPage' => FALSE,
            'hasPreviousPage' => FALSE,
          ],
          'nodes' => [
            ['id' => $published_comment->uuid()],
          ],
        ],
      ],
      $this->defaultCacheMetaData()
        ->setCacheMaxAge(0)
        ->addCacheableDependency($published_comment)
        ->addCacheContexts(['languages:language_interface'])
    );
  }

  /**
   * Create the comment entity.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity the comment is made on.
   * @param \Drupal\Core\Session\AccountInterface|null $user
   *   An optional user to create the comment as.
   * @param mixed[] $values
   *   An optional array of values to pass to Comment::create.
   *
   * @return \Drupal\comment\CommentInterface
   *   Created comment entity.
   */
  private function createComment(EntityInterface $entity, ?AccountInterface $user = NULL, array $values = []) {
    if ($user !== NULL) {
      $values += ['uid' => $user->id()];
    }

    /** @var \Drupal\comment\CommentInterface $comment */
    $comment = Comment::create(
      $values +
      [
        'entity_id' => $entity->id(),
        'entity_type' => $entity->getEntityTypeId(),
        'comment_type' => 'comment',
        'field_name' => 'comments',
      ]
    );

    $comment->save();

    return $comment;
  }

  /**
   * Helper method to get cache for commentsCreated tets.
   */
  private function createMetadataForCommentsCreated(UserInterface $user): CacheableMetadata {
    $cache_metadata = $this->defaultCacheMetaData();
    $cache_metadata->setCacheContexts([
      'languages:language_interface',
      'user.permissions',
    ]);
    $cache_metadata->addCacheableDependency($user);

    return $cache_metadata;
  }

  /**
   * Helper method to get query for commentsCreated tests.
   */
  private function getQueryForCommentsCreated(): string {
    return '
      query ($id: ID!) {
        user(id: $id) {
          id
          commentsCreated
        }
      }
    ';
  }

  /**
   * Test that the default value for the commentsCreated count is zero.
   */
  public function testUserCreatedCommentsIsZero(): void {
    $user = $this->setUpCurrentUser([], array_merge(['administer users'], $this->userPermissions()));

    // Set expected array.
    $expected_data = [
      'user' => [
        'id' => $user->uuid(),
        'commentsCreated' => 0,
      ],
    ];

    // Scenario: The default value for the count is zero.
    $this->assertResults(
      $this->getQueryForcommentsCreated(),
      ['id' => $user->uuid()],
      $expected_data,
      $this->createMetadataForcommentsCreated($user)
    );
  }

  /**
   * Test that adding an event will increase the user's statistic count.
   */
  public function testUserCreatedCommentsCount(): void {
    $user = $this->setUpCurrentUser([], array_merge(['administer users'], $this->userPermissions()));
    $node = $this->createNode();

    // Create comment.
    $this->createComment($node, $user);

    // Set expected array.
    $expected_data = [
      'user' => [
        'id' => $user->uuid(),
        'commentsCreated' => 1,
      ],
    ];

    // Scenario: Adding an event will increase the user's statistic count.
    $this->assertResults(
      $this->getQueryForcommentsCreated(),
      ['id' => $user->uuid()],
      $expected_data,
      $this->createMetadataForcommentsCreated($user)
    );

  }

  /**
   * Test that deleting an item is reflected in the number of Comments created.
   */
  public function testUserCreatedCommentsDeleted(): void {
    $user = $this->setUpCurrentUser([], array_merge(['administer users'], $this->userPermissions()));
    $node = $this->createNode();

    // Create comment.
    $comment = $this->createComment($node, $user);

    // Delete comment.
    $comment->delete();

    // Set expected array.
    $expected_data = [
      'user' => [
        'id' => $user->uuid(),
        'commentsCreated' => 0,
      ],
    ];

    // Scenario: Deleting an event is reflected in the number of Comments
    // created by the user.
    $this->assertResults(
      $this->getQueryForcommentsCreated(),
      ['id' => $user->uuid()],
      $expected_data,
      $this->createMetadataForcommentsCreated($user)
    );

  }

  /**
   * Test that the database not called if cache is set .
   */
  public function testUserCreatedCommentsCached(): void {
    $user = $this->setUpCurrentUser([], array_merge(['administer users'], $this->userPermissions()));
    // Set custom cache result.
    $new_result = 35;
    $cid = CommentsCreated::CID_BASE . $user->id();
    \Drupal::service('cache.default')
      ->set($cid, $new_result, Cache::PERMANENT, [$cid]);

    // Update expected counter.
    // Set expected array.
    $expected_data = [
      'user' => [
        'id' => $user->uuid(),
        'commentsCreated' => $new_result,
      ],
    ];

    // Scenario: Requesting the same statistic twice should not trigger
    // multiple database queries, the database not called if cache is set.
    $this->assertResults(
      $this->getQueryForcommentsCreated(),
      ['id' => $user->uuid()],
      $expected_data,
      $this->createMetadataForcommentsCreated($user)
    );

  }

}
