/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

do_get_profile();

const {
  NewTabStarterGenerator,
  trimConversation,
  addMemoriesToPrompt,
  cleanInferenceOutput,
  generateConversationStartersSidebar,
  generateFollowupPrompts,
  MemoriesGetterForSuggestionPrompts,
} = ChromeUtils.importESModule(
  "moz-src:///browser/components/aiwindow/models/ConversationSuggestions.sys.mjs"
);

const { sanitizeUntrustedContent } = ChromeUtils.importESModule(
  "moz-src:///browser/components/aiwindow/models/ChatUtils.sys.mjs"
);

const { openAIEngine } = ChromeUtils.importESModule(
  "moz-src:///browser/components/aiwindow/models/Utils.sys.mjs"
);
const { MemoriesManager } = ChromeUtils.importESModule(
  "moz-src:///browser/components/aiwindow/models/memories/MemoriesManager.sys.mjs"
);
const { MESSAGE_ROLE } = ChromeUtils.importESModule(
  "moz-src:///browser/components/aiwindow/ui/modules/AIWindowConstants.sys.mjs"
);
const { sinon } = ChromeUtils.importESModule(
  "resource://testing-common/Sinon.sys.mjs"
);

/**
 * Constants for preference keys and test values
 */
const PREF_API_KEY = "browser.smartwindow.apiKey";
const PREF_ENDPOINT = "browser.smartwindow.endpoint";
const PREF_MODEL = "browser.smartwindow.model";
const PREF_HISTORY_ENABLED = "places.history.enabled";
const PREF_PRIVATE_BROWSING = "browser.privatebrowsing.autostart";

const API_KEY = "test-api-key";
const ENDPOINT = "https://api.test-endpoint.com/v1";
const MODEL = "test-model";

async function loadRemoteSettingsSnapshot() {
  const file = do_get_file("ai-window-prompts-remote-settings-snapshot.json");
  const data = await IOUtils.readUTF8(file.path);
  return JSON.parse(data);
}

let REAL_REMOTE_SETTINGS_SNAPSHOT;

add_setup(async function () {
  REAL_REMOTE_SETTINGS_SNAPSHOT = await loadRemoteSettingsSnapshot();
});

/**
 * Cleans up preferences after testing
 */
registerCleanupFunction(() => {
  for (let pref of [
    PREF_API_KEY,
    PREF_ENDPOINT,
    PREF_MODEL,
    PREF_HISTORY_ENABLED,
    PREF_PRIVATE_BROWSING,
  ]) {
    if (Services.prefs.prefHasUserValue(pref)) {
      Services.prefs.clearUserPref(pref);
    }
  }
});

/**
 * Tests for trimConversation function
 */
add_task(async function test_trimConversation() {
  const cases = [
    {
      input: [
        // empty case
      ],
      expected: [],
    },
    {
      input: [
        // more than 15 messages
        ...Array.from({ length: 20 }, (_, i) => ({
          role: i % 2 === 0 ? MESSAGE_ROLE.USER : MESSAGE_ROLE.ASSISTANT,
          content: `Message ${i + 1}`,
        })),
      ],
      expected: [
        ...Array.from({ length: 15 }, (_, i) => ({
          role: (i + 5) % 2 === 0 ? "user" : "assistant",
          content: `Message ${i + 6}`,
        })),
      ],
    },
    {
      input: [
        // should remove tool call/responses
        { role: MESSAGE_ROLE.USER, content: "What's the weather like?" },
        {
          role: MESSAGE_ROLE.ASSISTANT,
          content: "",
          tool_call: { name: "get_weather", arguments: "{}" },
        },
        {
          role: MESSAGE_ROLE.TOOL,
          content: "Here are the latest news articles.",
        },
      ],
      expected: [{ role: "user", content: "What's the weather like?" }],
    },
    {
      input: [
        // should remove system message
        { role: MESSAGE_ROLE.SYSTEM, content: "System message" },
        { role: MESSAGE_ROLE.USER, content: "Hello" },
        { role: MESSAGE_ROLE.ASSISTANT, content: "Hi there!" },
      ],
      expected: [
        { role: "user", content: "Hello" },
        { role: "assistant", content: "Hi there!" },
      ],
    },
    {
      input: [
        // should remove messages with empty content
        { role: MESSAGE_ROLE.USER, content: "\n" },
        { role: MESSAGE_ROLE.ASSISTANT, content: "   " },
      ],
      expected: [],
    },
    {
      input: [
        // no valid messages
        { role: MESSAGE_ROLE.SYSTEM, content: "System message" },
        {
          role: MESSAGE_ROLE.ASSISTANT,
          content: "",
          tool_call: { name: "get_info", arguments: "{}" },
        },
      ],
      expected: [],
    },
    {
      input: [
        // should slice after filtering invalid messages
        ...Array(10)
          .fill(0)
          .flatMap((_, i) => [
            {
              role: MESSAGE_ROLE.USER,
              content: `User message ${i + 1}`,
            },
            { role: MESSAGE_ROLE.SYSTEM, content: "System message" },
          ]),
      ],
      expected: [
        ...Array.from({ length: 10 }, (_, i) => ({
          role: "user",
          content: `User message ${i + 1}`,
        })),
      ],
    },
  ];

  for (const { input, expected } of cases) {
    const result = trimConversation(input);
    Assert.deepEqual(
      result,
      expected,
      "trimConversation should return only user/assistant messages with content"
    );
  }
});

/**
 * Test for addMemoriesToPrompt function when there are memories
 */
