<?php

declare(strict_types=1);

namespace Drupal\Tests\localgov_elections\Functional;

use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
use Drupal\file\Entity\File;
use Drupal\node\NodeStorageInterface;
use Drupal\paragraphs\Entity\Paragraph;

/**
 * Tests alias functionality for Election nodes.
 *
 * @group localgov_elections
 */
final class ElectionAliasTest extends BrowserTestBase {

  /**
   * Test using the Testing profile.
   *
   * @var string
   */
  protected $profile = 'testing';

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

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'pathauto',
    'localgov_core',
    'localgov_media',
  ];


  /**
   * Node storage.
   *
   * @var \Drupal\node\NodeStorageInterface
   */
  protected NodeStorageInterface $nodeStorage;


  /**
   * An election node.
   *
   * @var \Drupal\node\NodeInterface
   */
  protected $election;

  /**
   * An area vote node.
   *
   * @var \Drupal\node\NodeInterface
   */
  protected $areaVote;

  /**
   * The alias manager service.
   *
   * @var \Drupal\path_alias\AliasManager
   */
  protected $aliasManager;

  /**
   * An array of aliases an internal paths for election pages.
   *
   * @var array
   */
  protected array $aliasPaths = [];

  /**
   * A notice paragraph on the election.
   *
   * @var \Drupal\paragraphs\Entity\Paragraph
   */
  protected Paragraph $notice;

  /**
   * Internal path and alias for the notice detail page.
   *
   * @var array
   */
  protected array $noticePaths = [];

  /**
   * {@inheritdoc}
   */
  // @codingStandardsIgnoreStart
  protected $strictConfigSchema = FALSE;
  // @codingStandardsIgnoreEnd

  /**
   * {@inheritdoc}
   */
  protected function initMink() {
    // Rebuild the router before the first HTTP request to ensure
    // views.view_route_names state is fully populated. localgov_core's
    // install hook enables localgov_page_header_display_extender in
    // views.settings, which can trigger a route rebuild before
    // localgov_election_areas_admin view route is registered, causing
    // ViewsLocalTask::getDerivativeDefinitions() to fail on first request.
    $this->container->get('router.builder')->rebuild();
    return parent::initMink();
  }

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

    // Install localgov_elections here to include optional configuration in D11.
    $this->container->get('module_installer')->install(['localgov_elections']);
    $this->rebuildContainer();
    $this->container->get('router.builder')->rebuild();

    $this->nodeStorage = $this->container->get('entity_type.manager')->getStorage('node');
    $this->aliasManager = $this->container->get('path_alias.manager');

    $this->election = $this->drupalCreateNode([
      'title' => "UK Election 2024",
      'status' => TRUE,
      'type' => "localgov_election",
    ]);

    // Ensure the election has a base alias.
    // Check if pathauto created one, if not create manually.
    $alias_storage = $this->container->get('entity_type.manager')->getStorage('path_alias');
    $internal_path = '/node/' . $this->election->id();
    $existing_aliases = $alias_storage->loadByProperties(['path' => $internal_path]);

    if (empty($existing_aliases)) {
      // Create a base alias manually for the test.
      $alias_storage->create([
        'path' => $internal_path,
        'alias' => '/election/uk-election-2024',
        'langcode' => 'en',
      ])->save();
    }

    // Add a notice paragraph to the election before generating aliases.
    $this->notice = Paragraph::create([
      'type' => 'localgov_election_notice',
      'localgov_election_notice_title' => 'Test Election Notice',
      'localgov_election_notice_date' => '2024-01-15T00:00:00',
    ]);
    $this->notice->save();
    $this->election->get('localgov_election_notices')->appendItem($this->notice);
    $this->election->save();

    // Manually generate election sub-page aliases.
    /** @var \Drupal\localgov_elections\Service\ElectionAliasManager $election_alias_manager */
    $election_alias_manager = \Drupal::service('localgov_elections.alias_manager');
    $election_alias_manager->generateElectionAliases($this->election);

    $notice_internal = '/node/' . $this->election->id() . '/notices/' . $this->notice->id();
    $notice_alias = $this->aliasManager->getAliasByPath($notice_internal);
    $this->noticePaths = ['internal' => $notice_internal, 'alias' => $notice_alias];

    $file = File::create([
      'uri' => 'public://empty_test.pdf',
      'status' => 1,
    ]);
    $file->save();

    $this->areaVote = $this->drupalCreateNode([
      'title' => "An area",
      'status' => TRUE,
      'type' => "localgov_area_vote",
      'localgov_election' => $this->election->id(),
      'localgov_election_votes_final' => TRUE,
      'localgov_election_cand_file' => $file->id(),
      'localgov_election_boundary_data' => json_encode([
        'type' => 'Point',
        'coordinates' => [1.234567, 2.345678],
      ]),
    ]);

    // Map page.
    $map_page_url = Url::fromRoute('view.localgov_election_electoral_map.page_map',
        [
          'node' => $this->election->id(),
        ]
    );
    $map_internal = $map_page_url->getInternalPath();
    $map_alias = $this->aliasManager->getAliasByPath('/' . $map_internal);
    $this->aliasPaths['map'] = ['internal' => '/' . $map_internal, 'alias' => $map_alias];

    // Results timeline.
    $results_page_url = Url::fromRoute('view.localgov_election_results_timeline.page_timeline',
        [
          'node' => $this->election->id(),
        ]
    );
    $results_internal = $results_page_url->getInternalPath();
    $results_alias = $this->aliasManager->getAliasByPath('/' . $results_internal);
    $this->aliasPaths['results'] = ['internal' => '/' . $results_internal, 'alias' => $results_alias];

    // Vote share.
    $share_page_url = Url::fromRoute('view.localgov_election_results_vote.page_vote_share',
        [
          'node' => $this->election->id(),
        ]
    );
    $share_internal = $share_page_url->getInternalPath();
    $share_alias = $this->aliasManager->getAliasByPath('/' . $share_internal);
    $this->aliasPaths['share'] = ['internal' => '/' . $share_internal, 'alias' => $share_alias];

    // Electoral candidates.
    $candidate_page_url = Url::fromRoute('view.localgov_electoral_candidates.page_candidates',
        [
          'node' => $this->election->id(),
        ]
    );
    $candidate_internal = $candidate_page_url->getInternalPath();
    $candidate_alias = $this->aliasManager->getAliasByPath('/' . $candidate_internal);
    $this->aliasPaths['candidate'] = ['internal' => '/' . $candidate_internal, 'alias' => $candidate_alias];

  }

  /**
   * Test that aliases are generated and are not the internal paths.
   */
  public function testElectionAliasesAreGenerated(): void {
    $this->assertNotEquals($this->aliasPaths['map']['alias'], $this->aliasPaths['map']['internal']);
    $this->assertNotEquals($this->aliasPaths['results']['alias'], $this->aliasPaths['results']['internal']);
    $this->assertNotEquals($this->aliasPaths['share']['alias'], $this->aliasPaths['share']['internal']);
    $this->assertNotEquals($this->aliasPaths['candidate']['alias'], $this->aliasPaths['candidate']['internal']);
  }

  /**
   * Test that electoral aliases match our expected patterns.
   */
  public function testElectionAliasPatterns(): void {
    // Debug output to see what aliases were actually generated.
    $this->assertNotEmpty($this->aliasPaths['map']['alias'], 'Map alias should not be empty');
    $this->assertNotEmpty($this->aliasPaths['results']['alias'], 'Results alias should not be empty');

    $this->assertTrue(
        $this->matchesPattern(
            $this->aliasPaths['map']['alias'],
            "#^/election/[^/]+/electoral-map$#"),
        sprintf('Map alias "%s" does not match expected pattern', $this->aliasPaths['map']['alias'])
    );
    $this->assertTrue(
        $this->matchesPattern(
            $this->aliasPaths['results']['alias'],
            "#^/election/[^/]+/results$#"),
        sprintf('Results alias "%s" does not match expected pattern', $this->aliasPaths['results']['alias'])
    );
    $this->assertTrue(
        $this->matchesPattern(
            $this->aliasPaths['share']['alias'],
            "#^/election/[^/]+/share$#"),
        sprintf('Share alias "%s" does not match expected pattern', $this->aliasPaths['share']['alias'])
    );
    $this->assertTrue(
        $this->matchesPattern(
            $this->aliasPaths['candidate']['alias'],
            "#^/election/[^/]+/candidates$#"),
        sprintf('Candidate alias "%s" does not match expected pattern', $this->aliasPaths['candidate']['alias'])
    );
  }

  /**
   * Test that a notice alias is generated and is not the internal path.
   */
  public function testNoticeAliasIsGenerated(): void {
    $this->assertNotEquals($this->noticePaths['alias'], $this->noticePaths['internal']);
  }

  /**
   * Test that the notice alias matches the expected pattern.
   */
  public function testNoticeAliasPattern(): void {
    $this->assertNotEmpty($this->noticePaths['alias'], 'Notice alias should not be empty.');
    $this->assertTrue(
      $this->matchesPattern($this->noticePaths['alias'], '#^/election/[^/]+/notices/\d+(-[\w_-]+)?$#'),
      sprintf('Notice alias "%s" does not match expected pattern.', $this->noticePaths['alias'])
    );
  }

  /**
   * Test notice alias generation when notice is added to an aliased election.
   */
  public function testNoticeAliasGeneratedOnElectionUpdate(): void {
    $new_notice = Paragraph::create([
      'type' => 'localgov_election_notice',
      'localgov_election_notice_title' => 'Second Notice',
      'localgov_election_notice_date' => '2024-02-01T00:00:00',
    ]);
    $new_notice->save();

    $this->election->get('localgov_election_notices')->appendItem($new_notice);
    $this->election->save();

    $internal = '/node/' . $this->election->id() . '/notices/' . $new_notice->id();
    $alias = $this->aliasManager->getAliasByPath($internal);

    $this->assertNotEquals($alias, $internal, 'New notice should have an alias after election save.');
    $this->assertTrue(
      $this->matchesPattern($alias, '#^/election/[^/]+/notices/\d+(-[\w_-]+)?$#'),
      sprintf('New notice alias "%s" does not match expected pattern.', $alias)
    );
  }

  /**
   * Test that a notice alias is deleted when the notice is removed.
   */
  public function testNoticeAliasDeletedWhenNoticeRemoved(): void {
    $alias_storage = $this->container->get('entity_type.manager')->getStorage('path_alias');

    $ids_before = $alias_storage->getQuery()
      ->condition('path', $this->noticePaths['internal'])
      ->accessCheck(FALSE)
      ->execute();
    $this->assertNotEmpty($ids_before, 'Alias should exist before notice removal.');

    $this->election->set('localgov_election_notices', []);
    $this->election->save();

    $ids_after = $alias_storage->getQuery()
      ->condition('path', $this->noticePaths['internal'])
      ->accessCheck(FALSE)
      ->execute();
    $this->assertEmpty($ids_after, 'Alias should be deleted after notice removal.');
  }

  /**
   * Check if a string matches a pattern.
   */
  private function matchesPattern($string, $pattern): bool {

    return preg_match($pattern, $string) === 1;
  }

  /**
   * Test election sub-page aliases change after election alias changes.
   */
  public function testSubPageAliasesChangeAfterElectionAliasChange(): void {

    // Get the current paths to compare later.
    $election_path = $this->election->toUrl()->toString();
    $old_map_alias = $this->aliasPaths['map']['alias'];
    $old_results_alias = $this->aliasPaths['results']['alias'];
    $old_share_alias = $this->aliasPaths['share']['alias'];
    $old_candidate_alias = $this->aliasPaths['candidate']['alias'];

    // Change the election alias.
    $alias_storage = $this->container->get('entity_type.manager')->getStorage('path_alias');
    $alias_query = $alias_storage->getQuery();
    $alias_query->condition('alias', $election_path);
    $alias_query->accessCheck(FALSE);
    $alias_ids = $alias_query->execute();

    if (empty($alias_ids)) {
      $this->markTestSkipped('No path alias found for election node.');
    }

    $alias_id = reset($alias_ids);
    $alias = $alias_storage->load($alias_id);

    if (!$alias) {
      $this->markTestSkipped('Could not load path alias entity.');
    }

    /** @var \Drupal\path_alias\Entity\PathAlias $alias */
    $alias->set('alias', '/election/new-election-name');
    $alias->save();

    // Get the newly generated aliases.
    $map_page_url = Url::fromRoute('view.localgov_election_electoral_map.page_map',
        [
          'node' => $this->election->id(),
        ]
    );

    $results_page_url = Url::fromRoute('view.localgov_election_results_timeline.page_timeline',
        [
          'node' => $this->election->id(),
        ]
    );

    $share_page_url = Url::fromRoute('view.localgov_election_results_vote.page_vote_share',
        [
          'node' => $this->election->id(),
        ]
    );

    $candidate_page_url = Url::fromRoute('view.localgov_electoral_candidates.page_candidates',
        [
          'node' => $this->election->id(),
        ]
    );

    // Check they no longer match.
    $this->assertNotEquals($map_page_url, $old_map_alias);
    $this->assertNotEquals($results_page_url, $old_results_alias);
    $this->assertNotEquals($share_page_url, $old_share_alias);
    $this->assertNotEquals($candidate_page_url, $old_candidate_alias);
  }

}
