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

const bankStore = new Map();
const callLLMMock = vi.fn();

vi.mock('../pb', () => ({ pb: { collection: () => ({}) } }));

vi.mock('../db', () => ({
  getQuizBank: vi.fn(async (topicId) => bankStore.get(topicId) || []),
  setQuizBank: vi.fn(async (topicId, qs) => { bankStore.set(topicId, qs); }),
  getTopics: vi.fn(async () => []),
  deleteQuestionFromBank: vi.fn(),
  getCachedQuiz: vi.fn(),
  setCachedQuiz: vi.fn(),
  getQuizResult: vi.fn(),
  saveQuizResult: vi.fn(),
  getTeamMembers: vi.fn(async () => []),
  upsertLeaderboardEntry: vi.fn(),
  getCurriculum: vi.fn(),
}));

vi.mock('../llm', () => ({
  callLLM: (...args) => callLLMMock(...args),
  cachedSystem: (text) => [{ type: 'text', text }],
}));
vi.mock('../curriculumService', () => ({
  getCurriculumTopic: vi.fn(async () => ({ topic: null })),
  getQuarterForWeek: vi.fn(() => 1),
}));

import { forceGenerateTopicQuestions } from '../testService';

const topic = { id: 'onboarding', label: 'Onboarding', type: 'concept', description: 'Onboarding for new joiners.' };

function makeQuestion(i, overrides = {}) {
  return {
    id: `q-${i}`,
    question: `Sample question ${i}?`,
    topicLabel: 'Onboarding',
    options: ['A) one', 'B) two', 'C) three', 'D) four'],
    correctIndex: i % 4,
    explanation: 'This is a substantive explanation for the correct answer.',
    difficulty: 'medium',
    ...overrides,
  };
}

function llmEmits(questions) {
  callLLMMock.mockResolvedValueOnce({ toolUses: [{ name: 'emit_quiz_questions', input: { questions } }] });
}

describe('forceGenerateTopicQuestions', () => {
  let debugSpy, warnSpy;
  beforeEach(() => {
    bankStore.clear();
    callLLMMock.mockReset();
    debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => { });
    warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
  });
  afterEach(() => { debugSpy.mockRestore(); warnSpy.mockRestore(); });

  it('persists a well-formed batch and assigns topic-scoped ids', async () => {
    llmEmits([0, 1, 2, 3, 4].map((i) => makeQuestion(i)));
    const out = await forceGenerateTopicQuestions(topic, 5);
    expect(out).toHaveLength(5);
    for (const q of out) expect(q.id.startsWith('onboarding-')).toBe(true);
    expect(bankStore.get('onboarding')).toHaveLength(5);
  });

  it('re-rolls when one correctIndex dominates the batch, then accepts on the third try', async () => {
    const allZero = [0, 1, 2, 3, 4].map((i) => makeQuestion(i, { correctIndex: 0 }));
    llmEmits(allZero);
    llmEmits(allZero);
    llmEmits(allZero);
    const out = await forceGenerateTopicQuestions(topic, 5);
    expect(callLLMMock).toHaveBeenCalledTimes(3);
    expect(out).toHaveLength(5);
    expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('correctIndex dominated'));
  });

  it('rejects a batch containing a banned "all of the above" option', async () => {
    const bad = [0, 1, 2, 3, 4].map((i) =>
      makeQuestion(i, { options: ['A) x', 'B) y', 'C) z', 'D) All of the above'] }),
    );
    llmEmits(bad);
    llmEmits(bad);
    llmEmits(bad);
    await expect(forceGenerateTopicQuestions(topic, 5)).rejects.toThrow(/banned filler|rejected/i);
  });

  it('rejects a batch where an explanation is too short', async () => {
    const bad = [0, 1, 2, 3, 4].map((i) => makeQuestion(i, { explanation: 'Because.' }));
    llmEmits(bad);
    llmEmits(bad);
    llmEmits(bad);
    await expect(forceGenerateTopicQuestions(topic, 5)).rejects.toThrow(/too short|rejected/i);
  });

  it('drops duplicates whose normalized text matches an existing bank entry', async () => {
    bankStore.set('onboarding', [
      { ...makeQuestion(99), id: 'old-1', question: 'What is the BUDDY system???' },
    ]);
    llmEmits([
      makeQuestion(0, { question: 'what is the buddy system!' }),
      makeQuestion(1, { question: 'Brand new question one?' }),
      makeQuestion(2, { question: 'Brand new question two?' }),
      makeQuestion(3, { question: 'Brand new question three?' }),
      makeQuestion(4, { question: 'Brand new question four?' }),
    ]);
    const out = await forceGenerateTopicQuestions(topic, 5);
    expect(out).toHaveLength(4);
    expect(out.find((q) => q.question.toLowerCase().includes('buddy'))).toBeUndefined();
    expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining('dropped duplicate'), expect.any(String));
  });
});