add_task(async function test_addMemoriesToPrompt_have_memories() {
  const sb = sinon.createSandbox();
  try {
    const basePrompt = "Base prompt content.";
    const fakeMemories = ["Memory summary 1", "Memory summary 2"];
    const memoriesStub = sb
      .stub(MemoriesGetterForSuggestionPrompts, "getMemorySummariesForPrompt")
      .resolves(fakeMemories);
    const conversationMemoriesPrompt = "Memories block:\n{memories}";
    const promptWithMemories = await addMemoriesToPrompt(
      basePrompt,
      conversationMemoriesPrompt
    );

    Assert.ok(
      memoriesStub.calledOnce,
      "getMemorySummariesForPrompt should be called"
    );
    Assert.ok(
      promptWithMemories.includes("- Memory summary 1") &&
        promptWithMemories.includes("- Memory summary 2"),
      "Prompt should include memories"
    );
  } finally {
    sb.restore();
  }
});

/**
 * Test for addMemoriesToPrompt function when there are no memories
 */
add_task(async function test_addMemoriesToPrompt_dont_have_memories() {
  const sb = sinon.createSandbox();
  try {
    const basePrompt = "Base prompt content.";
    const fakeMemories = [];
    const memoriesStub = sb
      .stub(MemoriesGetterForSuggestionPrompts, "getMemorySummariesForPrompt")
      .resolves(fakeMemories);
    const conversationMemoriesPrompt = "Memories block:\n{memories}";
    const promptWithMemories = await addMemoriesToPrompt(
      basePrompt,
      conversationMemoriesPrompt
    );

    Assert.ok(
      memoriesStub.calledOnce,
      "getMemorySummariesForPrompt should be called"
    );
    Assert.equal(
      promptWithMemories,
      basePrompt,
      "Prompt should be unchanged when no memories"
    );
  } finally {
    sb.restore();
  }
});

/**
 * Tests for cleanInferenceOutput function
 */
add_task(function test_cleanInferenceOutput() {
  const sb = sinon.createSandbox();
  try {
    const cases = [
      {
        input: {
          finalOutput: `- Suggestion 1\n\n* Suggestion 2\n1. Suggestion 3.\n2)Suggestion 4\n[5] Suggestion 5\n6.\nLabel: Suggestion 6\nSuggestion 7.\nSuggestion 8?`,
        },
        expected: [
          "Suggestion 1",
          "Suggestion 2",
          "Suggestion 3",
          "Suggestion 4",
          "Suggestion 5",
          "Suggestion 6",
          "Suggestion 7",
          "Suggestion 8?",
        ],
      },
      {
        input: { finalOutput: `Suggestion X\nSuggestion Y\nSuggestion Z` },
        expected: ["Suggestion X", "Suggestion Y", "Suggestion Z"],
      },
      {
        input: { finalOutput: "" },
        expected: [],
      },
    ];

    for (const { input, expected } of cases) {
      const result = cleanInferenceOutput(input);
      Assert.deepEqual(
        result,
        expected,
        "cleanInferenceOutput should return expected output"
      );
    }
  } finally {
    sb.restore();
  }
});

/**
 * Tests for createNewTabPromptGenerator generating prompts based on tab count
 */
add_task(
  async function test_createNewTabPromptGenerator_with_history_enabled() {
    const sb = sinon.createSandbox();
    const writingPrompts = [
      "Write a first draft",
      "Improve writing",
      "Proofread a message",
    ];

    const planningPrompts = [
      "Simplify a topic",
      "Brainstorm ideas",
      "Help make a plan",
    ];
    try {
      const cases = [
        {
          input: { tabCount: -1 },
          expectedBrowsing: [],
        },
        {
          input: { tabCount: 0 },
          expectedBrowsing: ["Find tabs in history"],
        },
        {
          input: { tabCount: 1 },
          expectedBrowsing: ["Find tabs in history", "Summarize tabs"],
        },
        {
          input: { tabCount: 2 },
          expectedBrowsing: [
            "Find tabs in history",
            "Summarize tabs",
            "Compare tabs",
          ],
        },
        {
          input: { tabCount: 3 },
          expectedBrowsing: [
            "Find tabs in history",
            "Summarize tabs",
            "Compare tabs",
          ],
        },
      ];
      const promptGenerator = NewTabStarterGenerator;
      for (const { input, expectedBrowsing } of cases) {
        const results = await promptGenerator.getPrompts(input.tabCount);
        if (input.tabCount <= -1) {
          Assert.equal(results.length, 2, "Should return 2 suggestions");
        } else {
          Assert.equal(results.length, 3, "Should return 3 suggestions");
        }
        for (const result of results) {
          Assert.equal(
            result.type,
            "chat",
            "Each result should have type 'chat'"
          );
        }
        Assert.ok(
          writingPrompts.includes(results[0].text),
          "Results should include a valid writing prompt"
        );
        Assert.ok(
          planningPrompts.includes(results[1].text),
          "Results should include a valid planning prompt"
        );
        if (results[2]) {
          Assert.ok(
            expectedBrowsing.includes(results[2].text),
            "Results should include a valid browsing prompt"
          );
        }
      }
    } finally {
      sb.restore();
    }
  }
);

/**
 * Tests for createNewTabPromptGenerator generating prompts based on tab count, with history disabled
 */
