<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_chat\Unit\Service;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\flowdrop_chat\Service\SystemPromptBuilder;
use Drupal\flowdrop_node_type\Service\FlowDropNodeTypeManagerInterface;
use Drupal\flowdrop_node_type\Service\NodeMetadataResolver;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;

/**
 * Tests the SystemPromptBuilder service.
 *
 * @coversDefaultClass \Drupal\flowdrop_chat\Service\SystemPromptBuilder
 * @group flowdrop_chat
 */
class SystemPromptBuilderTest extends UnitTestCase {

  use ProphecyTrait;

  /**
   * The node type manager mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\flowdrop_node_type\Service\FlowDropNodeTypeManagerInterface
   */
  private $nodeTypeManager;

  /**
   * The config factory mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\Core\Config\ConfigFactoryInterface
   */
  private $configFactory;

  /**
   * The config mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\Core\Config\ImmutableConfig
   */
  private $config;

  /**
   * The node metadata resolver mock.
   *
   * @var \Prophecy\Prophecy\ObjectProphecy|\Drupal\flowdrop_node_type\Service\NodeMetadataResolver
   */
  private $metadataResolver;

  /**
   * The service under test.
   *
   * @var \Drupal\flowdrop_chat\Service\SystemPromptBuilder
   */
  private SystemPromptBuilder $builder;

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

    $this->nodeTypeManager = $this->prophesize(FlowDropNodeTypeManagerInterface::class);
    $this->configFactory = $this->prophesize(ConfigFactoryInterface::class);
    $this->config = $this->prophesize(ImmutableConfig::class);

    $this->configFactory->get('flowdrop_chat.settings')
      ->willReturn($this->config->reveal());

    $this->nodeTypeManager->getAllNodeTypes()
      ->willReturn([]);

    // By default the resolver finds no live node type, so the builder falls
    // back to each node's stored metadata. Tests that exercise re-resolution
    // override this per node type.
    $this->metadataResolver = $this->prophesize(NodeMetadataResolver::class);
    $this->metadataResolver->buildForNodeTypeId(Argument::any())
      ->willReturn(NULL);

