import { describe, it, expect, vi, beforeEach } from "vitest";

vi.mock("@lila/db", () => ({ getGameTerms: vi.fn(), getDistractors: vi.fn() }));

import { getGameTerms, getDistractors } from "@lila/db";
import { generateMultiplayerQuestions } from "./multiplayerGameService.js";

const mockGetGameTerms = vi.mocked(getGameTerms);
const mockGetDistractors = vi.mocked(getDistractors);

const fakeTerms = [
  { termId: "t1", sourceText: "dog", targetText: "cane", sourceGloss: null },
  { termId: "t2", sourceText: "cat", targetText: "gatto", sourceGloss: null },
  {
    termId: "t3",
    sourceText: "house",
    targetText: "casa",
    sourceGloss: "a building for living in",
  },
];

beforeEach(() => {
  vi.clearAllMocks();
  mockGetGameTerms.mockResolvedValue(fakeTerms);
  mockGetDistractors.mockResolvedValue(["wrong1", "wrong2", "wrong3"]);
});

describe("generateMultiplayerQuestions", () => {
  it("returns the correct number of questions", async () => {
    const questions = await generateMultiplayerQuestions();

    expect(questions).toHaveLength(3);
  });

  it("each question has exactly 4 options", async () => {
    const questions = await generateMultiplayerQuestions();

    for (const question of questions) {
      expect(question.options).toHaveLength(4);
    }
  });

  it("each question has a unique questionId", async () => {
    const questions = await generateMultiplayerQuestions();
    const ids = questions.map((q) => q.questionId);

    expect(new Set(ids).size).toBe(ids.length);
  });

  it("options have sequential optionIds 0-3", async () => {
    const questions = await generateMultiplayerQuestions();

    for (const question of questions) {
      const optionIds = question.options.map((o) => o.optionId);
      expect(optionIds).toEqual([0, 1, 2, 3]);
    }
  });

  it("the correct answer is always among the options", async () => {
    const questions = await generateMultiplayerQuestions();

    for (let i = 0; i < questions.length; i++) {
      const question = questions[i]!;
      const correctText = fakeTerms[i]!.targetText;
      const optionTexts = question.options.map((o) => o.text);

      expect(optionTexts).toContain(correctText);
    }
  });

  it("correctOptionId points to the option whose text matches the correct answer", async () => {
    const questions = await generateMultiplayerQuestions();

    for (let i = 0; i < questions.length; i++) {
      const question = questions[i]!;
      const correctText = fakeTerms[i]!.targetText;
      const correctOption = question.options.find(
        (o) => o.optionId === question.correctOptionId,
      );

      expect(correctOption?.text).toBe(correctText);
    }
  });

  it("sets the prompt from the source text", async () => {
    const questions = await generateMultiplayerQuestions();

    expect(questions[0]!.prompt).toBe("dog");
    expect(questions[1]!.prompt).toBe("cat");
    expect(questions[2]!.prompt).toBe("house");
  });

  it("passes gloss through (null or string)", async () => {
    const questions = await generateMultiplayerQuestions();

    expect(questions[0]!.gloss).toBeNull();
    expect(questions[2]!.gloss).toBe("a building for living in");
  });

  it("calls getGameTerms with the multiplayer defaults", async () => {
    await generateMultiplayerQuestions();

    expect(mockGetGameTerms).toHaveBeenCalledWith(
      "en",
      "it",
      "noun",
      "easy",
      3,
    );
  });

  it("calls getDistractors once per question", async () => {
    await generateMultiplayerQuestions();

    expect(mockGetDistractors).toHaveBeenCalledTimes(3);
  });

  it("propagates unexpected errors from getGameTerms", async () => {
    mockGetGameTerms.mockRejectedValue(new Error("connection refused"));

    await expect(generateMultiplayerQuestions()).rejects.toThrow(
      "connection refused",
    );
  });
});