add_task(
  async function test_createNewTabPromptGenerator_with_history_disabled() {
    const sb = sinon.createSandbox();
    const writingPrompts = [
      "Write a first draft",
      "Improve writing",
      "Proofread a message",
    ];

    const planningPrompts = [
      "Simplify a topic",
      "Brainstorm ideas",
      "Help make a plan",
    ];
    try {
      const cases = [
        {
          input: { tabCount: -1 },
          expectedBrowsing: [],
        },
        {
          input: { tabCount: 0 },
          expectedBrowsing: [],
        },
        {
          input: { tabCount: 1 },
          expectedBrowsing: ["Summarize tabs"],
        },
        {
          input: { tabCount: 2 },
          expectedBrowsing: ["Summarize tabs", "Compare tabs"],
        },
        {
          input: { tabCount: 3 },
          expectedBrowsing: ["Summarize tabs", "Compare tabs"],
        },
      ];
      for (const pref of [
        [{ key: PREF_HISTORY_ENABLED, value: false }],
        [{ key: PREF_PRIVATE_BROWSING, value: true }],
        [
          { key: PREF_HISTORY_ENABLED, value: false },
          { key: PREF_PRIVATE_BROWSING, value: true },
        ],
      ]) {
        for (const p of pref) {
          Services.prefs.setBoolPref(p.key, p.value);
        }
        const promptGenerator = NewTabStarterGenerator;
        for (const { input, expectedBrowsing } of cases) {
          const results = await promptGenerator.getPrompts(input.tabCount);
          if (input.tabCount <= 0) {
            Assert.equal(results.length, 2, "Should return 2 suggestions");
          } else {
            Assert.equal(results.length, 3, "Should return 3 suggestions");
          }
          for (const result of results) {
            Assert.equal(
              result.type,
              "chat",
              "Each result should have type 'chat'"
            );
          }
          Assert.ok(
            writingPrompts.includes(results[0].text),
            "Results should include a valid writing prompt"
          );
          Assert.ok(
            planningPrompts.includes(results[1].text),
            "Results should include a valid planning prompt"
          );
          if (results[2]) {
            Assert.ok(
              expectedBrowsing.includes(results[2].text),
              "Results should include a valid browsing prompt"
            );
          }
        }
        for (const p of pref) {
          Services.prefs.clearUserPref(p.key);
        }
      }
    } finally {
      sb.restore();
    }
  }
);

/**
 * Tests for generateConversationStartersSidebar successfully generating suggestions
 */
add_task(async function test_generateConversationStartersSidebar_happy_path() {
  Services.prefs.setStringPref(PREF_API_KEY, API_KEY);
  Services.prefs.setStringPref(PREF_ENDPOINT, ENDPOINT);
  Services.prefs.setStringPref(PREF_MODEL, MODEL);

  const sb = sinon.createSandbox();
  try {
    // Mock the openAIEngine and memories response
    const fakeEngine = {
      run: sb.stub().resolves({
        finalOutput: `1. Suggestion 1\n\n- Suggestion 2\nLabel: Suggestion 3.\nSuggestion 4\nSuggestion 5\nSuggestion 6`,
      }),
    };
    sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine);

    const fakeMemories = ["Memory summary 1", "Memory summary 2"];
    const memoriesStub = sb
      .stub(MemoriesGetterForSuggestionPrompts, "getMemorySummariesForPrompt")
      .resolves(fakeMemories);

    const n = 3;
    const contextTabs = [
      { title: "Current Tab", url: "https://current.example.com" },
      { title: "Tab 2", url: "https://tab2.example.com" },
    ];

    const result = await generateConversationStartersSidebar(
      contextTabs,
      n,
      true
    );
    Assert.ok(fakeEngine.run.calledOnce, "Engine run should be called once");
    Assert.ok(
      memoriesStub.calledOnce,
      "getMemorySummariesForPrompt should be called once"
    );

    // Verify the prompt content
    const callArgs = fakeEngine.run.firstCall.args[0];
    Assert.equal(
      callArgs.args.length,
      2,
      "run should be called with 2 messages"
    );

    Assert.ok(
      callArgs.args[1].content.includes(
        JSON.stringify({
          title: sanitizeUntrustedContent("Current Tab"),
          url: "https://current.example.com",
        })
      ),
      "Prompt should include current tab info"
    );
    Assert.ok(
      callArgs.args[1].content.includes(
        JSON.stringify({
          title: sanitizeUntrustedContent("Tab 2"),
          url: "https://tab2.example.com",
        })
      ),
      "Prompt should include other tab info"
    );
    Assert.ok(
      callArgs.args[1].content.includes(
        "\n- Memory summary 1\n- Memory summary 2"
      ),
      "Prompt should include memory summaries"
    );

    Assert.deepEqual(
      result,
      [
        { text: "Suggestion 1", type: "chat" },
        { text: "Suggestion 2", type: "chat" },
        { text: "Suggestion 3", type: "chat" },
      ],
      "Suggestions should match expected values"
    );
  } finally {
    sb.restore();
  }
});

/**
 * Tests for generateConversationStartersSidebar without including memories
 */
add_task(
  async function test_generateConversationStartersSidebar_without_memories() {
    Services.prefs.setStringPref(PREF_API_KEY, API_KEY);
    Services.prefs.setStringPref(PREF_ENDPOINT, ENDPOINT);
    Services.prefs.setStringPref(PREF_MODEL, MODEL);

    const sb = sinon.createSandbox();
    try {
      // Mock the openAIEngine and memories response
      const fakeEngine = {
        run: sb.stub().resolves({
          finalOutput: `1. Suggestion 1\n\n- Suggestion 2\nLabel: Suggestion 3.\nSuggestion 4\nSuggestion 5\nSuggestion 6`,
        }),
      };
      sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine);

      const fakeMemories = ["Memory summary 1", "Memory summary 2"];
      const memoriesStub = sb
        .stub(MemoriesGetterForSuggestionPrompts, "getMemorySummariesForPrompt")
        .resolves(fakeMemories);

      const n = 3;
      const contextTabs = [
        { title: "Current Tab", url: "https://current.example.com" },
        { title: "Tab 2", url: "https://tab2.example.com" },
      ];

      const result = await generateConversationStartersSidebar(
        contextTabs,
        n,
        false
      );
      Assert.ok(fakeEngine.run.calledOnce, "Engine run should be called once");
      Assert.ok(
        !memoriesStub.calledOnce,
        "getMemorySummariesForPrompt shouldn't be called"
      );

      // Verify the prompt content
      const callArgs = fakeEngine.run.firstCall.args[0];
      Assert.equal(
        callArgs.args.length,
        2,
        "run should be called with 2 messages"
      );
      Assert.ok(
        callArgs.args[1].content.includes(
          JSON.stringify({
            title: sanitizeUntrustedContent("Current Tab"),
            url: "https://current.example.com",
          })
        ),
        "Prompt should include current tab info"
      );
      Assert.ok(
        callArgs.args[1].content.includes(
          JSON.stringify({
            title: sanitizeUntrustedContent("Tab 2"),
            url: "https://tab2.example.com",
          })
        ),
        "Prompt should include other tab info"
      );
      Assert.ok(
        !callArgs.args[1].content.includes(
          "\n- Memory summary 1\n- Memory summary 2"
        ),
        "Prompt should not include memory summaries"
      );

      Assert.deepEqual(
        result,
        [
          { text: "Suggestion 1", type: "chat" },
          { text: "Suggestion 2", type: "chat" },
          { text: "Suggestion 3", type: "chat" },
        ],
        "Suggestions should match expected values"
      );
    } finally {
      sb.restore();
    }
  }
);

