<?php

declare(strict_types=1);

namespace Drupal\Tests\simple_interactive_maps\Kernel;

use Drupal\Core\Render\RenderContext;
use Drupal\filter\FilterPluginCollection;
use Drupal\filter\FilterProcessResult;
use Drupal\KernelTests\KernelTestBase;
use Drupal\simple_interactive_maps\Entity\InteractiveMap;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;

/**
 * Regression guard for MapFilter's entity-storage dependency injection.
 *
 * Storage is now fetched from EntityTypeManagerInterface at the call site
 * inside process(), instead of being cached as a class property set only in
 * the constructor.
 */
#[Group('simple_interactive_maps')]
#[CoversClass(\Drupal\simple_interactive_maps\Plugin\Filter\MapFilter::class)]
#[RunTestsInSeparateProcesses]
final class MapFilterTest extends KernelTestBase {

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'system',
    'filter',
    'field_group',
    'simple_interactive_maps',
  ];

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();
    $this->installConfig(['filter']);
  }

  /**
   * Gets the simple_interactive_maps_map_filter plugin instance.
   */
  private function getFilterPlugin(): object {
    /** @var \Drupal\filter\FilterPluginManager $filterManager */
    $filterManager = $this->container->get('plugin.manager.filter');
    $collection = new FilterPluginCollection($filterManager, []);
    return $collection->get('simple_interactive_maps_map_filter');
  }

  /**
   * Builds a map whose regions cover every key the plugin provides.
   *
   * Mirrors the way InteractiveMapForm::save() seeds a newly created map.
   * Without this, MapBuilder::getRenderHelper() (invoked while rendering the
   * map theme hook) hits unconfigured region keys.
   */
  private function createFullyConfiguredMap(): InteractiveMap {
    /** @var \Drupal\simple_interactive_maps\MapDefinitionPluginManager $pluginManager */
    $pluginManager = $this->container->get('plugin.manager.map_definition');
    $plugin = $pluginManager->createInstance('us_states_territories');

    $regions = [];
    foreach ($plugin->mapData() as $key => $value) {
      $regions[$key] = [
        'id' => $key,
        'label' => $value['label'],
        'hidden' => 0,
        'group' => '',
        'fill_color' => '#4ba0a6',
        'hover_color' => '#076369',
        'stroke_color' => '#e6e6e6',
        'text_color' => '#000000',
        'tooltip' => ['value' => '', 'format' => 'plain_text'],
        'action' => ['plugin_id' => 'none', 'plugin_configuration' => []],
      ];
    }

    $map = InteractiveMap::create([
      'id' => 'test_map',
      'label' => 'Test Map',
      'base_map' => 'us_states_territories',
      'regions' => $regions,
    ]);
    $map->save();

    return $map;
  }

  /**
   * Renders $filter->process() inside an active render context.
   *
   * Filter plugins call $this->renderer->render(), which requires an active
   * render context (the way it always has one while rendering a real page).
   */
  private function processInRenderContext(object $filter, string $text): FilterProcessResult {
    $renderer = $this->container->get('renderer');
    return $renderer->executeInRenderContext(new RenderContext(), function () use ($filter, $text) {
      return $filter->process($text, 'en');
    });
  }

  /**
   * Tests that the <simple-map> tag is replaced with the rendered map.
   */
  public function testDataMapIdTagIsReplacedWithTheRenderedMap(): void {
    $this->createFullyConfiguredMap();
    $filter = $this->getFilterPlugin();

    $processed = (string) $this->processInRenderContext($filter, '<simple-map data-map-id="test_map" data-show-description="false"></simple-map>')->getProcessedText();

    $this->assertStringContainsString('data-map-id="test_map"', $processed);
    $this->assertStringContainsString('interactive_map__wrapper', $processed);
  }

  /**
   * Tests that the legacy shortcode is replaced with the rendered map.
   */
  public function testLegacyShortcodeIsReplacedWithTheRenderedMap(): void {
    $this->createFullyConfiguredMap();
    $filter = $this->getFilterPlugin();

    $processed = (string) $this->processInRenderContext($filter, 'Some text [interactive_map map=test_map] more text')->getProcessedText();

    $this->assertStringContainsString('interactive_map__wrapper', $processed);
    $this->assertStringNotContainsString('[interactive_map map=test_map]', $processed);
  }

  /**
   * Regression guard for MapFilter never declaring a cacheable dependency.
   *
   * MapFilter never declared the embedded InteractiveMap entity as a
   * cacheable dependency on the FilterProcessResult. That meant editing a
   * map's regions (e.g. adding a "Display Modal" action) never invalidated
   * the cached rendered output of any node/field that embeds it via this
   * filter - editors would see their change everywhere except the page(s)
   * where the map was actually embedded, until someone manually cleared the
   * cache. Reproduced by hand: saving the map with new region data did not
   * change the previously rendered node output until
   * $result->addCacheableDependency() was added for both the <simple-map>
   * tag and the [interactive_map map=...] shortcode paths.
   */
  public function testProcessingTagDeclaresTheMapAsCacheableDependency(): void {
    $map = $this->createFullyConfiguredMap();
    $filter = $this->getFilterPlugin();

    $result = $this->processInRenderContext($filter, '<simple-map data-map-id="test_map" data-show-description="false"></simple-map>');

    $this->assertContains($map->getCacheTags()[0], $result->getCacheTags());
  }

  /**
   * Same regression as the tag test, for the legacy shortcode path.
   *
   * See testProcessingTagDeclaresTheMapAsCacheableDependency().
   */
  public function testProcessingShortcodeDeclaresTheMapAsCacheableDependency(): void {
    $map = $this->createFullyConfiguredMap();
    $filter = $this->getFilterPlugin();

    $result = $this->processInRenderContext($filter, '[interactive_map map=test_map]');

    $this->assertContains($map->getCacheTags()[0], $result->getCacheTags());
  }

  /**
   * Tests that an unknown map ID is left unprocessed.
   */
  public function testUnknownMapIdIsLeftUnprocessed(): void {
    $filter = $this->getFilterPlugin();

    $result = $filter->process('[interactive_map map=does_not_exist]', 'en');

    $this->assertStringContainsString('[interactive_map map=does_not_exist]', (string) $result->getProcessedText());
  }

}
