<?php

declare(strict_types=1);

namespace Drupal\KernelTests\Components;

use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Render\Component\Exception\InvalidComponentDataException;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Theme\ComponentPluginManager;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;

/**
 * Tests the correct rendering of components.
 */
#[Group('sdc')]
#[RunTestsInSeparateProcesses]
class ComponentRenderTest extends ComponentKernelTestBase {

  use StringTranslationTrait;

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

  /**
   * {@inheritdoc}
   */
  protected static $themes = ['sdc_theme_test'];

  /**
   * Check using a component with an include and default context.
   */
  public function testRenderIncludeDefaultContent(): void {
    $build = [
      '#type' => 'inline_template',
      '#template' => "{% embed('sdc_theme_test_base:my-card-no-schema') %}{% block card_body %}Foo bar{% endblock %}{% endembed %}",
    ];
    $crawler = $this->renderComponentRenderArray($build);
    $this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test_base:my-card-no-schema"] .component--my-card-no-schema__body:contains("Foo bar")'));
  }

  /**
   * Check using a component with an include and no default context.
   *
   * This covers passing a render array to a 'string' prop, and mapping the
   * prop to a context variable.
   */
  public function testRenderIncludeDataMapping(): void {
    $content = [
      'label' => [
        '#type' => 'html_tag',
        '#tag' => 'span',
        '#value' => 'Another button ç',
      ],
    ];
    $build = [
      '#type' => 'inline_template',
      '#context' => ['content' => $content],
      '#template' => "{{ include('sdc_test:my-button', { text: content.label, iconType: 'external' }, with_context = false) }}",
    ];
    $crawler = $this->renderComponentRenderArray($build);
    $this->assertNotEmpty($crawler->filter('#sdc-wrapper button:contains("Another button ç")'));
  }

  /**
   * Render a card with slots that include a CTA component.
   */
  public function testRenderEmbedWithNested(): void {
    $content = [
      'heading' => [
        '#type' => 'html_tag',
        '#tag' => 'span',
        '#value' => 'Just a link',
      ],
    ];
    $build = [
      '#type' => 'inline_template',
      '#context' => ['content' => $content],
      '#template' => "{% embed 'sdc_theme_test:my-card' with { variant: 'horizontal', header: 'Card header', content: content } only %}{% block card_body %}This is a card with a CTA {{ include('sdc_test:my-cta', { text: content.heading, href: 'https://www.example.org', target: '_blank' }, with_context = false) }}{% endblock %}{% endembed %}",
    ];
    $crawler = $this->renderComponentRenderArray($build);
    $this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test:my-card"] h2.component--my-card__header:contains("Card header")'));
    $this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test:my-card"] .component--my-card__body:contains("This is a card with a CTA")'));
    $this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test:my-card"] .component--my-card__body a[data-component-id="sdc_test:my-cta"]:contains("Just a link")'));
    $this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test:my-card"] .component--my-card__body a[data-component-id="sdc_test:my-cta"][href="https://www.example.org"][target="_blank"]'));

    // Now render a component and assert it contains the debug comments.
    $build = [
      '#type' => 'component',
      '#component' => 'sdc_test:my-banner',
      '#props' => [
        'heading' => 'I am a banner',
        'ctaText' => 'Click me',
        'ctaHref' => 'https://www.example.org',
        'ctaTarget' => '',
      ],
      '#slots' => [
        'banner_body' => [
          '#type' => 'html_tag',
          '#tag' => 'p',
          '#value' => 'This is the contents of the banner body.',
        ],
      ],
    ];
    $metadata = new BubbleableMetadata();
    $this->renderComponentRenderArray($build, $metadata);
    $this->assertEquals(['core/components.sdc_test--my-cta', 'core/components.sdc_test--my-banner'], $metadata->getAttachments()['library']);
  }

  /**
   * Check using the libraryOverrides.
   */
  public function testRenderLibraryOverrides(): void {
    $build = [
      '#type' => 'inline_template',
      '#template' => "{{ include('sdc_theme_test:lib-overrides') }}",
    ];
    $metadata = new BubbleableMetadata();
    $this->renderComponentRenderArray($build, $metadata);
    $this->assertEquals(['core/components.sdc_theme_test--lib-overrides'], $metadata->getAttachments()['library']);
  }

