<?php

declare(strict_types=1);

namespace Drupal\Tests\flowdrop_ai_context\Kernel\Plugin\FlowDropNodeProcessor;

use Drupal\ai_context\Entity\AiContextItem;
use Drupal\flowdrop\DTO\ExecutionContextDTO;
use Drupal\flowdrop\DTO\ParameterBag;
use Drupal\flowdrop_ai_context\Plugin\FlowDropNodeProcessor\AiContextDynamicSelector;
use Drupal\KernelTests\KernelTestBase;
use Drupal\user\Entity\User;

/**
 * @coversDefaultClass \Drupal\flowdrop_ai_context\Plugin\FlowDropNodeProcessor\AiContextDynamicSelector
 * @group flowdrop_ai_context
 * @runTestsInSeparateProcesses
 */
final class AiContextDynamicSelectorTest extends KernelTestBase {

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'system',
    'user',
    'field',
    'datetime',
    'text',
    'filter',
    'options',
    'node',
    'taxonomy',
    'views',
    'content_moderation',
    'workflows',
    'key',
    'modeler_api',
    'ai',
    'ai_agents',
    'scheduler',
    'scheduler_content_moderation_integration',
    'ai_context',
    'flowdrop',
    'flowdrop_ai_context',
  ];

  /**
   * Owner account for created context entities.
   */
  private User $owner;

  /**
   * The processor under test.
   */
  private AiContextDynamicSelector $processor;

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

    $this->installEntitySchema('user');
    $this->installEntitySchema('taxonomy_vocabulary');
    $this->installEntitySchema('taxonomy_term');
    $this->installEntitySchema('ai_context_item');
    $this->installEntitySchema('ai_context_usage');
    $this->installSchema('system', ['sequences']);
    $this->installConfig(['filter', 'ai_context']);

    $admin = User::create(['name' => 'admin', 'status' => 1]);
    $admin->save();
    $this->owner = $admin;
    $this->container->get('current_user')->setAccount($admin);

    $this->processor = AiContextDynamicSelector::create(
      $this->container,
      [],
      'ai_context_dynamic_selector',
      ['id' => 'ai_context_dynamic_selector'],
    );
  }

  /**
   * @covers ::validateParams
   */
  public function testValidateParamsFailsWhenTaskMissing(): void {
    $this->assertFalse($this->processor->validateParams([])->isValid());
  }

  /**
   * @covers ::validateParams
   */
  public function testValidateParamsFailsWhenTaskEmpty(): void {
    $this->assertFalse($this->processor->validateParams(['task' => ''])->isValid());
  }

  /**
   * @covers ::validateParams
   */
  public function testValidateParamsSucceedsWithTask(): void {
    $this->assertTrue($this->processor->validateParams(['task' => 'Find brand voice'])->isValid());
  }

  /**
   * @covers ::getParameterSchema
   */
  public function testParameterSchemaExposesTaskAndMaxItems(): void {
    $schema = $this->processor->getParameterSchema();
    $this->assertArrayHasKey('task', $schema['properties']);
    $this->assertArrayHasKey('max_items', $schema['properties']);
    $this->assertSame(0, $schema['properties']['max_items']['default']);
  }

  /**
   * @covers ::getOutputSchema
   */
  public function testOutputSchemaExposesAllDocumentedKeys(): void {
    $schema = $this->processor->getOutputSchema();
    foreach (['rendered_text', 'selected_ids', 'item_count', 'tokens_used', 'success', 'error_message'] as $key) {
      $this->assertArrayHasKey($key, $schema['properties']);
    }
  }

  /**
   * @covers ::process
   */
  public function testProcessReturnsEmptyResultWithoutErrorOnNoMatches(): void {
    $output = $this->processor->process(new ParameterBag(['task' => 'no candidates exist yet']));

    $this->assertTrue($output['success']);
    $this->assertSame('', $output['error_message']);
    $this->assertSame(0, $output['item_count']);
    $this->assertSame([], $output['selected_ids']);
    $this->assertSame('', $output['rendered_text']);
  }

  /**
   * @covers ::process
   */
  public function testProcessSelectsPublishedItemsAndRendersBlock(): void {
    $item = $this->createContextItem('Brand Voice', 'Use friendly tone.');

    $output = $this->processor->process(new ParameterBag(['task' => 'tone of voice']));

    $this->assertTrue($output['success']);
    $this->assertSame('', $output['error_message']);
    $this->assertGreaterThanOrEqual(1, $output['item_count']);
    $this->assertContains((string) $item->id(), $output['selected_ids']);
    $this->assertStringContainsString('- id: ' . $item->id(), $output['rendered_text']);
    $this->assertGreaterThan(0, $output['tokens_used']);
  }

  /**
   * @covers ::process
   */
  public function testProcessRespectsMaxItems(): void {
    $this->createContextItem('Item A', 'content a');
    $this->createContextItem('Item B', 'content b');
    $this->createContextItem('Item C', 'content c');

    $output = $this->processor->process(new ParameterBag([
      'task' => 'whatever',
      'max_items' => 2,
    ]));

    $this->assertTrue($output['success']);
    $this->assertLessThanOrEqual(2, $output['item_count']);
  }

  /**
   * @covers ::getParameterSchema
   */
  public function testParameterSchemaExposesAdvancedParameters(): void {
    $props = $this->processor->getParameterSchema()['properties'];
    foreach ([
      'scope_subscriptions',
      'agent_id',
      'entity_type',
      'entity_id',
      'always_include',
      'never_include',
      'selection_mode',
      'max_tokens',
    ] as $key) {
      $this->assertArrayHasKey($key, $props);
    }
    $this->assertSame('ai_context_scope_subscriptions', $props['scope_subscriptions']['format']);
    $this->assertSame('ai_context_agent_selector', $props['agent_id']['format']);
    $this->assertContains('minimal', $props['selection_mode']['enum']);
    $this->assertContains('match_all', $props['selection_mode']['enum']);
  }

  /**
   * @covers ::getOutputSchema
   */
  public function testOutputSchemaIncludesSelectedItems(): void {
    $schema = $this->processor->getOutputSchema();
    $this->assertArrayHasKey('selected_items', $schema['properties']);
  }

  /**
   * @covers ::process
   * @covers ::buildSelectedItems
   */
  public function testProcessReturnsSelectedItemsMetadata(): void {
    $this->createContextItem('Brand Voice', 'Use friendly tone.');

    $output = $this->processor->process(new ParameterBag(['task' => 'tone']));

    $this->assertNotEmpty($output['selected_items']);
    $first = $output['selected_items'][0];
    $this->assertArrayHasKey('id', $first);
    $this->assertArrayHasKey('label', $first);
    $this->assertArrayHasKey('purpose', $first);
  }

  /**
   * @covers ::process
   */
  public function testNeverIncludeExcludesItem(): void {
    $keep = $this->createContextItem('Keep', 'keep content');
    $drop = $this->createContextItem('Drop', 'drop content');

    $output = $this->processor->process(new ParameterBag([
      'task' => 'whatever',
      'never_include' => [(string) $drop->id()],
    ]));

    $this->assertContains((string) $keep->id(), $output['selected_ids']);
    $this->assertNotContains((string) $drop->id(), $output['selected_ids']);
  }

  /**
   * @covers ::process
   */
  public function testAlwaysIncludeForcesItemInMinimalMode(): void {
    $item = $this->createContextItem('Forced', 'forced content');

    // In minimal mode a non-global item is not a candidate on its own.
    $without = $this->processor->process(new ParameterBag([
      'task' => 'whatever',
      'selection_mode' => 'minimal',
    ]));
    $this->assertNotContains((string) $item->id(), $without['selected_ids']);

    // always_include forces it in regardless of scope filtering.
    $with = $this->processor->process(new ParameterBag([
      'task' => 'whatever',
      'selection_mode' => 'minimal',
      'always_include' => [(string) $item->id()],
    ]));
    $this->assertContains((string) $item->id(), $with['selected_ids']);
  }

  /**
   * @covers ::process
   */
  public function testUsageTrackingRecordsSelectedItemsWhenEnabled(): void {
    $this->config('ai_context.settings')->set('usage_tracking_enabled', TRUE)->save();
    $this->createContextItem('Tracked', 'tracked content');

    $this->processor->setExecutionContext(
      new ExecutionContextDTO([], 'wf_test', 'pipe_1', 'exec_1', 'node_1'),
    );

    $this->processor->process(new ParameterBag(['task' => 'tone']));

    $usage = $this->container->get('entity_type.manager')
      ->getStorage('ai_context_usage')
      ->loadMultiple();
    $this->assertNotEmpty($usage);
  }

  /**
   * @covers ::process
   */
  public function testUsageTrackingNoOpWhenDisabled(): void {
    // Usage tracking is disabled by default.
    $this->createContextItem('Untracked', 'untracked content');

    $this->processor->setExecutionContext(
      new ExecutionContextDTO([], 'wf_test', 'pipe_1', 'exec_1', 'node_1'),
    );

    $this->processor->process(new ParameterBag(['task' => 'tone']));

    $usage = $this->container->get('entity_type.manager')
      ->getStorage('ai_context_usage')
      ->loadMultiple();
    $this->assertEmpty($usage);
  }

  /**
   * Creates one published test context item.
   *
   * @param string $label
   *   The label of the context item.
   * @param string $content
   *   The content (guidance) of the context item.
   * @param array<string, mixed> $overrides
   *   Entity field overrides.
   */
  private function createContextItem(string $label, string $content, array $overrides = []): AiContextItem {
    $item = AiContextItem::create(array_merge([
      'type' => 'default',
      'uid' => $this->owner->id(),
      'label' => $label,
      'description' => [['value' => 'Desc for ' . $label, 'format' => 'plain_text']],
      'purpose' => [['value' => 'Purpose for ' . $label, 'format' => 'plain_text']],
      'content' => [['value' => $content, 'format' => 'plain_text']],
      'status' => 1,
      'is_global' => 0,
    ], $overrides));
    $item->save();
    return $item;
  }

}
