<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_node_processor\Unit\Plugin\FlowDropNodeProcessor;

use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\flowdrop\DTO\ParameterBag;
use Drupal\flowdrop_node_processor\Plugin\FlowDropNodeProcessor\HtmlToMarkdown;
use PHPUnit\Framework\TestCase;

/**
 * Tests the HtmlToMarkdown node processor.
 *
 * @coversDefaultClass \Drupal\flowdrop_node_processor\Plugin\FlowDropNodeProcessor\HtmlToMarkdown
 * @group flowdrop_node_processor
 */
class HtmlToMarkdownTest extends TestCase {

  /**
   * Creates an HtmlToMarkdown processor instance.
   *
   * @return \Drupal\flowdrop_node_processor\Plugin\FlowDropNodeProcessor\HtmlToMarkdown
   *   The processor instance.
   */
  private function createProcessor(?LoggerChannelFactoryInterface $loggerFactory = NULL): HtmlToMarkdown {
    if ($loggerFactory === NULL) {
      $logger = $this->createMock(LoggerChannelInterface::class);
      $loggerFactory = $this->createMock(LoggerChannelFactoryInterface::class);
      $loggerFactory->method('get')->willReturn($logger);
    }
    return new HtmlToMarkdown([], "html_to_markdown", ["id" => "html_to_markdown"], $loggerFactory);
  }

