import * as path from 'path';
import { test, expect } from '@fixtures/rstudio.fixture';
import { requireAiCredentials } from '@utils/ai-credentials';
import { CHAT_PROVIDERS } from '@utils/constants';
import { ConsolePaneActions } from '@actions/console_pane.actions';
import { AssistantOptionsActions } from '@actions/assistant_options.actions';
import { ChatPaneActions } from '@actions/chat_pane.actions';
import { ChatPane } from '@pages/chat_pane.page';
import type { EnvironmentVersions } from '@pages/console_pane.page';
import { useSuiteSandbox } from '@utils/sandbox';
import { closeProjectIfOpen, createAndOpenProject } from '@utils/project';
import { setPref } from '@utils/commands';
import { createChatActions, annotateVersions } from './_chat-setup';

// ---------------------------------------------------------------------------
// Project-level skill (lives in <project>/.positai/skills/ under a sandbox
// project. Absolute paths are computed in beforeAll once the sandbox dir
// is known.)
// ---------------------------------------------------------------------------

const PROJECT_NAME = 'user_skills_test_project';
const PROJECT_SKILL_NAME = 'custom-data-summary';
const PROJECT_MARKER = 'PROJECT_SKILL_ACTIVE_QA7742';

// ---------------------------------------------------------------------------
// User-level skill (lives in ~/.posit/assistant/skills/ under the user home
// dir; the legacy ~/.positai/ location is only consulted when the new
// directory does not exist, and the sandbox seeds ~/.posit/assistant with
// the host's credentials).
//
// The Playwright sandbox redirects HOME / USERPROFILE for the rstudio child
// process (and therefore Databot, which inherits its env) to
// $PW_SANDBOX/user-home/. We compute the absolute path from the sandbox so
// the file lands where Databot looks, regardless of what the runner's own
// os.homedir() returns.
// ---------------------------------------------------------------------------

const USER_SKILL_NAME = 'custom-code-review';
const USER_MARKER = 'USER_SKILL_REVIEW_QA8853';

// Resolve lazily so the spec can be loaded for --list / type-checks without
// PW_SANDBOX being set; the assertion fires at test-run time instead.
function userHome(): string {
  const s = process.env.PW_SANDBOX;
  if (!s) {
    throw new Error(
      'PW_SANDBOX is not set; fixtures/sandbox-setup.ts should populate it before any spec is loaded',
    );
  }
  return path.join(s, 'user-home').replace(/\\/g, '/');
}
const userSkillDir = () => `${userHome()}/.posit/assistant/skills/${USER_SKILL_NAME}`;
const userSkillPath = () => `${userSkillDir()}/SKILL.md`;

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/**
 * Create a SKILL.md file via two short R console commands.
 *
 * Long single commands through pressSequentially are fragile (autocomplete
 * interference, timing issues). Instead we use two short commands:
 * 1. dir.create() for the directory
 * 2. writeLines(c(...)) for the file content (kept minimal)
 */