  /**
   * Ensures the schema violations are reported properly.
   */
  public function testRenderPropValidation(): void {
    // 1. Violates the minLength for the text property.
    $content = ['label' => '1'];
    $build = [
      '#type' => 'inline_template',
      '#context' => ['content' => $content],
      '#template' => "{{ include('sdc_test:my-button', { text: content.label, iconType: 'external' }, with_context = false) }}",
    ];
    try {
      $this->renderComponentRenderArray($build);
      $this->fail('Invalid prop did not cause an exception');
    }
    catch (\Throwable) {
      $this->addToAssertionCount(1);
    }

    // 2. Violates the required header property.
    $build = [
      '#type' => 'inline_template',
      '#context' => [],
      '#template' => "{{ include('sdc_theme_test:my-card', with_context = false) }}",
    ];
    try {
      $this->renderComponentRenderArray($build);
      $this->fail('Invalid prop did not cause an exception');
    }
    catch (\Throwable) {
      $this->addToAssertionCount(1);
    }
  }

  /**
   * Ensure fuzzy coercing of arrays and objects works properly.
   */
  public function testRenderArrayObjectTypeCast(): void {
    $content = ['test' => []];
    $build = [
      '#type' => 'inline_template',
      '#context' => ['content' => $content],
      '#template' => "{{ include('sdc_test:array-to-object', { testProp: content.test }, with_context = false) }}",
    ];
    try {
      $this->renderComponentRenderArray($build);
      $this->addToAssertionCount(1);
    }
    catch (\Throwable) {
      $this->fail('Empty array was not converted to object');
    }
  }

  /**
   * Ensures that including an invalid component creates an error.
   */
  public function testRenderNonExistingComponent(): void {
    $build = [
      '#type' => 'inline_template',
      '#context' => [],
      '#template' => "{{ include('sdc_test:INVALID', with_context = false) }}",
    ];
    try {
      $this->renderComponentRenderArray($build);
      $this->fail('Invalid prop did not cause an exception');
    }
    catch (\Throwable) {
      $this->addToAssertionCount(1);
    }
  }