/**
 * Tests for generateConversationStartersSidebar when no memories are returned
 */
add_task(
  async function test_generateConversationStartersSidebar_no_memories_returned() {
    Services.prefs.setStringPref(PREF_API_KEY, API_KEY);
    Services.prefs.setStringPref(PREF_ENDPOINT, ENDPOINT);
    Services.prefs.setStringPref(PREF_MODEL, MODEL);

    const sb = sinon.createSandbox();
    try {
      // Mock the openAIEngine and memories response
      const fakeEngine = {
        run: sb.stub().resolves({
          finalOutput: `1. Suggestion 1\n\n- Suggestion 2\nLabel: Suggestion 3.\nSuggestion 4\nSuggestion 5\nSuggestion 6`,
        }),
      };
      sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine);

      const fakeMemories = [];
      const memoriesStub = sb
        .stub(MemoriesGetterForSuggestionPrompts, "getMemorySummariesForPrompt")
        .resolves(fakeMemories);

      const n = 3;
      const contextTabs = [
        { title: "Current Tab", url: "https://current.example.com" },
        { title: "Tab 2", url: "https://tab2.example.com" },
      ];

      const result = await generateConversationStartersSidebar(
        contextTabs,
        n,
        true
      );
      Assert.ok(fakeEngine.run.calledOnce, "Engine run should be called once");
      Assert.ok(
        memoriesStub.calledOnce,
        "getMemorySummariesForPrompt should be called once"
      );

      // Verify the prompt content
      const callArgs = fakeEngine.run.firstCall.args[0];
      Assert.equal(
        callArgs.args.length,
        2,
        "run should be called with 2 messages"
      );
      Assert.ok(
        callArgs.args[1].content.includes(
          JSON.stringify({
            title: sanitizeUntrustedContent("Current Tab"),
            url: "https://current.example.com",
          })
        ),
        "Prompt should include current tab info"
      );
      Assert.ok(
        callArgs.args[1].content.includes(
          JSON.stringify({
            title: sanitizeUntrustedContent("Tab 2"),
            url: "https://tab2.example.com",
          })
        ),
        "Prompt should include other tab info"
      );
      Assert.ok(
        !callArgs.args[1].content.includes("\nUser Memories:\n"),
        "Prompt shouldn't include user memories block"
      );

      Assert.deepEqual(
        result,
        [
          { text: "Suggestion 1", type: "chat" },
          { text: "Suggestion 2", type: "chat" },
          { text: "Suggestion 3", type: "chat" },
        ],
        "Suggestions should match expected values"
      );
    } finally {
      sb.restore();
    }
  }
);

/**
 * Tests for generateConversationStartersSidebar when no tabs are provided
 */
add_task(async function test_generateConversationStartersSidebar_no_tabs() {
  Services.prefs.setStringPref(PREF_API_KEY, API_KEY);
  Services.prefs.setStringPref(PREF_ENDPOINT, ENDPOINT);
  Services.prefs.setStringPref(PREF_MODEL, MODEL);

  const sb = sinon.createSandbox();
  try {
    // Mock the openAIEngine and memories response
    const fakeEngine = {
      run: sb.stub().resolves({
        finalOutput: `1. Suggestion 1\n\n- Suggestion 2\nLabel: Suggestion 3.\nSuggestion 4\nSuggestion 5\nSuggestion 6`,
      }),
    };
    sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine);

    const fakeMemories = ["Memory summary 1", "Memory summary 2"];
    const memoriesStub = sb
      .stub(MemoriesGetterForSuggestionPrompts, "getMemorySummariesForPrompt")
      .resolves(fakeMemories);

    const n = 3;
    const contextTabs = [];

    const result = await generateConversationStartersSidebar(
      contextTabs,
      n,
      true
    );
    Assert.ok(fakeEngine.run.calledOnce, "Engine run should be called once");
    Assert.ok(
      memoriesStub.calledOnce,
      "getMemorySummariesForPrompt should be called once"
    );

    // Verify the prompt content
    const callArgs = fakeEngine.run.firstCall.args[0];
    Assert.equal(
      callArgs.args.length,
      2,
      "run should be called with 2 messages"
    );
    Assert.ok(
      callArgs.args[1].content.includes("\nNo current tab\n"),
      "Prompt should indicate no current tab"
    );
    Assert.ok(
      callArgs.args[1].content.includes("\nNo tabs available\n"),
      "Prompt should indicate no tabs available"
    );
    Assert.ok(
      callArgs.args[1].content.includes(
        "\n- Memory summary 1\n- Memory summary 2"
      ),
      "Prompt should include memory summaries"
    );

    Assert.deepEqual(
      result,
      [
        { text: "Suggestion 1", type: "chat" },
        { text: "Suggestion 2", type: "chat" },
        { text: "Suggestion 3", type: "chat" },
      ],
      "Suggestions should match expected values"
    );
  } finally {
    sb.restore();
  }
});

