<?php

declare(strict_types=1);

namespace Drupal\Tests\ai_assistant_api\Kernel;

use Drupal\KernelTests\KernelTestBase;
use Drupal\Core\Site\Settings;
use Drupal\ai\OperationType\Chat\ChatOutput;
use Drupal\ai_assistant_api\AiAssistantApiRunner;
use Drupal\ai_assistant_api\Data\UserMessage;
use Drupal\ai_assistant_api\Entity\AiAssistant;
use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
use Drupal\user\UserInterface;

/**
 * Tests execution principal behavior in the AI assistant runner.
 *
 * @group ai_assistant_api
 */
class AiAssistantApiRunnerExecutionPrincipalTest extends KernelTestBase {

  /**
   * Modules to enable.
   *
   * @var string[]
   */
  protected static $modules = [
    'system',
    'user',
    'key',
    'token',
    'file',
    'ai',
    'ai_test',
    'ai_assistant_api',
  ];

  /**
   * A user with access to run the assistant.
   *
   * @var \Drupal\user\UserInterface
   */
  protected UserInterface $allowedUser;

  /**
   * A user without access to run the assistant.
   *
   * @var \Drupal\user\UserInterface
   */
  protected UserInterface $disallowedUser;

  /**
   * An explicit executor user with access.
   *
   * @var \Drupal\user\UserInterface
   */
  protected UserInterface $executorUser;

  /**
   * A second explicit executor user with access.
   *
   * @var \Drupal\user\UserInterface
   */
  protected UserInterface $secondExecutorUser;

  /**
   * A blocked executor user.
   *
   * @var \Drupal\user\UserInterface
   */
  protected UserInterface $blockedExecutorUser;

  /**
   * A counter used for unique assistant IDs in tests.
   *
   * @var int
   */
  protected int $assistantCounter = 0;

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

    $this->installEntitySchema('user');
    $this->installEntitySchema('ai_mock_provider_result');
    $this->installConfig(['user', 'ai', 'ai_test', 'ai_assistant_api']);

    Role::create([
      'id' => 'assistant_runner',
      'label' => 'Assistant Runner',
    ])->save();