  /**
   * Ensures the attributes are merged properly.
   */
  public function testRenderAttributeMerging(): void {
    $content = ['label' => 'I am a labels'];
    // 1. Check that if it exists Attribute object in the 'attributes' prop, you
    // get them merged.
    $build = [
      '#type' => 'inline_template',
      '#context' => [
        'content' => $content,
        'attributes' => new Attribute(['data-merged-attributes' => 'yes']),
      ],
      '#template' => "{{ include('sdc_test:my-button', { text: content.label, iconType: 'external', attributes: attributes }, with_context = false) }}",
    ];
    $crawler = $this->renderComponentRenderArray($build);
    $this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-merged-attributes="yes"][data-component-id="sdc_test:my-button"]'), $crawler->outerHtml());
    // 2. Check that if the 'attributes' exists, but there is some other data
    // type, then we don't touch it.
    $build = [
      '#type' => 'inline_template',
      '#context' => [
        'content' => $content,
        'attributes' => 'hard-coded-attr',
      ],
      '#template' => "{{ include('sdc_theme_test_base:my-card-no-schema', { header: content.label, attributes: attributes }, with_context = false) }}",
    ];
    $crawler = $this->renderComponentRenderArray($build);
    // The default data attribute should be missing.
    $this->assertEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test_base:my-card-no-schema"]'), $crawler->outerHtml());
    $this->assertNotEmpty($crawler->filter('#sdc-wrapper [hard-coded-attr]'), $crawler->outerHtml());
    // 3. Check that if the 'attributes' is empty, we get the defaults.
    $build = [
      '#type' => 'inline_template',
      '#context' => ['content' => $content],
      '#template' => "{{ include('sdc_theme_test_base:my-card-no-schema', { header: content.label }, with_context = false) }}",
    ];
    $crawler = $this->renderComponentRenderArray($build);
    // The default data attribute should not be missing.
    $this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test_base:my-card-no-schema"]'), $crawler->outerHtml());
  }

  /**
   * Ensures the element and prop attributes are merged properly.
   */
  public function testCheckElementAttributesAndPropAttributesMerging(): void {
    // Test no attributes.
    // Prop: no. Element: no.
    $build = [
      '#type' => 'component',
      '#component' => 'sdc_theme_test:my-card',
      '#props' => [
        'header' => 'Drupal.org',
      ],
    ];
    $crawler = $this->renderComponentRenderArray($build);
    $this->assertEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test:my-card"][foo="bar"]'), $crawler->outerHtml());
    $this->assertEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test:my-card"][bar="fpo"]'), $crawler->outerHtml());

    // Test for just prop attributes.
    // Prop: object. Element: no.
    $build = [
      '#type' => 'component',
      '#component' => 'sdc_theme_test:my-card',
      '#props' => [
        'header' => 'Drupal.org',
        'attributes' => new Attribute([
          'foo' => 'bar',
        ]),
      ],
    ];
    $crawler = $this->renderComponentRenderArray($build);
    $this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test:my-card"][foo="bar"]'), $crawler->outerHtml());

    // Test for prop attributes and element attributes as Attribute object.
    // Prop: object. Element: object.
    $build = [
      '#type' => 'component',
      '#component' => 'sdc_theme_test:my-card',
      '#props' => [
        'header' => 'Drupal.org',
        'attributes' => new Attribute([
          'foo' => 'bar',
        ]),
      ],
      '#attributes' => new Attribute([
        'bar' => 'foo',
      ]),
    ];
    $crawler = $this->renderComponentRenderArray($build);
    $this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test:my-card"][foo="bar"][bar="foo"]'), $crawler->outerHtml());

    // Test for no prop attributes and element attributes as Attribute object.
    // Prop: no. Element: object.
    $build = [
      '#type' => 'component',
      '#component' => 'sdc_theme_test:my-card',
      '#props' => [
        'header' => 'Drupal.org',
      ],
      '#attributes' => new Attribute([
        'bar' => 'foo',
      ]),
    ];
    $crawler = $this->renderComponentRenderArray($build);
    $this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test:my-card"][bar="foo"]'), $crawler->outerHtml());

    // Test for prop attributes and element attributes as Attribute array.
    // Prop: object. Element: array.
    $build = [
      '#type' => 'component',
      '#component' => 'sdc_theme_test:my-card',
      '#props' => [
        'header' => 'Drupal.org',
        'attributes' => new Attribute([
          'foo' => 'bar',
        ]),
      ],
      '#attributes' => [
        'bar' => 'foo',
      ],
    ];
    $crawler = $this->renderComponentRenderArray($build);
    $this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test:my-card"][foo="bar"][bar="foo"]'), $crawler->outerHtml());

    // Test for no prop attributes and element attributes as array.
    // Prop: no. Element: array.
    $build = [
      '#type' => 'component',
      '#component' => 'sdc_theme_test:my-card',
      '#props' => [
        'header' => 'Drupal.org',
      ],
      '#attributes' => [
        'bar' => 'foo',
      ],
    ];
    $crawler = $this->renderComponentRenderArray($build);
    $this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test:my-card"][bar="foo"]'), $crawler->outerHtml());

    // Prop: array. Element: no.
    $build = [
      '#type' => 'component',
      '#component' => 'sdc_theme_test:my-card',
      '#props' => [
        'header' => 'Drupal.org',
        'attributes' => [
          'foo' => 'bar',
        ],
      ],
    ];
    $crawler = $this->renderComponentRenderArray($build);
    $this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test:my-card"][foo="bar"]'), $crawler->outerHtml());

    // Prop: array. Element: object.
    $build = [
      '#type' => 'component',
      '#component' => 'sdc_theme_test:my-card',
      '#props' => [
        'header' => 'Drupal.org',
        'attributes' => [
          'foo' => 'bar',
        ],
      ],
      '#attributes' => new Attribute([
        'bar' => 'foo',
      ]),
    ];
    $crawler = $this->renderComponentRenderArray($build);
    $this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test:my-card"][foo="bar"][bar="foo"]'), $crawler->outerHtml());

    // Prop: array. Element: array.
    $build = [
      '#type' => 'component',
      '#component' => 'sdc_theme_test:my-card',
      '#props' => [
        'header' => 'Drupal.org',
        'attributes' => [
          'foo' => 'bar',
        ],
      ],
      '#attributes' => [
        'bar' => 'foo',
      ],
    ];
    $crawler = $this->renderComponentRenderArray($build);
    $this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test:my-card"][foo="bar"][bar="foo"]'), $crawler->outerHtml());

    // Test overlapping attributes.
    $build = [
      '#type' => 'component',
      '#component' => 'sdc_theme_test:my-card',
      '#props' => [
        'header' => 'Drupal.org',
        'attributes' => new Attribute([
          'foo' => 'bar',
        ]),
      ],
      '#attributes' => [
        'foo' => 'baz',
      ],
    ];
    $crawler = $this->renderComponentRenderArray($build);
    $this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test:my-card"][foo="bar"]'), $crawler->outerHtml());

    // Test array properties attributes.
    $build = [
      '#type' => 'component',
      '#component' => 'sdc_theme_test:my-card',
      '#props' => [
        'header' => 'Drupal.org',
        'attributes' => new Attribute([
          'class' => ['foo', 'bar'],
        ]),
      ],
      '#attributes' => [
        'class' => ['baz'],
      ],
    ];
    $crawler = $this->renderComponentRenderArray($build);
    $this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test:my-card"][class="baz foo bar"]'), $crawler->outerHtml());
  }

  /**
   * Ensures the alter callbacks work properly.
   */
  public function testRenderElementAlters(): void {
    $build = [
      '#type' => 'component',
      '#component' => 'sdc_test:my-banner',
      '#props' => [
        'heading' => 'I am a banner',
        'ctaText' => 'Click me',
        'ctaHref' => 'https://www.example.org',
        'ctaTarget' => '',
      ],
      '#propsAlter' => [
        fn ($props): array => [...$props, 'heading' => 'I am another banner'],
      ],
      '#slots' => [
        'banner_body' => [
          '#type' => 'html_tag',
          '#tag' => 'p',
          '#value' => 'This is the contents of the banner body.',
        ],
      ],
      '#slotsAlter' => [
        static fn ($slots): array => [...$slots, 'banner_body' => ['#markup' => '<h2>Just something else.</h2>']],
      ],
    ];
    $crawler = $this->renderComponentRenderArray($build);
    $this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_test:my-banner"] .component--my-banner--header h3:contains("I am another banner")'));
    $this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_test:my-banner"] .component--my-banner--body:contains("Just something else.")'));
  }

  /**
   * Ensure that the slots allow a render array or a scalar when using the render element.
   */
  public function testRenderSlots(): void {
    $slots = [
      'This is the contents of the banner body.',
      [
        '#plain_text' => 'This is the contents of the banner body.',
      ],
    ];
    foreach ($slots as $slot) {
      $build = [
        '#type' => 'component',
        '#component' => 'sdc_test:my-banner',
        '#props' => [
          'heading' => 'I am a banner',
          'ctaText' => 'Click me',
          'ctaHref' => 'https://www.example.org',
          'ctaTarget' => '',
        ],
        '#slots' => [
          'banner_body' => $slot,
        ],
      ];
      $crawler = $this->renderComponentRenderArray($build);
      $this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_test:my-banner"] .component--my-banner--body:contains("This is the contents of the banner body.")'));
    }
  }

  /**
   * Ensure that the slots throw an error for invalid slots.
   */
  public function testRenderInvalidSlot(): void {
    $build = [
      '#type' => 'component',
      '#component' => 'sdc_test:my-banner',
      '#props' => [
        'heading' => 'I am a banner',
        'ctaText' => 'Click me',
        'ctaHref' => 'https://www.example.org',
        'ctaTarget' => '',
      ],
      '#slots' => [
        'banner_body' => new \stdClass(),
      ],
    ];
    $this->expectException(InvalidComponentDataException::class);
    $this->expectExceptionMessage('Unable to render component "sdc_test:my-banner". A render array or a scalar is expected for the slot "banner_body" when using the render element with the "#slots" property');
    $this->renderComponentRenderArray($build);
  }

  /**
   * Ensure that components can have 0 props.
   */
  public function testRenderEmptyProps(): void {
    $build = [
      '#type' => 'component',
      '#component' => 'sdc_test:no-props',
      '#props' => [],
    ];
    $crawler = $this->renderComponentRenderArray($build);
    $this->assertEquals(
      $crawler->filter('#sdc-wrapper')->innerText(),
      'This is a test string.'
    );
  }

  /**
   * Ensure that components variants render.
   */
  public function testVariants(): void {
    $build = [
      '#type' => 'component',
      '#component' => 'sdc_test:my-cta',
      '#variant' => 'primary',
      '#props' => [
        'text' => 'Test link',
      ],
    ];
    $crawler = $this->renderComponentRenderArray($build);
    $this->assertNotEmpty($crawler->filter('#sdc-wrapper a[data-component-id="sdc_test:my-cta"][data-component-variant="primary"][class*="my-cta-primary"]'));

    // If there were an existing prop named variant, we don't override that for BC reasons.
    $build = [
      '#type' => 'component',
      '#component' => 'sdc_test:my-cta-with-variant-prop',
      '#variant' => 'tertiary',
      '#props' => [
        'text' => 'Test link',
        'variant' => 'secondary',
      ],
    ];
    $crawler = $this->renderComponentRenderArray($build);
    $this->assertEmpty($crawler->filter('#sdc-wrapper a[data-component-id="sdc_test:my-cta-with-variant-prop"][data-component-variant="tertiary"]'));
    $this->assertNotEmpty($crawler->filter('#sdc-wrapper a[data-component-id="sdc_test:my-cta-with-variant-prop"][data-component-variant="secondary"]'));
  }

  /**
   * Ensures some key aspects of the plugin definition are correctly computed.
   *
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function testPluginDefinition(): void {
    $plugin_manager = \Drupal::service('plugin.manager.sdc');
    assert($plugin_manager instanceof ComponentPluginManager);
    $definition = $plugin_manager->getDefinition('sdc_test:my-banner');
    $this->assertSame('my-banner', $definition['machineName']);
    $this->assertStringEndsWith('system/tests/modules/sdc_test/components/my-banner', $definition['path']);
    $this->assertEquals(['core/drupal'], $definition['library']['dependencies']);
    $this->assertNotEmpty($definition['library']['css']['component']);
    $this->assertSame('my-banner.twig', $definition['template']);
    $this->assertNotEmpty($definition['documentation']);
  }

}