/**
 * Tests for generateConversationStartersSidebar with one tab provided
 */
add_task(async function test_generateConversationStartersSidebar_one_tab() {
  Services.prefs.setStringPref(PREF_API_KEY, API_KEY);
  Services.prefs.setStringPref(PREF_ENDPOINT, ENDPOINT);
  Services.prefs.setStringPref(PREF_MODEL, MODEL);

  const sb = sinon.createSandbox();
  try {
    // Mock the openAIEngine and memories response
    const fakeEngine = {
      run: sb.stub().resolves({
        finalOutput: `1. Suggestion 1\n\n- Suggestion 2\nLabel: Suggestion 3.\nSuggestion 4\nSuggestion 5\nSuggestion 6`,
      }),
    };
    sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine);

    const fakeMemories = ["Memory summary 1", "Memory summary 2"];
    const memoriesStub = sb
      .stub(MemoriesGetterForSuggestionPrompts, "getMemorySummariesForPrompt")
      .resolves(fakeMemories);

    const n = 3;
    const contextTabs = [
      { title: "Only Tab", url: "https://only.example.com" },
    ];

    const result = await generateConversationStartersSidebar(
      contextTabs,
      n,
      true
    );
    Assert.ok(fakeEngine.run.calledOnce, "Engine run should be called once");
    Assert.ok(
      memoriesStub.calledOnce,
      "getMemorySummariesForPrompt should be called once"
    );

    // Verify the prompt content
    const callArgs = fakeEngine.run.firstCall.args[0];
    Assert.equal(
      callArgs.args.length,
      2,
      "run should be called with 2 messages"
    );
    Assert.ok(
      callArgs.args[1].content.includes(
        JSON.stringify({
          title: sanitizeUntrustedContent("Only Tab"),
          url: "https://only.example.com",
        })
      ),
      "Prompt should include current tab info"
    );
    Assert.ok(
      callArgs.args[1].content.includes("\nOnly current tab is open\n"),
      "Prompt should indicate only current tab is open"
    );
    Assert.ok(
      callArgs.args[1].content.includes(
        "\n- Memory summary 1\n- Memory summary 2"
      ),
      "Prompt should include memory summaries"
    );

    Assert.deepEqual(
      result,
      [
        { text: "Suggestion 1", type: "chat" },
        { text: "Suggestion 2", type: "chat" },
        { text: "Suggestion 3", type: "chat" },
      ],
      "Suggestions should match expected values"
    );
  } finally {
    sb.restore();
  }
});

/**
 * Tests that generateConversationStartersSidebar handles engine errors gracefully
 */
add_task(
  async function test_generateConversationStartersSidebar_engine_error() {
    Services.prefs.setStringPref(PREF_API_KEY, API_KEY);
    Services.prefs.setStringPref(PREF_ENDPOINT, ENDPOINT);
    Services.prefs.setStringPref(PREF_MODEL, MODEL);

    const sb = sinon.createSandbox();
    try {
      // Mock the openAIEngine and memories response
      const fakeEngine = {
        run: sb.stub().rejects(new Error("Engine failure")),
      };
      sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine);

      const fakeMemories = ["Memory summary 1", "Memory summary 2"];
      sb.stub(
        MemoriesGetterForSuggestionPrompts,
        "getMemorySummariesForPrompt"
      ).resolves(fakeMemories);

      const n = 3;
      const contextTabs = [
        { title: "Only Tab", url: "https://only.example.com" },
      ];

      const result = await generateConversationStartersSidebar(
        contextTabs,
        n,
        true
      );
      Assert.deepEqual(result, [], "Should return empty array on engine error");
    } finally {
      sb.restore();
    }
  }
);

/**
 * Tests that the system prompt, assistant limitations, and memories prompt are included in conversation starter prompts
 */
add_task(
  async function test_generateConversationStartersSidebar_includes_subcomponents() {
    Services.prefs.setStringPref(PREF_API_KEY, API_KEY);
    Services.prefs.setStringPref(PREF_ENDPOINT, ENDPOINT);
    Services.prefs.setStringPref(PREF_MODEL, MODEL);

    const sb = sinon.createSandbox();
    try {
      const fakeEngine = {
        run: sb.stub().resolves({
          finalOutput: `Suggestion 1\nSuggestion 2\nSuggestion 3`,
        }),
      };
      sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine);

      sb.stub(openAIEngine, "getRemoteClient").returns({
        get: sb.stub().resolves(REAL_REMOTE_SETTINGS_SNAPSHOT),
      });

      sb.stub(
        MemoriesGetterForSuggestionPrompts,
        "getMemorySummariesForPrompt"
      ).resolves(["memory 1", "memory 2"]);

      const n = 3;
      const contextTabs = [
        { title: "Test Tab", url: "https://test.example.com" },
      ];

      await generateConversationStartersSidebar(contextTabs, n, true);

      Assert.ok(fakeEngine.run.calledOnce, "Engine run should be called once");

      const callArgs = fakeEngine.run.firstCall.args[0];
      console.log("Prompt content:", callArgs.args);
      Assert.ok(
        callArgs.args[0].content.includes(
          "Generate the requested number of starters."
        ),
        "Prompt should include system prompt from remote settings"
      );
      Assert.ok(
        callArgs.args[1].content.includes(
          "You can do this and cannot do that."
        ),
        "Prompt should include assistant limitations from remote settings"
      );
      Assert.ok(
        callArgs.args[1].content.includes("Use the following memories"),
        "Prompt should include memories prompt from remote settings"
      );
    } finally {
      sb.restore();
    }
  }
);

/**
 * Tests for generateFollowupPrompts successfully generating suggestions
 */