    $this->builder = new SystemPromptBuilder(
      $this->nodeTypeManager->reveal(),
      $this->configFactory->reveal(),
      $this->metadataResolver->reveal(),
    );
  }

  /**
   * Tests placeholder substitution for {{AVAILABLE_NODE_TYPES}}.
   *
   * @covers ::build
   */
  public function testNodeTypesPlaceholderSubstitution(): void {
    $template = 'Node types: {{AVAILABLE_NODE_TYPES}}';
    $this->config->get('system_prompt_template')->willReturn($template);

    $nodeTypes = [
      'llm_node' => ['id' => 'llm_node', 'name' => 'LLM'],
      'api_node' => ['id' => 'api_node', 'name' => 'API'],
    ];
    $this->nodeTypeManager->getAllNodeTypes()->willReturn($nodeTypes);

    $result = $this->builder->build([], []);

    // slimNodeType() expands minimal node type data with defaults.
    $slimmedTypes = [
      [
        'id' => 'llm_node',
        'name' => 'LLM',
        'type' => 'default',
        'supportedTypes' => ['default'],
        'category' => '',
        'inputs' => [],
        'outputs' => [],
        'configSchema' => [],
      ],
      [
        'id' => 'api_node',
        'name' => 'API',
        'type' => 'default',
        'supportedTypes' => ['default'],
        'category' => '',
        'inputs' => [],
        'outputs' => [],
        'configSchema' => [],
      ],
    ];
    $expected = json_encode($slimmedTypes, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
    $this->assertStringContainsString($expected, $result);
  }

  /**
   * Tests placeholder substitution for {{WORKFLOW_STATE}}.
   *
   * @covers ::build
   */
  public function testWorkflowStatePlaceholderSubstitution(): void {
    $template = 'State: {{WORKFLOW_STATE}}';
    $this->config->get('system_prompt_template')->willReturn($template);

    $state = ['nodes' => [['id' => 'llm_node.1']], 'edges' => []];
    $result = $this->builder->build($state, []);

    // preprocessWorkflowState() normalises nodes into a slim structure.
    $expectedState = [
      'nodes' => [
        [
          'id' => 'llm_node.1',
          'position' => ['x' => 0, 'y' => 0],
          'label' => '',
          'type' => '',
          'config' => [],
          'inputs' => [],
          'outputs' => [],
        ],
      ],
      'edges' => [],
    ];
    $expected = json_encode($expectedState, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
    $this->assertStringContainsString($expected, $result);
  }

  /**
   * Tests placeholder substitution for {{CHAT_HISTORY}}.
   *
   * @covers ::build
   */
  public function testChatHistoryPlaceholderSubstitution(): void {
    $template = 'History: {{CHAT_HISTORY}}';
    $this->config->get('system_prompt_template')->willReturn($template);

    $history = [
      ['role' => 'user', 'content' => 'Add an LLM node'],
      ['role' => 'assistant', 'content' => 'Done!'],
    ];
    $result = $this->builder->build([], $history);

    $this->assertStringContainsString('<message role="user">Add an LLM node</message>', $result);
    $this->assertStringContainsString('<message role="assistant">Done!</message>', $result);
  }

  /**
   * Tests that empty chat history shows placeholder text.
   *
   * @covers ::build
   */
  public function testEmptyChatHistory(): void {
    $template = 'History: {{CHAT_HISTORY}}';
    $this->config->get('system_prompt_template')->willReturn($template);

    $result = $this->builder->build([], []);

    $this->assertStringContainsString('No previous messages.', $result);
  }

  /**
   * Tests that {{USER_MESSAGE}} section is stripped from output.
   *
   * @covers ::build
   */
  public function testUserMessageSectionIsStripped(): void {
    $template = "Some content\n---\n\n## User Message\n\n{{USER_MESSAGE}}\n\n---\nEnd";
    $this->config->get('system_prompt_template')->willReturn($template);

    $result = $this->builder->build([], []);

    $this->assertStringNotContainsString('{{USER_MESSAGE}}', $result);
    $this->assertStringNotContainsString('## User Message', $result);
    $this->assertStringContainsString('Some content', $result);
    $this->assertStringContainsString('End', $result);
  }

  /**
   * Tests fallback {{USER_MESSAGE}} removal when pattern doesn't match.
   *
   * @covers ::build
   */
  public function testUserMessagePlaceholderFallbackRemoval(): void {
    $template = 'Prompt with {{USER_MESSAGE}} inline';
    $this->config->get('system_prompt_template')->willReturn($template);

    $result = $this->builder->build([], []);

    $this->assertStringNotContainsString('{{USER_MESSAGE}}', $result);
    $this->assertStringContainsString('Prompt with  inline', $result);
  }

  /**
   * Tests fallback to built-in template when config template is empty.
   *
   * @covers ::build
   */
  public function testFallbackToDefaultTemplate(): void {
    $this->config->get('system_prompt_template')->willReturn('');

    $result = $this->builder->build([], []);

    // The default template contains the FlowDrop header.
    $this->assertStringContainsString('FlowDrop Workflow Builder', $result);
    $this->assertStringContainsString('DSL Command Reference', $result);
    // User message section should be stripped even from default template.
    $this->assertStringNotContainsString('{{USER_MESSAGE}}', $result);
  }

  /**
   * Tests fallback to default when config returns NULL.
   *
   * @covers ::build
   */
  public function testFallbackToDefaultTemplateWhenNull(): void {
    $this->config->get('system_prompt_template')->willReturn(NULL);

    $result = $this->builder->build([], []);

    $this->assertStringContainsString('FlowDrop Workflow Builder', $result);
  }

  /**
   * Tests that workflow state as string is used directly.
   *
   * @covers ::build
   */
  public function testWorkflowStateAsString(): void {
    $template = 'State: {{WORKFLOW_STATE}}';
    $this->config->get('system_prompt_template')->willReturn($template);

    $stateJson = '{"nodes":[],"edges":[]}';
    $result = $this->builder->build($stateJson, []);

    // String state is decoded, preprocessed, and re-encoded with pretty-print.
    $expectedState = json_encode(['nodes' => [], 'edges' => []], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
    $this->assertStringContainsString('State: ' . $expectedState, $result);
  }

}