async function createSkillFile(
  consoleActions: ConsolePaneActions,
  dir: string,
  filePath: string,
  name: string,
  description: string,
  marker: string,
): Promise<void> {
  await consoleActions.executeInConsole(`dir.create("${dir}", recursive = TRUE)`, { wait: true });

  // Keep content minimal to stay under ~300 chars for pressSequentially reliability
  const cmd =
    `writeLines(c("---", "name: ${name}", "description: ${description}", "---", "", "Start with: ${marker}", "Markers are MANDATORY."), "${filePath}")`;
  await consoleActions.executeInConsole(cmd, { wait: true });
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

test.describe.serial('User-Added Skills', { tag: ['@ai', '@serial'] }, () => {
  requireAiCredentials(test, 'positai');

  const sandbox = useSuiteSandbox();
  let chatPane: ChatPane;
  let chatActions: ChatPaneActions;
  let consoleActions: ConsolePaneActions;
  let assistantActions: AssistantOptionsActions;
  let versions: EnvironmentVersions;
  let projectSkillDir = '';
  let projectSkillPath = '';

  test.beforeAll(async ({ rstudioPage: page }) => {
    ({ consoleActions, assistantActions, chatActions, chatPane } = createChatActions(page));

    await consoleActions.clearConsole();

    // -----------------------------------------------------------------------
    // Step 0: Create a real project inside the sandbox so the
    // "project-level" skill actually lands in a project directory rather
    // than in the user's home dir.
    // -----------------------------------------------------------------------
    const sandboxR = sandbox.dir.replace(/\\/g, '/');
    const projectDir = await createAndOpenProject(page, sandboxR, PROJECT_NAME);
    projectSkillDir = `${projectDir}/.positai/skills/${PROJECT_SKILL_NAME}`;
    projectSkillPath = `${projectSkillDir}/SKILL.md`;

    // Re-create actions after session restart
    ({ consoleActions, assistantActions, chatActions, chatPane } = createChatActions(page));
    await consoleActions.clearConsole();
    versions = await consoleActions.getEnvironmentVersions();

    // -----------------------------------------------------------------------
    // Step 1: Stop any running backend FIRST.
    //
    // The default chat_provider is "posit", so the backend auto-starts when
    // RStudio launches. We must stop it before creating skill files, because
    // skills are only discovered during DatabotCore.initialize() (at process
    // start). Setting the preference to "none" (a valid enum value -- NOT "")
    // triggers onChatProviderChanged() → stopBackend() on the GWT side.
    // -----------------------------------------------------------------------
    await setPref(page, 'chat_provider', 'none');
    // Deliberate wait: setPref triggers stopBackend() asynchronously on the
    // GWT side. There's no bridge-exposed signal for "backend has fully shut
    // down", so we settle for an observation window long enough for the
    // child process to terminate before we recreate skill files below.
    await page.waitForTimeout(5000);

    // -----------------------------------------------------------------------
    // Step 2: Create a project-level skill (.positai/skills/ in workspace)
    // -----------------------------------------------------------------------
    await createSkillFile(
      consoleActions,
      projectSkillDir,
      projectSkillPath,
      PROJECT_SKILL_NAME,
      'Summarize or describe a dataset.',
      PROJECT_MARKER,
    );

    // -----------------------------------------------------------------------
    // Step 3: Create a user-level skill (~/.positai/skills/ in home dir)
    // -----------------------------------------------------------------------
    await createSkillFile(
      consoleActions,
      userSkillDir(),
      userSkillPath(),
      USER_SKILL_NAME,
      'Review or critique a code snippet.',
      USER_MARKER,
    );

    // -----------------------------------------------------------------------
    // Step 4: Verify both files exist and have correct content
    // -----------------------------------------------------------------------
    await consoleActions.clearConsole();
    await consoleActions.executeInConsole(
      `cat(readLines("${projectSkillPath}"), sep = "\\n")`,
      { wait: true },
    );
    const projectOutput = await consoleActions.consolePane.consoleOutput.innerText();

    await consoleActions.clearConsole();
    await consoleActions.executeInConsole(
      `cat(readLines("${userSkillPath()}"), sep = "\\n")`,
      { wait: true },
    );
    const userOutput = await consoleActions.consolePane.consoleOutput.innerText();

    // -----------------------------------------------------------------------
    // Step 5: Start a fresh backend by setting chat_provider back to "posit".
    //
    // This triggers onChatProviderChanged() → checkForUpdates() → startBackend()
    // The new backend process runs DatabotCore.initialize() → discoverSkills()
    // which picks up our newly created skill files.
    // -----------------------------------------------------------------------
    await assistantActions.setChatProvider(CHAT_PROVIDERS['posit-assistant']);

    // -----------------------------------------------------------------------
    // Step 6: Open the chat pane and wait for the UI to be ready
    // -----------------------------------------------------------------------
    await chatActions.openChatPane();
    await chatActions.dismissSetupPrompts();
  });

  test.beforeEach(async () => {
    annotateVersions(versions);
  });

  test.afterAll(async ({ rstudioPage: page }) => {
    // The project-level skill lives inside the sandbox project; the sandbox
    // afterAll (registered by useSuiteSandbox) removes the entire sandbox
    // tree, so no explicit cleanup is needed for it here.

    // Clean up only the specific user-level skill we created (leave the rest
    // of ~/.posit/assistant/ intact)
    await consoleActions.executeInConsole(
      `unlink("${userSkillDir()}", recursive = TRUE)`,
      { wait: true },
    );

    // Close the project we opened in beforeAll so subsequent test files start
    // from a no-project workbench (per the convention in utils/test-reset.ts).
    await closeProjectIfOpen(page);
  });

  test('both custom skills are discovered by assistant', async () => {
    await chatActions.startNewConversation();

    const initialCount = await chatPane.getMessageCount();
    await chatActions.sendChatMessage(
      'List all Agent Skills that are currently available to you. ' +
      'Include the exact name of each skill.'
    );
    const newCount = await chatActions.waitForResponse(initialCount);
    expect(newCount).toBeGreaterThan(initialCount);

    // The response should mention both custom skills by name
    const lastMessage = chatPane.messageItem.last();
    await expect(lastMessage).toContainText(PROJECT_SKILL_NAME, { timeout: 10000 });
    await expect(lastMessage).toContainText(USER_SKILL_NAME, { timeout: 10000 });
  });

  test('project-level skill markers appear in response', async () => {
    await chatActions.startNewConversation();

    const initialCount = await chatPane.getMessageCount();
    await chatActions.sendChatMessage(
      'Summarize the iris dataset. ' +
      'Use your available Agent Skills. ' +
      'Do not run any code.'
    );

    // Wait for the full response, handling any Allow dialogs that appear
    await chatActions.pollWithAllowDialogs(async () => {
      const isStillProcessing = await chatPane.isStopButtonVisible();
      const messageCount = await chatPane.getMessageCount();
      return !isStillProcessing && messageCount > initialCount;
    }, 120000);

    // Verify the response contains the markers from our project skill
    const lastMessage = chatPane.messageItem.last();
    const responseText = await lastMessage.innerText();

    expect(responseText).toContain(PROJECT_MARKER);
  });

  test('user-level skill is selected when its description matches the prompt', async () => {
    await chatActions.startNewConversation();

    const initialCount = await chatPane.getMessageCount();
    await chatActions.sendChatMessage(
      'Review this R code snippet and provide feedback: x <- 1:10; mean(x). ' +
      'Use your available Agent Skills. ' +
      'Do not run any code.'
    );

    // Wait for the full response, handling any Allow dialogs that appear
    await chatActions.pollWithAllowDialogs(async () => {
      const isStillProcessing = await chatPane.isStopButtonVisible();
      const messageCount = await chatPane.getMessageCount();
      return !isStillProcessing && messageCount > initialCount;
    }, 120000);

    // Databot emits `skill: <name>` in the message when the model selects a
    // registered skill. That happens iff the skill was discovered at backend
    // init -- which iff the SKILL.md was read from
    // $HOME/.posit/assistant/skills/, which here is sandbox-redirected to
    // $PW_SANDBOX/user-home/.posit/assistant/skills/.
    // We avoid asserting on the marker emitted inside the SKILL.md body because
    // LLMs are unreliable at reproducing long opaque tokens verbatim.
    const lastMessage = chatPane.messageItem.last();
    const responseText = await lastMessage.innerText();

    expect(responseText).toMatch(new RegExp(`skill:\\s*${USER_SKILL_NAME}`));
  });
});