add_task(async function test_generateFollowupPrompts_happy_path() {
  Services.prefs.setStringPref(PREF_API_KEY, API_KEY);
  Services.prefs.setStringPref(PREF_ENDPOINT, ENDPOINT);
  Services.prefs.setStringPref(PREF_MODEL, MODEL);

  const sb = sinon.createSandbox();
  try {
    // Mock the openAIEngine and memories response
    const fakeEngine = {
      run: sb.stub().resolves({
        finalOutput: `1. Suggestion 1\n\n- Suggestion 2.\nSuggestion 3.\nSuggestion 4\nSuggestion 5\nSuggestion 6`,
      }),
    };
    sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine);

    const fakeMemories = ["Memory summary 1", "Memory summary 2"];
    const memoriesStub = sb
      .stub(MemoriesGetterForSuggestionPrompts, "getMemorySummariesForPrompt")
      .resolves(fakeMemories);

    const n = 2;
    const conversationHistory = [
      { role: MESSAGE_ROLE.USER, content: "Hello" },
      { role: MESSAGE_ROLE.ASSISTANT, content: "Hi there!" },
    ];
    const currentTab = {
      title: "Current Tab",
      url: "https://current.example.com",
    };

    // Using memories
    const result = await generateFollowupPrompts(
      conversationHistory,
      currentTab,
      n,
      true
    );
    Assert.ok(fakeEngine.run.calledOnce, "Engine run should be called once");
    Assert.ok(
      memoriesStub.calledOnce,
      "getMemorySummariesForPrompt should be called once"
    );

    const callArgs = fakeEngine.run.firstCall.args[0];
    Assert.equal(
      callArgs.messages.length,
      2,
      "run should be called with 2 messages"
    );
    Assert.ok(
      callArgs.messages[1].content.includes(
        JSON.stringify({
          title: sanitizeUntrustedContent("Current Tab"),
          url: "https://current.example.com",
        })
      ),
      "Prompt should include current tab info"
    );
    Assert.ok(
      callArgs.messages[1].content.includes(
        '[{"role":"user","content":"Hello"},{"role":"assistant","content":"Hi there!"}]'
      ),
      "Prompt should include conversation history"
    );
    Assert.ok(
      callArgs.messages[1].content.includes(
        "\n- Memory summary 1\n- Memory summary 2"
      ),
      "Prompt should include memory summaries"
    );

    Assert.deepEqual(
      result,
      [
        { text: "Suggestion 1", type: "chat" },
        { text: "Suggestion 2", type: "chat" },
      ],
      "Suggestions should match"
    );
  } finally {
    sb.restore();
  }
});

/**
 * Tests for generateFollowupPrompts without including memories
 */
add_task(async function test_generateFollowupPrompts_no_memories() {
  Services.prefs.setStringPref(PREF_API_KEY, API_KEY);
  Services.prefs.setStringPref(PREF_ENDPOINT, ENDPOINT);
  Services.prefs.setStringPref(PREF_MODEL, MODEL);

  const sb = sinon.createSandbox();
  try {
    // Mock the openAIEngine and memories response
    const fakeEngine = {
      run: sb.stub().resolves({
        finalOutput: `1. Suggestion 1\n\n- Suggestion 2.\nSuggestion 3.\nSuggestion 4\nSuggestion 5\nSuggestion 6`,
      }),
    };
    sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine);

    const fakeMemories = ["Memory summary 1", "Memory summary 2"];
    const memoriesStub = sb
      .stub(MemoriesGetterForSuggestionPrompts, "getMemorySummariesForPrompt")
      .resolves(fakeMemories);

    const n = 2;
    const conversationHistory = [
      { role: MESSAGE_ROLE.USER, content: "Hello" },
      { role: MESSAGE_ROLE.ASSISTANT, content: "Hi there!" },
    ];
    const currentTab = {
      title: "Current Tab",
      url: "https://current.example.com",
    };

    const result = await generateFollowupPrompts(
      conversationHistory,
      currentTab,
      n,
      false
    );
    Assert.ok(fakeEngine.run.calledOnce, "Engine run should be called once");
    Assert.ok(
      !memoriesStub.calledOnce,
      "getMemorySummariesForPrompt shouldn't be called"
    );

    const callArgs = fakeEngine.run.firstCall.args[0];
    Assert.equal(
      callArgs.messages.length,
      2,
      "run should be called with 2 messages"
    );
    Assert.ok(
      callArgs.messages[1].content.includes(
        JSON.stringify({
          title: sanitizeUntrustedContent("Current Tab"),
          url: "https://current.example.com",
        })
      ),
      "Prompt should include current tab info"
    );
    Assert.ok(
      callArgs.messages[1].content.includes(
        '[{"role":"user","content":"Hello"},{"role":"assistant","content":"Hi there!"}]'
      ),
      "Prompt should include conversation history"
    );
    Assert.ok(
      !callArgs.messages[1].content.includes(
        "\n- Memory summary 1\n- Memory summary 2"
      ),
      "Prompt shouldn't include memory summaries"
    );

    Assert.deepEqual(
      result,
      [
        { text: "Suggestion 1", type: "chat" },
        { text: "Suggestion 2", type: "chat" },
      ],
      "Suggestions should match"
    );
  } finally {
    sb.restore();
  }
});

/**
 * Tests for generateFollowupPrompts when no memories are returned
 */