  /**
   * Tests basic paragraph conversion.
   *
   * @covers ::process
   */
  public function testParagraphConversion(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => "<p>Hello world</p>",
    ]));

    $this->assertTrue($result["success"]);
    $this->assertStringContainsString("Hello world", $result["markdown"]);
  }

  /**
   * Tests heading conversion with default ATX style.
   *
   * @covers ::process
   */
  public function testHeadingAtxStyle(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => "<h1>Title</h1><h2>Subtitle</h2>",
    ]));

    $this->assertTrue($result["success"]);
    $this->assertStringContainsString("# Title", $result["markdown"]);
    $this->assertStringContainsString("## Subtitle", $result["markdown"]);
  }

  /**
   * Tests heading conversion with setext style.
   *
   * @covers ::process
   */
  public function testHeadingSetextStyle(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => "<h1>Title</h1>",
      "heading_style" => "setext",
    ]));

    $this->assertTrue($result["success"]);
    $this->assertStringContainsString("Title", $result["markdown"]);
    $this->assertStringContainsString("===", $result["markdown"]);
  }

  /**
   * Tests bold conversion with default style.
   *
   * @covers ::process
   */
  public function testBoldConversion(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => "<p><strong>bold text</strong></p>",
    ]));

    $this->assertTrue($result["success"]);
    $this->assertStringContainsString("**bold text**", $result["markdown"]);
  }

  /**
   * Tests bold conversion with underscore style.
   *
   * @covers ::process
   */
  public function testBoldUnderscoreStyle(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => "<p><strong>bold text</strong></p>",
      "bold_style" => "__",
    ]));

    $this->assertTrue($result["success"]);
    $this->assertStringContainsString("__bold text__", $result["markdown"]);
  }

  /**
   * Tests italic conversion with default style.
   *
   * @covers ::process
   */
  public function testItalicConversion(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => "<p><em>italic text</em></p>",
    ]));

    $this->assertTrue($result["success"]);
    $this->assertStringContainsString("*italic text*", $result["markdown"]);
  }

  /**
   * Tests italic conversion with underscore style.
   *
   * @covers ::process
   */
  public function testItalicUnderscoreStyle(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => "<p><em>italic text</em></p>",
      "italic_style" => "_",
    ]));

    $this->assertTrue($result["success"]);
    $this->assertStringContainsString("_italic text_", $result["markdown"]);
  }

  /**
   * Tests link conversion.
   *
   * @covers ::process
   */
  public function testLinkConversion(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => '<p><a href="https://example.com">Example</a></p>',
    ]));

    $this->assertTrue($result["success"]);
    $this->assertStringContainsString("[Example](https://example.com)", $result["markdown"]);
  }

  /**
   * Tests unordered list conversion.
   *
   * @covers ::process
   */
  public function testUnorderedListConversion(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => "<ul><li>Item 1</li><li>Item 2</li></ul>",
    ]));

    $this->assertTrue($result["success"]);
    $this->assertStringContainsString("Item 1", $result["markdown"]);
    $this->assertStringContainsString("Item 2", $result["markdown"]);
  }

  /**
   * Tests empty input returns success with empty markdown.
   *
   * @covers ::process
   */
  public function testEmptyInput(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => "",
    ]));

    $this->assertTrue($result["success"]);
    $this->assertSame("", $result["markdown"]);
  }

  /**
   * Tests null input returns success with empty markdown.
   *
   * @covers ::process
   */
  public function testNullInput(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => NULL,
    ]));

    $this->assertTrue($result["success"]);
    $this->assertSame("", $result["markdown"]);
  }

  /**
   * Tests missing html parameter returns success with empty markdown.
   *
   * @covers ::process
   */
  public function testMissingInput(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([]));

    $this->assertTrue($result["success"]);
    $this->assertSame("", $result["markdown"]);
  }

  /**
   * Tests oversized input is rejected with logging.
   *
   * @covers ::process
   */
  public function testOversizedInputRejected(): void {
    $logger = $this->createMock(LoggerChannelInterface::class);
    $logger->expects($this->once())
      ->method('warning')
      ->with(
        $this->stringContains('Input exceeds maximum size'),
        $this->anything(),
      );
    $loggerFactory = $this->createMock(LoggerChannelFactoryInterface::class);
    $loggerFactory->method('get')->with('flowdrop_node_processor')->willReturn($logger);

    $processor = $this->createProcessor($loggerFactory);
    $oversizedHtml = str_repeat('a', 1048577);
    $result = $processor->process(new ParameterBag([
      "html" => $oversizedHtml,
    ]));

    $this->assertFalse($result["success"]);
    $this->assertSame("", $result["markdown"]);
  }

  /**
   * Tests input at exactly the max size is accepted.
   *
   * @covers ::process
   */
  public function testMaxSizeInputAccepted(): void {
    $processor = $this->createProcessor();
    $html = '<p>' . str_repeat('a', 1048576 - 7) . '</p>';
    $result = $processor->process(new ParameterBag([
      "html" => $html,
    ]));

    $this->assertTrue($result["success"]);
  }

  /**
   * Tests validation accepts valid parameters.
   *
   * @covers ::validateParams
   */
  public function testValidateParamsSuccess(): void {
    $processor = $this->createProcessor();
    $result = $processor->validateParams([
      "html" => "<p>test</p>",
      "heading_style" => "atx",
      "bold_style" => "**",
      "italic_style" => "*",
    ]);

    $this->assertTrue($result->isValid());
  }

  /**
   * Tests validation rejects invalid heading style.
   *
   * @covers ::validateParams
   */
  public function testValidateParamsInvalidHeadingStyle(): void {
    $processor = $this->createProcessor();
    $result = $processor->validateParams([
      "heading_style" => "invalid",
    ]);

    $this->assertFalse($result->isValid());
    $this->assertTrue($result->hasErrors());
  }

  /**
   * Tests validation rejects invalid bold style.
   *
   * @covers ::validateParams
   */
  public function testValidateParamsInvalidBoldStyle(): void {
    $processor = $this->createProcessor();
    $result = $processor->validateParams([
      "bold_style" => "invalid",
    ]);

    $this->assertFalse($result->isValid());
    $this->assertTrue($result->hasErrors());
  }

  /**
   * Tests validation rejects invalid italic style.
   *
   * @covers ::validateParams
   */
  public function testValidateParamsInvalidItalicStyle(): void {
    $processor = $this->createProcessor();
    $result = $processor->validateParams([
      "italic_style" => "invalid",
    ]);

    $this->assertFalse($result->isValid());
    $this->assertTrue($result->hasErrors());
  }

  /**
   * Tests validation collects multiple errors.
   *
   * @covers ::validateParams
   */
  public function testValidateParamsMultipleErrors(): void {
    $processor = $this->createProcessor();
    $result = $processor->validateParams([
      "heading_style" => "bad",
      "bold_style" => "bad",
      "italic_style" => "bad",
    ]);

    $this->assertFalse($result->isValid());
    $this->assertSame(3, $result->getErrorCount());
  }

  /**
   * Tests that script tags are removed from output.
   *
   * @covers ::process
   */
  public function testScriptTagRemoved(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => "<p>Hello</p><script>alert('XSS')</script>",
    ]));

    $this->assertTrue($result["success"]);
    $this->assertStringNotContainsString("script", $result["markdown"]);
    $this->assertStringNotContainsString("alert", $result["markdown"]);
    $this->assertStringContainsString("Hello", $result["markdown"]);
  }

  /**
   * Tests that iframe tags are removed from output.
   *
   * @covers ::process
   */
  public function testIframeTagRemoved(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => '<p>Content</p><iframe src="https://evil.com"></iframe>',
    ]));

    $this->assertTrue($result["success"]);
    $this->assertStringNotContainsString("iframe", $result["markdown"]);
    $this->assertStringNotContainsString("evil.com", $result["markdown"]);
    $this->assertStringContainsString("Content", $result["markdown"]);
  }

  /**
   * Tests that style tags are removed from output.
   *
   * @covers ::process
   */
  public function testStyleTagRemoved(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => '<style>body{display:none}</style><p>Visible</p>',
    ]));

    $this->assertTrue($result["success"]);
    $this->assertStringNotContainsString("style", $result["markdown"]);
    $this->assertStringNotContainsString("display:none", $result["markdown"]);
    $this->assertStringContainsString("Visible", $result["markdown"]);
  }

  /**
   * Tests that javascript: URIs in links are sanitized.
   *
   * @covers ::process
   */
  public function testJavascriptUriSanitized(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => '<p><a href="javascript:alert(1)">Click</a></p>',
    ]));

    $this->assertTrue($result["success"]);
    $this->assertStringContainsString("Click", $result["markdown"]);
    $this->assertStringNotContainsString("javascript:", $result["markdown"]);
    $this->assertStringContainsString("[Click](#)", $result["markdown"]);
  }

  /**
   * Tests that HTML entity-encoded javascript: URIs are sanitized.
   *
   * @covers ::process
   */
  public function testEncodedJavascriptUriSanitized(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => '<p><a href="&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;alert(1)">Click</a></p>',
    ]));

    $this->assertTrue($result["success"]);
    $this->assertStringNotContainsString("javascript:", $result["markdown"]);
  }

  /**
   * Tests that data: URIs in links are sanitized.
   *
   * @covers ::process
   */
  public function testDataUriSanitized(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => '<p><a href="data:text/html,&lt;script&gt;alert(1)&lt;/script&gt;">Click</a></p>',
    ]));

    $this->assertTrue($result["success"]);
    $this->assertStringNotContainsString("data:", $result["markdown"]);
    $this->assertStringContainsString("[Click](#)", $result["markdown"]);
  }

  /**
   * Tests that data: URIs in images are sanitized.
   *
   * @covers ::process
   */
  public function testDataUriInImageSanitized(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => '<img src="data:image/svg+xml;base64,PHN2ZyBvbmxvYWQ9ImFsZXJ0KDEpIj48L3N2Zz4=">',
    ]));

    $this->assertTrue($result["success"]);
    $this->assertStringNotContainsString("data:", $result["markdown"]);
  }

  /**
   * Tests that vbscript: URIs in links are sanitized.
   *
   * @covers ::process
   */
  public function testVbscriptUriSanitized(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => '<p><a href="vbscript:MsgBox(1)">Click</a></p>',
    ]));

    $this->assertTrue($result["success"]);
    $this->assertStringNotContainsString("vbscript:", $result["markdown"]);
  }

  /**
   * Tests that safe URIs (http, https, mailto, ftp, tel) are preserved.
   *
   * @covers ::process
   */
  public function testSafeUrisPreserved(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => '<p><a href="https://example.com">HTTPS</a> <a href="http://example.com">HTTP</a> <a href="mailto:test@example.com">Email</a> <a href="ftp://files.example.com">FTP</a> <a href="tel:+1234567890">Phone</a></p>',
    ]));

    $this->assertTrue($result["success"]);
    $this->assertStringContainsString("https://example.com", $result["markdown"]);
    $this->assertStringContainsString("http://example.com", $result["markdown"]);
    $this->assertStringContainsString("mailto:test@example.com", $result["markdown"]);
    $this->assertStringContainsString("ftp://files.example.com", $result["markdown"]);
    $this->assertStringContainsString("tel:+1234567890", $result["markdown"]);
  }

  /**
   * Tests that unknown URI schemes are blocked by allowlist.
   *
   * @covers ::process
   */
  public function testUnknownUriSchemeBlocked(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => '<p><a href="custom-scheme:payload">Click</a></p>',
    ]));

    $this->assertTrue($result["success"]);
    $this->assertStringNotContainsString("custom-scheme:", $result["markdown"]);
    $this->assertStringContainsString("[Click](#)", $result["markdown"]);
  }

  /**
   * Tests that object and embed tags are removed from output.
   *
   * @covers ::process
   */
  public function testObjectAndEmbedTagsRemoved(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => '<p>Safe</p><object data="evil.swf"></object><embed src="evil.swf">',
    ]));

    $this->assertTrue($result["success"]);
    $this->assertStringNotContainsString("object", $result["markdown"]);
    $this->assertStringNotContainsString("embed", $result["markdown"]);
    $this->assertStringNotContainsString("evil.swf", $result["markdown"]);
    $this->assertStringContainsString("Safe", $result["markdown"]);
  }

  /**
   * Tests that form tags are removed from output.
   *
   * @covers ::process
   */
  public function testFormTagRemoved(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => '<form action="https://evil.com"><input type="text"></form><p>Content</p>',
    ]));

    $this->assertTrue($result["success"]);
    $this->assertStringNotContainsString("form", $result["markdown"]);
    $this->assertStringNotContainsString("input", $result["markdown"]);
    $this->assertStringContainsString("Content", $result["markdown"]);
  }

  /**
   * Tests parameter schema structure.
   *
   * @covers ::getParameterSchema
   */
  public function testGetParameterSchema(): void {
    $processor = $this->createProcessor();
    $schema = $processor->getParameterSchema();

    $this->assertSame("object", $schema["type"]);
    $this->assertArrayHasKey("html", $schema["properties"]);
    $this->assertArrayHasKey("heading_style", $schema["properties"]);
    $this->assertArrayHasKey("bold_style", $schema["properties"]);
    $this->assertArrayHasKey("italic_style", $schema["properties"]);
    $this->assertArrayHasKey("strip_placeholder_links", $schema["properties"]);
    $this->assertArrayNotHasKey("strip_tags", $schema["properties"]);
  }

  /**
   * Tests output schema structure.
   *
   * @covers ::getOutputSchema
   */
  public function testGetOutputSchema(): void {
    $processor = $this->createProcessor();
    $schema = $processor->getOutputSchema();

    $this->assertSame("object", $schema["type"]);
    $this->assertArrayHasKey("markdown", $schema["properties"]);
    $this->assertArrayHasKey("success", $schema["properties"]);
  }

  /**
   * Tests that SVG elements and their content are removed.
   *
   * @covers ::process
   */
  public function testSvgTagRemoved(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => '<p>Safe</p><svg onload="alert(1)"><circle r="50"/></svg>',
    ]));

    $this->assertTrue($result["success"]);
    $this->assertStringNotContainsString("svg", $result["markdown"]);
    $this->assertStringNotContainsString("onload", $result["markdown"]);
    $this->assertStringNotContainsString("circle", $result["markdown"]);
    $this->assertStringContainsString("Safe", $result["markdown"]);
  }

  /**
   * Tests that math elements are removed from output.
   *
   * @covers ::process
   */
  public function testMathTagRemoved(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => '<p>Content</p><math><mi>x</mi></math>',
    ]));

    $this->assertTrue($result["success"]);
    $this->assertStringNotContainsString("math", $result["markdown"]);
    $this->assertStringContainsString("Content", $result["markdown"]);
  }

  /**
   * Tests that applet tags are removed from output.
   *
   * @covers ::process
   */
  public function testAppletTagRemoved(): void {
    $processor = $this->createProcessor();
    $result = $processor->process(new ParameterBag([
      "html" => '<p>Safe</p><applet code="Evil.class"></applet>',
    ]));

    $this->assertTrue($result["success"]);
    $this->assertStringNotContainsString("applet", $result["markdown"]);
    $this->assertStringNotContainsString("Evil", $result["markdown"]);
    $this->assertStringContainsString("Safe", $result["markdown"]);
  }

}
