<?php

declare(strict_types=1);

namespace Drupal\Tests\file\Functional;

use Drupal\Core\Entity\EntityInterface;
use Drupal\entity_test\Entity\EntityTestConstraints;
use Drupal\entity_test\EntityTestHelper;
use Drupal\file\Entity\File;
use Drupal\node\Entity\Node;
use Drupal\user\Entity\Role;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;

/**
 * Tests file listing page functionality.
 */
#[Group('file')]
#[RunTestsInSeparateProcesses]
class FileListingTest extends FileFieldTestBase {

  /**
   * {@inheritdoc}
   */
  protected static $modules = ['views', 'file', 'image', 'entity_test'];

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

  /**
   * An authenticated user.
   *
   * @var \Drupal\user\UserInterface
   */
  protected $baseUser;

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

    // This test expects unused managed files to be marked as a temporary file.
    $this->config('file.settings')
      ->set('make_unused_managed_files_temporary', TRUE)
      ->save();

    $this->adminUser = $this->drupalCreateUser([
      'access files overview',
      'bypass node access',
      'delete any file',
    ]);
    $this->baseUser = $this->drupalCreateUser();
    $this->createFileField('file', 'node', 'article', [], ['file_extensions' => 'txt png']);
  }

  /**
   * Calculates total count of usages for a file.
   *
   * @param array $usage
   *   Array of file usage information as returned from file_usage subsystem.
   *
   * @return int
   *   Total usage count.
   */
  protected function sumUsages($usage): int {
    $count = 0;
    foreach ($usage as $module) {
      foreach ($module as $entity_type) {
        foreach ($entity_type as $entity) {
          $count += $entity;
        }
      }
    }

    return $count;
  }

  /**
   * Tests file overview with different user permissions.
   */
  public function testFileListingPages(): void {
    $file_usage = $this->container->get('file.usage');
    // Users without sufficient permissions should not see file listing.
    $this->drupalLogin($this->baseUser);
    $this->drupalGet('admin/content/files');
    $this->assertSession()->statusCodeEquals(403);

    // Log in with user with right permissions and test listing.
    $this->drupalLogin($this->adminUser);

    for ($i = 0; $i < 5; $i++) {
      $nodes[] = $this->drupalCreateNode(['type' => 'article']);
    }

    $this->drupalGet('admin/content/files');
    $this->assertSession()->statusCodeEquals(200);
    $this->assertSession()->pageTextContains('No files available.');
    $this->drupalGet('admin/content/files');
    $this->assertSession()->statusCodeEquals(200);

    // Create a file with no usage.
    $file = $this->createFile();

    $this->drupalGet('admin/content/files/usage/' . $file->id());
    $this->assertSession()->statusCodeEquals(200);
    $this->assertSession()->titleEquals('File usage information for ' . $file->getFilename() . ' | Drupal');

    foreach ($nodes as &$node) {
      $this->drupalGet('node/' . $node->id() . '/edit');
      $file = $this->getTestFile('image');

      $edit = [
        'files[file_0]' => \Drupal::service('file_system')->realpath($file->getFileUri()),
      ];
      $this->submitForm($edit, 'Save');
      $node = Node::load($node->id());
    }

    $this->drupalGet('admin/content/files');

    foreach ($nodes as $node) {
      $file = File::load($node->file->target_id);
      $this->assertSession()->pageTextContains($file->getFilename());
      $this->assertSession()->linkByHrefExists($file->createFileUrl());
      $this->assertSession()->linkByHrefExists('admin/content/files/usage/' . $file->id());
      $this->assertSession()->linkByHrefExists($file->toUrl('delete-form')->toString());
    }
    $this->assertSession()->elementTextNotContains('css', '.views-element-container table', 'Temporary');
    $this->assertSession()->elementTextContains('css', '.views-element-container table', 'Permanent');

    // Use one file two times and check usage information.
    $orphaned_file = $nodes[1]->file->target_id;
    $used_file = $nodes[0]->file->target_id;
    $nodes[1]->file->target_id = $used_file;
    $nodes[1]->save();

    $this->drupalGet('admin/content/files');
    $file = File::load($orphaned_file);
    $usage = $this->sumUsages($file_usage->listUsage($file));
    $this->assertSession()->responseContains('admin/content/files/usage/' . $file->id() . '">' . $usage);

    $file = File::load($used_file);
    $usage = $this->sumUsages($file_usage->listUsage($file));
    $this->assertSession()->responseContains('admin/content/files/usage/' . $file->id() . '">' . $usage);

    $this->assertSession()->elementsCount('xpath', "//td[contains(@class, 'views-field-status') and contains(text(), 'Temporary')]", 1);

    // Test file usage page.
    foreach ($nodes as $node) {
      $file = File::load($node->file->target_id);
      $usage = $file_usage->listUsage($file);
      $this->drupalGet('admin/content/files/usage/' . $file->id());
      $this->assertSession()->statusCodeEquals(200);
      $this->assertSession()->pageTextContains($node->getTitle());
      // Verify that registering entity type is found on usage page.
      $this->assertSession()->pageTextContains('node');
      // Verify that registering module is found on usage page.
      $this->assertSession()->pageTextContains('file');
      foreach ($usage as $module) {
        foreach ($module as $entity_type) {
          foreach ($entity_type as $entity) {
            // Verify that usage count is found on usage page.
            $this->assertSession()->pageTextContains($entity);
          }
        }
      }
      $this->assertSession()->linkByHrefExists('node/' . $node->id(), 0, 'Link to registering entity found on usage page.');
    }

    // Log in as another user that has access to the file list but cannot delete
    // files.
    $role_id = $this->drupalCreateRole([
      'access files overview',
      'bypass node access',
    ]);
    $this->drupalLogin($this->drupalCreateUser(values: ['roles' => [$role_id]]));

    $this->drupalGet('admin/content/files');
    foreach ($nodes as $node) {
      $file = File::load($node->file->target_id);
      $this->assertSession()->pageTextContains($file->getFilename());
      $this->assertSession()->linkByHrefExists($file->createFileUrl());
      $this->assertSession()->linkByHrefExists('admin/content/files/usage/' . $file->id());
      $this->assertSession()->linkByHrefNotExists($file->toUrl('delete-form')->toString());
    }
    // Give the user's role permission to delete files.
    Role::load($role_id)->grantPermission('delete any file')->save();
    $this->drupalGet('admin/content/files');
    foreach ($nodes as $node) {
      $file = File::load($node->file->target_id);
      $this->assertSession()->pageTextContains($file->getFilename());
      $this->assertSession()->linkByHrefExists($file->createFileUrl());
      $this->assertSession()->linkByHrefExists('admin/content/files/usage/' . $file->id());
      $this->assertSession()->linkByHrefExists($file->toUrl('delete-form')->toString());
    }
    // Load the page in a definite order.
    $this->drupalGet('admin/content/files', ['query' => ['order' => 'filename', 'sort' => 'asc']]);
    $this->clickLink('Delete');
    $file_uri = File::load(1)->getFileUri();
    $this->assertSession()->addressMatches('#file/1/delete$#');
    $this->assertSession()->pageTextContains('Are you sure you want to delete the file druplicon.txt?');
    $this->assertFileExists($file_uri);
    $this->assertSession()->buttonExists('Delete')->press();
    $this->assertSession()->pageTextContains('The file druplicon.txt has been deleted.');
    $this->assertFileDoesNotExist($file_uri);
  }

  /**
   * Tests file listing usage page for entities with no canonical link template.
   */
  public function testFileListingUsageNoLink(): void {
    // Login with user with right permissions and test listing.
    $this->drupalLogin($this->adminUser);

    // Create a bundle and attach a File field to the bundle.
    $bundle = $this->randomMachineName();
    EntityTestHelper::createBundle($bundle, NULL, 'entity_test_constraints');
    $this->createFileField('field_test_file', 'entity_test_constraints', $bundle, [], ['file_extensions' => 'txt png']);

    // Create file to attach to entity.
    $file = File::create([
      'filename' => 'druplicon.txt',
      'uri' => 'public://druplicon.txt',
      'filemime' => 'text/plain',
    ]);
    $file->setPermanent();
    file_put_contents($file->getFileUri(), 'hello world');
    $file->save();

    // Create entity and attach the created file.
    $entity_name = $this->randomMachineName();
    $entity = EntityTestConstraints::create([
      'uid' => 1,
      'name' => $entity_name,
      'type' => $bundle,
      'field_test_file' => [
        'target_id' => $file->id(),
      ],
    ]);
    $entity->save();

    // Create node entity and attach the created file.
    $node = $this->drupalCreateNode(['type' => 'article', 'file' => $file]);
    $node->save();

    // Load the file usage page for the created and attached file.
    $this->drupalGet('admin/content/files/usage/' . $file->id());

    $this->assertSession()->statusCodeEquals(200);
    // Entity name should be displayed.
    $this->assertSession()->pageTextContains($entity_name);
    $this->assertSession()->linkNotExists($entity_name, 'Linked entity name not added to file usage listing.');
    $this->assertSession()->linkExists($node->getTitle());
  }

  /**
   * Creates and saves a test file.
   *
   * @return \Drupal\Core\Entity\EntityInterface
   *   A file entity.
   */
  protected function createFile(): EntityInterface {
    // Create a new file entity.
    $file = File::create([
      'uid' => 1,
      'filename' => 'druplicon.txt',
      'uri' => 'public://druplicon.txt',
      'filemime' => 'text/plain',
      'created' => 1,
      'changed' => 1,
    ]);
    $file->setPermanent();
    file_put_contents($file->getFileUri(), 'hello world');

    // Save it, inserting a new record.
    $file->save();

    return $file;
  }

}