add_task(async function test_generateFollowupPrompts_no_memories_returned() {
  Services.prefs.setStringPref(PREF_API_KEY, API_KEY);
  Services.prefs.setStringPref(PREF_ENDPOINT, ENDPOINT);
  Services.prefs.setStringPref(PREF_MODEL, MODEL);

  const sb = sinon.createSandbox();
  try {
    // Mock the openAIEngine and memories response
    const fakeEngine = {
      run: sb.stub().resolves({
        finalOutput: `1. Suggestion 1\n\n- Suggestion 2.\nSuggestion 3.\nSuggestion 4\nSuggestion 5\nSuggestion 6`,
      }),
    };
    sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine);

    const fakeMemories = [];
    const memoriesStub = sb
      .stub(MemoriesGetterForSuggestionPrompts, "getMemorySummariesForPrompt")
      .resolves(fakeMemories);

    const n = 2;
    const conversationHistory = [
      { role: MESSAGE_ROLE.USER, content: "Hello" },
      { role: MESSAGE_ROLE.ASSISTANT, content: "Hi there!" },
    ];
    const currentTab = {
      title: "Current Tab",
      url: "https://current.example.com",
    };

    // Using memories
    const result = await generateFollowupPrompts(
      conversationHistory,
      currentTab,
      n,
      true
    );
    Assert.ok(fakeEngine.run.calledOnce, "Engine run should be called once");
    Assert.ok(
      memoriesStub.calledOnce,
      "getMemorySummariesForPrompt should be called once"
    );

    const callArgs = fakeEngine.run.firstCall.args[0];
    Assert.equal(
      callArgs.messages.length,
      2,
      "run should be called with 2 messages"
    );
    Assert.ok(
      callArgs.messages[1].content.includes(
        JSON.stringify({
          title: sanitizeUntrustedContent("Current Tab"),
          url: "https://current.example.com",
        })
      ),
      "Prompt should include current tab info"
    );
    Assert.ok(
      callArgs.messages[1].content.includes(
        '[{"role":"user","content":"Hello"},{"role":"assistant","content":"Hi there!"}]'
      ),
      "Prompt should include conversation history"
    );
    Assert.ok(
      !callArgs.messages[1].content.includes("\nUser Memories:\n"),
      "Prompt shouldn't include user memories block"
    );

    Assert.deepEqual(
      result,
      [
        { text: "Suggestion 1", type: "chat" },
        { text: "Suggestion 2", type: "chat" },
      ],
      "Suggestions should match"
    );
  } finally {
    sb.restore();
  }
});

/**
 * Tests for generateFollowupPrompts without a current tab
 */
add_task(async function test_generateFollowupPrompts_no_current_tab() {
  Services.prefs.setStringPref(PREF_API_KEY, API_KEY);
  Services.prefs.setStringPref(PREF_ENDPOINT, ENDPOINT);
  Services.prefs.setStringPref(PREF_MODEL, MODEL);

  const sb = sinon.createSandbox();
  try {
    // Mock the openAIEngine and memories response
    const fakeEngine = {
      run: sb.stub().resolves({
        finalOutput: `1. Suggestion 1\n\n- Suggestion 2.\nSuggestion 3.\nSuggestion 4\nSuggestion 5\nSuggestion 6`,
      }),
    };
    sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine);

    const fakeMemories = [];
    const memoriesStub = sb
      .stub(MemoriesGetterForSuggestionPrompts, "getMemorySummariesForPrompt")
      .resolves(fakeMemories);

    const n = 2;
    const conversationHistory = [
      { role: MESSAGE_ROLE.USER, content: "Hello" },
      { role: MESSAGE_ROLE.ASSISTANT, content: "Hi there!" },
    ];
    const currentTab = {};

    const result = await generateFollowupPrompts(
      conversationHistory,
      currentTab,
      n,
      false
    );
    Assert.ok(fakeEngine.run.calledOnce, "Engine run should be called once");
    Assert.ok(
      !memoriesStub.calledOnce,
      "getMemorySummariesForPrompt shouldn't be called"
    );

    const callArgs = fakeEngine.run.firstCall.args[0];
    Assert.equal(
      callArgs.messages.length,
      2,
      "run should be called with 2 messages"
    );
    Assert.ok(
      callArgs.messages[1].content.includes("\nNo tab\n"),
      "Prompt shouldn't include any tab info"
    );
    Assert.ok(
      callArgs.messages[1].content.includes(
        '[{"role":"user","content":"Hello"},{"role":"assistant","content":"Hi there!"}]'
      ),
      "Prompt should include conversation history"
    );
    Assert.ok(
      !callArgs.messages[1].content.includes("\nUser Memories:\n"),
      "Prompt shouldn't include user memories block"
    );

    Assert.deepEqual(
      result,
      [
        { text: "Suggestion 1", type: "chat" },
        { text: "Suggestion 2", type: "chat" },
      ],
      "Suggestions should match"
    );
  } finally {
    sb.restore();
  }
});

/**
 * Tests that generateFollowupPrompts handles engine errors gracefully
 */
add_task(async function test_generateFollowupPrompts_engine_error() {
  Services.prefs.setStringPref(PREF_API_KEY, API_KEY);
  Services.prefs.setStringPref(PREF_ENDPOINT, ENDPOINT);
  Services.prefs.setStringPref(PREF_MODEL, MODEL);

  const sb = sinon.createSandbox();
  try {
    // Mock the openAIEngine and memories response
    const fakeEngine = {
      run: sb.stub().rejects(new Error("Engine failure")),
    };
    sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine);

    const fakeMemories = [];
    sb.stub(
      MemoriesGetterForSuggestionPrompts,
      "getMemorySummariesForPrompt"
    ).resolves(fakeMemories);

    const n = 2;
    const conversationHistory = [
      { role: MESSAGE_ROLE.USER, content: "Hello" },
      { role: MESSAGE_ROLE.ASSISTANT, content: "Hi there!" },
    ];
    const currentTab = {};

    const result = await generateFollowupPrompts(
      conversationHistory,
      currentTab,
      n,
      false
    );
    Assert.deepEqual(result, [], "Should return empty array on engine error");
  } finally {
    sb.restore();
  }
});

/**
 * Tests that assistant limitations are included in followup prompts
 */
