<?php

namespace Drupal\Tests\ai_automators\Kernel;

use Drupal\KernelTests\KernelTestBase;

/**
 * Tests the 11002 provider_config migration and runtime resolution.
 *
 * Covers two regressions surfaced by issue #3577173 on MR !1449:
 *
 * 1. Automators created in 1.x with one of the default_* pseudo-providers
 *    never stored a model. After 11002 unsets the legacy ai_provider /
 *    ai_model keys, RuleBase::getModel() must still resolve the model from
 *    provider_config['provider'] -- otherwise the runtime calls the LLM
 *    with an empty model id.
 * 2. The same migration on an automator with an explicit provider/model
 *    must produce a clean provider_config structure that getModel() and
 *    getProvider() read directly.
 *
 * @group ai_automators
 */
class ProviderConfigMigrationTest extends KernelTestBase {

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'system',
    'user',
    'field',
    'file',
    'filter',
    'text',
    'token',
    'key',
    'node',
    'options',
    'ai',
    'ai_test',
    'ai_automators',
  ];

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

    $this->installEntitySchema('user');
    $this->installEntitySchema('file');
    $this->installEntitySchema('node');
    $this->installEntitySchema('node_type');
    $this->installConfig(['system', 'field', 'file', 'filter', 'node', 'ai']);

    \Drupal::entityTypeManager()
      ->getStorage('node_type')
      ->create(['type' => 'article', 'name' => 'Article'])
      ->save();

    // Pin echoai as the default chat provider so the default_* pseudo
    // providers can resolve to a real provider/model in this kernel test.
    $this->config('ai.settings')
      ->set('default_providers', [
        'chat' => [
          'provider_id' => 'echoai',
          'model_id' => 'gpt-5-mini',
        ],
        'chat_with_complex_json' => [
          'provider_id' => 'echoai',
          'model_id' => 'gpt-4.1-mini',
        ],
        'chat_with_structured_response' => [
          'provider_id' => 'echoai',
          'model_id' => 'o4-mini',
        ],
        'chat_with_image_vision' => [
          'provider_id' => 'echoai',
          'model_id' => 'o3-mini',
        ],
      ])
      ->save();

    \Drupal::moduleHandler()->loadInclude('ai_automators', 'install');
    \Drupal::moduleHandler()->loadInclude('ai_automators', 'php', 'ai_automators.post_update');
  }

  /**
   * Builds an ai_automator entity with a single automator type.
   *
   * @param string $id
   *   The entity id.
   * @param array<string,mixed> $settings
   *   The settings array to seed under automator_types[].settings.
   *
   * @return \Drupal\ai_automators\AiAutomatorInterface
   *   The saved automator config entity.
   */
  protected function createAutomator(string $id, array $settings) {
    $automator = \Drupal::entityTypeManager()
      ->getStorage('ai_automator')
      ->create([
        'id' => $id,
        'label' => $id,
        'entity_type' => 'node',
        'bundle' => 'article',
        'field_name' => 'title',
        'worker_type' => 'direct',
        'weight' => 0,
      ]);
    $automator->addAutomatorType([
      'uuid' => $id . '-uuid',
      'id' => 'llm_string',
      'weight' => 0,
      'label' => 'LLM Text',
      'settings' => $settings,
    ]);
    $automator->save();
    return $automator;
  }

  /**
   * Reloads an automator and returns its first automator type settings.
   *
   * @param string $id
   *   The entity id.
   *
   * @return array<string,mixed>
   *   The settings array as stored after migration.
   */
  protected function reloadSettings(string $id): array {
    $automator = \Drupal::entityTypeManager()
      ->getStorage('ai_automator')
      ->loadUnchanged($id);
    foreach ($automator->getAutomatorTypes() as $type) {
      return $type->getConfiguration()['settings'];
    }
    return [];
  }

  /**
   * Invokes the protected getModel/getProvider on a llm_string instance.
   *
   * @param array<string,mixed> $settings
   *   The settings array to evaluate.
   *
   * @return array{provider:string,model:string}
   *   The resolved provider id and model id.
   */
  protected function resolve(array $settings): array {
    $rule = \Drupal::service('plugin.manager.ai_automator')
      ->createInstance('llm_string');
    $ref = new \ReflectionClass($rule);
    $getModel = $ref->getMethod('getModel');
    $getModel->setAccessible(TRUE);
    $getProvider = $ref->getMethod('getProvider');
    $getProvider->setAccessible(TRUE);
    return [
      'provider' => $getProvider->invokeArgs($rule, [$settings]),
      'model' => $getModel->invokeArgs($rule, [&$settings]),
    ];
  }

  /**
   * Data provider for the four default_* pseudo-providers.
   *
   * @return array<string,array{0:string,1:string}>
   *   Keyed by test label, each row is [pseudo provider key, expected model].
   */
  public static function pseudoProviderProvider(): array {
    return [
      'default' => ['default', 'gpt-5-mini'],
      'default_json' => ['default_json', 'gpt-4.1-mini'],
      'default_structured_response' => ['default_structured_response', 'o4-mini'],
      'default_vision' => ['default_vision', 'o3-mini'],
    ];
  }

  /**
   * Each pseudo-provider migrates and resolves to its configured model.
   *
   * Locks in the fix for cadence96's "you must provide a model parameter"
   * report on MR !1449: 1.x automators that picked one of the default_*
   * pseudo options never stored a model, so after post_update_20001 the
   * model can only
   * be resolved by mapping provider_config['provider'] back to the
   * configured default for the operation type.
   *
   * @dataProvider pseudoProviderProvider
   */
  public function testPseudoProviderRoundTrip(string $pseudoKey, string $expectedModel): void {
    $id = 'test_' . $pseudoKey;
    $this->createAutomator($id, [
      'mode' => 'base',
      'base_field' => 'title',
      'prompt' => 'x',
      // Old form key when the user picked one of the default_* options.
      'ai_provider' => $pseudoKey,
    ]);

    \ai_automators_post_update_20001();

    $settings = $this->reloadSettings($id);
    $this->assertArrayHasKey('provider_config', $settings);
    $this->assertSame($pseudoKey, $settings['provider_config']['provider']);
    $this->assertSame('', $settings['provider_config']['model']);
    $this->assertArrayNotHasKey('ai_provider', $settings);
    $this->assertArrayNotHasKey('ai_model', $settings);

    $resolved = $this->resolve($settings);
    $this->assertSame('echoai', $resolved['provider']);
    $this->assertSame(
      $expectedModel,
      $resolved['model'],
      sprintf('getModel() must resolve %s pseudo-provider via provider_config when the migration has removed the legacy ai_provider key.', $pseudoKey)
    );
  }

  /**
   * Explicit provider+model migrates into provider_config and resolves.
   */
  public function testExplicitProviderModelRoundTrip(): void {
    $this->createAutomator('test_explicit', [
      'mode' => 'base',
      'base_field' => 'title',
      'prompt' => 'x',
      'ai_provider' => 'echoai',
      'ai_model' => 'default',
      'ai_provider_max_tokens' => 100,
    ]);

    \ai_automators_post_update_20001();

    $settings = $this->reloadSettings('test_explicit');
    $this->assertSame('echoai', $settings['provider_config']['provider']);
    $this->assertSame('default', $settings['provider_config']['model']);
    $this->assertSame(['max_tokens' => 100], $settings['provider_config']['config']);
    $this->assertArrayNotHasKey('ai_provider', $settings);
    $this->assertArrayNotHasKey('ai_model', $settings);
    $this->assertArrayNotHasKey('ai_provider_max_tokens', $settings);

    $resolved = $this->resolve($settings);
    $this->assertSame('echoai', $resolved['provider']);
    $this->assertSame('default', $resolved['model']);
  }

  /**
   * Every automator type plugin instantiates without a constructor TypeError.
   *
   * Locks in the fix for the LlmModerationState TypeError reported in
   * cadence96 #13: a leaf class that drifts from the parent constructor
   * signature (e.g. still passes ai.form_helper as positional arg #5)
   * fails this test instead of only blowing up at runtime when a user
   * lands on the field config admin page.
   *
   * Plugins whose container dependencies are missing in this kernel
   * environment (e.g. llm_metatag needs the metatag.manager service) are
   * skipped — their failure mode is a ServiceNotFoundException, not a
   * constructor TypeError, so they are not relevant to this regression.
   */
  public function testAllAutomatorTypesInstantiateWithoutTypeError(): void {
    /** @var \Drupal\Component\Plugin\PluginManagerInterface $manager */
    $manager = \Drupal::service('plugin.manager.ai_automator');
    $definitions = $manager->getDefinitions();
    $this->assertNotEmpty($definitions, 'The ai_automator plugin manager should expose at least one definition.');

    $instantiated = 0;
    $skipped = [];
    foreach (array_keys($definitions) as $pluginId) {
      try {
        $instance = $manager->createInstance($pluginId);
      }
      catch (\TypeError $e) {
        $this->fail(sprintf(
          'Plugin "%s" failed to construct: %s',
          $pluginId,
          $e->getMessage()
        ));
      }
      catch (\Throwable $e) {
        // Missing services / unavailable dependencies are not constructor
        // signature drift, so they are not what this test guards against.
        $skipped[$pluginId] = $e->getMessage();
        continue;
      }
      $this->assertNotNull($instance, sprintf('createInstance returned null for "%s".', $pluginId));
      $instantiated++;
    }

    $this->assertGreaterThan(
      0,
      $instantiated,
      sprintf('No automator type plugin instantiated successfully. Skipped: %s', implode(', ', array_keys($skipped)))
    );
  }

}