    $this->allowedUser = $this->createUserWithRoles(['assistant_runner']);
    $this->disallowedUser = $this->createUserWithRoles();
    $this->executorUser = $this->createUserWithRoles(['assistant_runner']);
    $this->secondExecutorUser = $this->createUserWithRoles(['assistant_runner']);
    $this->blockedExecutorUser = $this->createUserWithRoles(['assistant_runner'], FALSE);
  }

  /**
   * Ensures missing/invalid/non-loadable executors fail before execution.
   *
   * @dataProvider invalidExecutorProvider
   */
  public function testInvalidExecutorFailsEarly(int|string|null $executorUid, string $expectedMessage): void {
    $this->setCurrentUserAccount($this->allowedUser);

    $assistant = $this->createAssistant('does_not_exist');
    $runner = $this->buildRunner($assistant);
    $runner->setExecutorUid($executorUid);

    try {
      $runner->process();
      $this->fail('Expected an executor validation failure.');
    }
    catch (\InvalidArgumentException $exception) {
      $this->assertStringContainsString($expectedMessage, $exception->getMessage());
    }
  }

  /**
   * Data provider for invalid executor scenarios.
   *
   * @return array
   *   Provider values.
   */
  public static function invalidExecutorProvider(): array {
    return [
      'missing' => [NULL, 'A valid executor UID is required.'],
      'invalid' => ['abc', 'A valid executor UID is required.'],
      'non-loadable' => [999999, 'could not be loaded'],
    ];
  }

  /**
   * Ensures blocked executor fails before provider or action execution.
   */
  public function testBlockedExecutorFailsEarly(): void {
    $this->setCurrentUserAccount($this->allowedUser);

    $assistant = $this->createAssistant('does_not_exist');
    $runner = $this->buildRunner($assistant);
    $runner->setExecutorUid((int) $this->blockedExecutorUser->id());

    $this->expectException(\InvalidArgumentException::class);
    $this->expectExceptionMessage('is blocked');
    $runner->process();
  }

  /**
   * Ensures executor mode fails clearly if account switcher is unavailable.
   */
  public function testExecutorRequiresAccountSwitcherWhenMissing(): void {
    $this->setCurrentUserAccount($this->allowedUser);

    $assistant = $this->createAssistant('echoai');
    $runner = $this->buildRunnerWithoutAccountSwitcher($assistant);
    $runner->setExecutorUid((int) $this->executorUser->id());

    $this->expectException(\LogicException::class);
    $this->expectExceptionMessage('Account switcher service is required');
    $runner->process();
  }

  /**
   * Ensures explicit executor overrides ambient session user for access checks.
   */
  public function testProvidedExecutorOverridesAmbientUser(): void {
    $this->setCurrentUserAccount($this->disallowedUser);

    $assistant = $this->createAssistant('echoai');
    $runner = $this->buildRunner($assistant);
    $runner->setExecutorUid((int) $this->executorUser->id());

    $output = $runner->process();
    $this->assertTrue($output instanceof ChatOutput || is_array($output));
    $this->assertSame((string) $this->disallowedUser->id(), (string) \Drupal::currentUser()->id());
  }

  /**
   * Ensures session_one_thread storage is isolated by explicit executor.
   */
  public function testSessionOneThreadUsesExecutorScopedThreadKey(): void {
    $this->setCurrentUserAccount($this->disallowedUser);

    $assistant = $this->createAssistant('echoai', 'session_one_thread');

    $firstRunner = $this->buildRunner($assistant);
    $firstRunner->setExecutorUid((int) $this->executorUser->id());
    $firstOutput = $firstRunner->process();
    $this->assertTrue($firstOutput instanceof ChatOutput || is_array($firstOutput));
    $firstThreadId = $firstRunner->getThreadsKey();

    $repeatRunner = $this->buildRunner($assistant);
    $repeatRunner->setExecutorUid((int) $this->executorUser->id());
    $repeatOutput = $repeatRunner->process();
    $this->assertTrue($repeatOutput instanceof ChatOutput || is_array($repeatOutput));
    $this->assertSame($firstThreadId, $repeatRunner->getThreadsKey());

    $secondRunner = $this->buildRunner($assistant);
    $secondRunner->setExecutorUid((int) $this->secondExecutorUser->id());
    $secondOutput = $secondRunner->process();
    $this->assertTrue($secondOutput instanceof ChatOutput || is_array($secondOutput));
    $this->assertNotSame($firstThreadId, $secondRunner->getThreadsKey());
    $this->assertSame((string) $this->disallowedUser->id(), (string) \Drupal::currentUser()->id());
  }

  /**
   * Ensures switchBack() runs on exception paths.
   */
  public function testExceptionPathRestoresPriorAccount(): void {
    $this->setCurrentUserAccount($this->disallowedUser);

    $assistant = $this->createAssistant('does_not_exist');
    $runner = $this->buildRunner($assistant);
    $runner->setExecutorUid((int) $this->executorUser->id());

    try {
      $runner->process();
      $this->fail('Expected a provider/plugin exception.');
    }
    catch (\Exception $exception) {
      $this->assertStringContainsString('does_not_exist', $exception->getMessage());
    }

    $this->assertSame((string) $this->disallowedUser->id(), (string) \Drupal::currentUser()->id());
  }

  /**
   * Ensures omitted executor keeps legacy ambient-user behavior.
   */
  public function testOmittedExecutorFollowsAmbientBehavior(): void {
    $assistant = $this->createAssistant('echoai');

    $this->setCurrentUserAccount($this->disallowedUser);
    $disallowedRunner = $this->buildRunner($assistant);
    try {
      $disallowedRunner->process();
      $this->fail('Expected ambient user access check failure.');
    }
    catch (\Exception $exception) {
      $this->assertStringContainsString('required role', $exception->getMessage());
    }

    $this->setCurrentUserAccount($this->allowedUser);
    $allowedRunner = $this->buildRunner($assistant);
    $output = $allowedRunner->process();
    $this->assertTrue($output instanceof ChatOutput || is_array($output));
  }

  /**
   * Ensures nested account switch stacks unwind back to the outer account.
   */
  public function testNestedAccountSwitchStackRestoresOuterAccount(): void {
    $this->setCurrentUserAccount($this->allowedUser);
    /** @var \Drupal\Core\Session\AccountSwitcherInterface $accountSwitcher */
    $accountSwitcher = $this->container->get('account_switcher');
    $accountSwitcher->switchTo($this->disallowedUser);

    $assistant = $this->createAssistant('echoai');
    $runner = $this->buildRunner($assistant);
    $runner->setExecutorUid((int) $this->executorUser->id());
    $runner->process();

    $this->assertSame((string) $this->disallowedUser->id(), (string) \Drupal::currentUser()->id());

    $accountSwitcher->switchBack();
    $this->assertSame((string) $this->allowedUser->id(), (string) \Drupal::currentUser()->id());
  }

  /**
   * Ensures the service container wires in the account switcher dependency.
   */
  public function testContainerRunnerServiceSupportsExplicitExecutor(): void {
    $this->setCurrentUserAccount($this->disallowedUser);

    $assistant = $this->createAssistant('does_not_exist');
    $runner = $this->buildContainerRunner($assistant);
    $runner->setExecutorUid((int) $this->executorUser->id());

    try {
      $runner->process();
      $this->fail('Expected the container runner to reach provider resolution.');
    }
    catch (\Exception $exception) {
      $this->assertStringContainsString('does_not_exist', $exception->getMessage());
    }

    $this->assertSame((string) $this->disallowedUser->id(), (string) \Drupal::currentUser()->id());
  }

  /**
   * Creates a test runner with baseline state.
   *
   * @param \Drupal\ai_assistant_api\Entity\AiAssistant $assistant
   *   The assistant to run.
   *
   * @return \Drupal\ai_assistant_api\AiAssistantApiRunner
   *   A prepared runner instance.
   */
  protected function buildRunner(AiAssistant $assistant): AiAssistantApiRunner {
    $runner = new AiAssistantApiRunner(
      $this->container->get('entity_type.manager'),
      $this->container->get('ai.provider'),
      $this->container->get('renderer'),
      $this->container->get('tempstore.private'),
      $this->container->get('ai_assistant_api.action_plugin.manager'),
      $this->container->get('event_dispatcher'),
      $this->container->get('current_user'),
      $this->container->get('logger.factory'),
      $this->container->get('ai.prompt_json_decode'),
      $this->container->get('ai_assistant_api.assistant_message_builder'),
      $this->container->get('session_manager'),
      new Settings(['ai_assistant_custom_prompts' => TRUE]),
      $this->container->get('module_handler'),
      $this->container->get('account_switcher'),
    );
    $runner->setAssistant($assistant);
    $runner->setUserMessage(new UserMessage('Hello execution principal'));
    $runner->setThrowException(TRUE);

    return $runner;
  }

  /**
   * Creates a legacy-style runner without account switcher injection.
   *
   * @param \Drupal\ai_assistant_api\Entity\AiAssistant $assistant
   *   The assistant to run.
   *
   * @return \Drupal\ai_assistant_api\AiAssistantApiRunner
   *   A prepared runner instance.
   */
  protected function buildRunnerWithoutAccountSwitcher(AiAssistant $assistant): AiAssistantApiRunner {
    $runner = new AiAssistantApiRunner(
      $this->container->get('entity_type.manager'),
      $this->container->get('ai.provider'),
      $this->container->get('renderer'),
      $this->container->get('tempstore.private'),
      $this->container->get('ai_assistant_api.action_plugin.manager'),
      $this->container->get('event_dispatcher'),
      $this->container->get('current_user'),
      $this->container->get('logger.factory'),
      $this->container->get('ai.prompt_json_decode'),
      $this->container->get('ai_assistant_api.assistant_message_builder'),
      $this->container->get('session_manager'),
      new Settings(['ai_assistant_custom_prompts' => TRUE]),
      $this->container->get('module_handler'),
    );
    $runner->setAssistant($assistant);
    $runner->setUserMessage(new UserMessage('Hello execution principal'));
    $runner->setThrowException(TRUE);

    return $runner;
  }

  /**
   * Creates a container-instantiated runner with baseline state.
   *
   * @param \Drupal\ai_assistant_api\Entity\AiAssistant $assistant
   *   The assistant to run.
   *
   * @return \Drupal\ai_assistant_api\AiAssistantApiRunner
   *   A prepared runner instance from the service container.
   */
  protected function buildContainerRunner(AiAssistant $assistant): AiAssistantApiRunner {
    /** @var \Drupal\ai_assistant_api\AiAssistantApiRunner $runner */
    $runner = $this->container->get('ai_assistant_api.runner');
    $runner->setAssistant($assistant);
    $runner->setUserMessage(new UserMessage('Hello execution principal'));
    $runner->setThrowException(TRUE);

    return $runner;
  }

  /**
   * Creates a baseline assistant for execution-principal tests.
   *
   * @param string $providerId
   *   The provider plugin ID.
   * @param string $allowHistory
   *   The assistant history mode.
   *
   * @return \Drupal\ai_assistant_api\Entity\AiAssistant
   *   A saved assistant entity.
   */
  protected function createAssistant(string $providerId, string $allowHistory = 'none'): AiAssistant {
    $this->assistantCounter++;
    $assistant = AiAssistant::create([
      'id' => 'execution_principal_test_' . $this->assistantCounter,
      'label' => 'Execution Principal Test ' . $this->assistantCounter,
      'description' => 'Execution principal runner test assistant.',
      'allow_history' => $allowHistory,
      'history_context_length' => '2',
      'pre_action_prompt' => '',
      'system_prompt' => '',
      'instructions' => '',
      'actions_enabled' => [],
      'error_message' => 'Runner failed.',
      'specific_error_messages' => [],
      'llm_provider' => $providerId,
      'llm_model' => 'gpt-test',
      'llm_configuration' => [],
      'roles' => ['assistant_runner' => TRUE],
      'use_function_calling' => FALSE,
      'ai_agent' => NULL,
    ]);
    $assistant->save();

    return $assistant;
  }

  /**
   * Creates a test user with optional roles.
   *
   * @param string[] $roles
   *   Role IDs to assign.
   * @param bool $active
   *   Whether the account is active.
   *
   * @return \Drupal\user\UserInterface
   *   The saved user entity.
   */
  protected function createUserWithRoles(array $roles = [], bool $active = TRUE): UserInterface {
    $user = User::create([
      'name' => $this->randomMachineName(),
      'mail' => $this->randomMachineName() . '@example.com',
      'status' => $active ? 1 : 0,
    ]);
    foreach ($roles as $role) {
      $user->addRole($role);
    }
    $user->save();

    return $user;
  }

  /**
   * Sets the ambient current user for the test runtime.
   *
   * @param \Drupal\user\UserInterface $account
   *   The account to set.
   */
  protected function setCurrentUserAccount(UserInterface $account): void {
    /** @var \Drupal\Core\Session\AccountProxyInterface $currentUser */
    $currentUser = $this->container->get('current_user');
    $currentUser->setAccount($account);
  }

}