add_task(
  async function test_generateFollowupPrompts_includes_assistant_limitations() {
    Services.prefs.setStringPref(PREF_API_KEY, API_KEY);
    Services.prefs.setStringPref(PREF_ENDPOINT, ENDPOINT);
    Services.prefs.setStringPref(PREF_MODEL, MODEL);

    const sb = sinon.createSandbox();
    try {
      const fakeEngine = {
        run: sb.stub().resolves({
          finalOutput: `Suggestion 1\nSuggestion 2`,
        }),
      };
      sb.stub(openAIEngine, "_createEngine").resolves(fakeEngine);

      sb.stub(openAIEngine, "getRemoteClient").returns({
        get: sb.stub().resolves(REAL_REMOTE_SETTINGS_SNAPSHOT),
      });

      sb.stub(
        MemoriesGetterForSuggestionPrompts,
        "getMemorySummariesForPrompt"
      ).resolves([]);

      const n = 2;
      const conversationHistory = [
        { role: MESSAGE_ROLE.USER, content: "Hello" },
        { role: MESSAGE_ROLE.ASSISTANT, content: "Hi there!" },
      ];
      const currentTab = { title: "Test", url: "https://test.example.com" };

      await generateFollowupPrompts(conversationHistory, currentTab, n, false);

      Assert.ok(fakeEngine.run.calledOnce, "Engine run should be called once");

      const callArgs = fakeEngine.run.firstCall.args[0];
      Assert.ok(
        callArgs.messages[1].content.includes(
          "You can do this and cannot do that."
        ),
        "Prompt should include assistant limitations from remote settings"
      );
    } finally {
      sb.restore();
    }
  }
);

/**
 * Tests for getMemorySummariesForPrompt happy path
 */
add_task(async function test_getMemorySummariesForPrompt_happy_path() {
  const sb = sinon.createSandbox();
  try {
    // Mock the MemoryStore to return fixed memories
    const fakeMemories = [
      {
        memory_summary: "Memory summary 1",
      },
      {
        memory_summary: "Memory summary 2",
      },
      {
        memory_summary: "Memory summary 3",
      },
    ];

    sb.stub(MemoriesManager, "getAllMemories").resolves(fakeMemories);

    const maxMemories = 2;
    const summaries =
      await MemoriesGetterForSuggestionPrompts.getMemorySummariesForPrompt(
        maxMemories
      );

    Assert.deepEqual(
      summaries,
      ["Memory summary 1", "Memory summary 2"],
      "Memory summaries should match expected values"
    );
  } finally {
    sb.restore();
  }
});

/**
 * Tests for getMemorySummariesForPrompt when no memories are returned
 */
add_task(async function test_getMemorySummariesForPrompt_no_memories() {
  const sb = sinon.createSandbox();
  try {
    // Mock the MemoryStore to return fixed memories
    const fakeMemories = [];

    sb.stub(MemoriesManager, "getAllMemories").resolves(fakeMemories);

    const maxMemories = 2;
    const summaries =
      await MemoriesGetterForSuggestionPrompts.getMemorySummariesForPrompt(
        maxMemories
      );

    Assert.equal(
      summaries.length,
      0,
      `getMemorySummariesForPrompt(${maxMemories}) should return 0 summaries`
    );
  } finally {
    sb.restore();
  }
});

/**
 * Tests for getMemorySummariesForPrompt with fewer memories than maxMemories
 */
add_task(async function test_getMemorySummariesForPrompt_too_few_memories() {
  const sb = sinon.createSandbox();
  try {
    // Mock the MemoryStore to return fixed memories
    const fakeMemories = [
      {
        memory_summary: "Memory summary 1",
      },
    ];

    sb.stub(MemoriesManager, "getAllMemories").resolves(fakeMemories);

    const maxMemories = 2;
    const summaries =
      await MemoriesGetterForSuggestionPrompts.getMemorySummariesForPrompt(
        maxMemories
      );

    Assert.deepEqual(
      summaries,
      ["Memory summary 1"],
      "Memory summaries should match expected values"
    );
  } finally {
    sb.restore();
  }
});

/**
 * Tests for getMemorySummariesForPrompt handling duplicate summaries
 */
add_task(async function test_getMemorySummariesForPrompt_duplicates() {
  const sb = sinon.createSandbox();
  try {
    // Mock the MemoryStore to return fixed memories
    const fakeMemories = [
      {
        memory_summary: "Duplicate summary",
      },
      {
        memory_summary: "duplicate summary",
      },
      {
        memory_summary: "Unique summary",
      },
    ];

    sb.stub(MemoriesManager, "getAllMemories").resolves(fakeMemories);

    const maxMemories = 2;
    const summaries =
      await MemoriesGetterForSuggestionPrompts.getMemorySummariesForPrompt(
        maxMemories
      );

    Assert.deepEqual(
      summaries,
      ["Duplicate summary", "Unique summary"],
      "Memory summaries should match expected values"
    );
  } finally {
    sb.restore();
  }
});

/**
 * Tests for getMemorySummariesForPrompt handling empty and whitespace-only summaries
 */
add_task(
  async function test_getMemorySummariesForPrompt_empty_and_whitespace() {
    const sb = sinon.createSandbox();
    try {
      // Mock the MemoryStore to return fixed memories
      const fakeMemories = [
        {
          memory_summary: "   \n",
        },
        {
          memory_summary: "",
        },
        {
          memory_summary: "Valid summary",
        },
      ];

      sb.stub(MemoriesManager, "getAllMemories").resolves(fakeMemories);

      const maxMemories = 2;
      const summaries =
        await MemoriesGetterForSuggestionPrompts.getMemorySummariesForPrompt(
          maxMemories
        );

      Assert.deepEqual(
        summaries,
        ["Valid summary"],
        "Memory summaries should match expected values"
      );
    } finally {
      sb.restore();
    }
  }
);
