import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import {
  ContentValidationError,
  OPTION_IDS,
  loadPacks,
  toContentQuestion,
  validatePacks,
  type ContentIssue,
} from '../src/content/index';

const tempDirs: string[] = [];
afterEach(() => {
  for (const dir of tempDirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
});

const MANIFEST = {
  key: 'sample-v1',
  name: 'Sample',
  description: 'Test pack',
  kind: 'GENERAL',
  categories: ['alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'eta', 'theta', 'general'].map((key) => ({
    key,
    label: key.toUpperCase(),
  })),
};

function mc(id: string, prompt: string, overrides: Record<string, unknown> = {}) {
  return {
    id,
    difficulty: 'STANDARD',
    type: 'MULTIPLE_CHOICE',
    prompt,
    options: ['One', 'Two', 'Three', 'Four'],
    answer: 0,
    explanation: 'Because one comes first.',
    wildcard: false,
    ...overrides,
  };
}

function makePacksDir(files: Record<string, unknown>, manifest: unknown = MANIFEST): string {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pintrush-content-'));
  tempDirs.push(dir);
  const packDir = path.join(dir, 'sample-v1');
  fs.mkdirSync(packDir);
  fs.writeFileSync(path.join(packDir, 'pack.json'), JSON.stringify(manifest));
  for (const [name, content] of Object.entries(files)) {
    fs.writeFileSync(path.join(packDir, name), typeof content === 'string' ? content : JSON.stringify(content));
  }
  return dir;
}

function messages(issues: ContentIssue[]): string {
  return issues.map((i) => `${i.file} ${i.questionId ?? ''} ${i.message}`).join('\n');
}

describe('real question packs', () => {
  const result = validatePacks();

  it('validate without errors', () => {
    expect(result.errors, messages(result.errors)).toEqual([]);
    expect(result.packs.map((p) => p.key)).toEqual(expect.arrayContaining(['originals-v1', 'music-mayhem-v1']));
  });

  it('convert to the stored shape', () => {
    for (const pack of result.packs) {
      expect(pack.categories).toHaveLength(9);
      for (const q of pack.questions) {
        expect(q.contentKey).toBe(`${pack.key}/${q.sourceId}`);
        expect(q.sourceId.startsWith(`${q.category}-`)).toBe(true);
        expect(pack.categories.map((c) => c.key)).toContain(q.category);
        expect(q.options.map((o) => o.id)).toEqual(OPTION_IDS.slice(0, q.options.length));
        expect(q.options.map((o) => o.id)).toContain(q.correctOptionId);
        if (q.type === 'TRUE_FALSE') expect(q.options.map((o) => o.text)).toEqual(['True', 'False']);
        else expect(q.options).toHaveLength(4);
        if (q.category === 'general' || q.category === 'behind-the-music') expect(q.wildcardEligible).toBe(true);
      }
    }
  });

  it('load deterministically', () => {
    expect(validatePacks().packs).toEqual(result.packs);
  });
});

describe('option shuffling', () => {
  it('moves the answer off the writer position deterministically and keeps the right text', () => {
    const letters = new Set<string>();
    for (let i = 1; i <= 40; i++) {
      const raw = { ...mc(`alpha-${String(i).padStart(3, '0')}`, `Question ${i}?`), answer: 0 } as Parameters<typeof toContentQuestion>[2];
      const q = toContentQuestion('sample-v1', 'alpha', raw);
      const again = toContentQuestion('sample-v1', 'alpha', raw);
      expect(again).toEqual(q);
      expect(q.options.find((o) => o.id === q.correctOptionId)?.text).toBe('One');
      expect(q.options.map((o) => o.text).sort()).toEqual(['Four', 'One', 'Three', 'Two']);
      letters.add(q.correctOptionId);
    }
    expect(letters.size).toBe(4);
  });

  it('keeps True/False order', () => {
    const raw = { ...mc('alpha-001', 'Is it?'), type: 'TRUE_FALSE', options: ['True', 'False'], answer: 1 } as Parameters<
      typeof toContentQuestion
    >[2];
    const q = toContentQuestion('sample-v1', 'alpha', raw);
    expect(q.options).toEqual([
      { id: 'a', text: 'True' },
      { id: 'b', text: 'False' },
    ]);
    expect(q.correctOptionId).toBe('b');
  });
});

describe('validation of bad packs', () => {
  it('accepts a minimal valid pack and applies wildcard rules', () => {
    const dir = makePacksDir({
      'alpha.json': { pack: 'sample-v1', category: 'alpha', questions: [mc('alpha-001', 'A?'), mc('alpha-002', 'B?', { wildcard: true })] },
      'general.json': { pack: 'sample-v1', category: 'general', questions: [mc('general-001', 'C?', { wildcard: false })] },
    });
    const { packs, errors, warnings } = validatePacks({ dir });
    expect(errors).toEqual([]);
    const qs = packs[0]!.questions;
    expect(qs.map((q) => [q.contentKey, q.wildcardEligible])).toEqual([
      ['sample-v1/alpha-001', false],
      ['sample-v1/alpha-002', true],
      ['sample-v1/general-001', true],
    ]);
    expect(packs[0]!.missingCategories).toHaveLength(7);
    expect(warnings.length).toBe(7);
    expect(loadPacks({ dir })[0]!.questions).toHaveLength(3);
  });

  it('reports every rule violation', () => {
    const dir = makePacksDir({
      'alpha.json': {
        pack: 'sample-v1',
        category: 'alpha',
        questions: [
          mc('alpha-001', 'Three options?', { options: ['A', 'B', 'C'] }),
          mc('alpha-002', 'Yes or no?', { type: 'TRUE_FALSE', options: ['Yes', 'No'] }),
          mc('alpha-003', 'Answer out of range?', { answer: 4 }),
          mc('alpha-004', 'Duplicate option?', { options: ['Same', 'same ', 'Other', 'Else'] }),
          mc('alpha-005', 'x'.repeat(161)),
          mc('alpha-006', 'Long option?', { options: ['y'.repeat(61), 'B', 'C', 'D'] }),
          mc('alpha-007', 'Long explanation?', { explanation: 'z'.repeat(201) }),
          mc('alpha-008', 'Shared   PROMPT?'),
          mc('alpha-008', 'Repeated id?'),
          mc('beta-009', 'Wrong prefix?'),
          mc('alpha-010', 'Bad difficulty?', { difficulty: 'HARD' }),
          mc('alpha-011', 'Fractional answer?', { answer: 1.5 }),
          { id: 'alpha-012', prompt: 'Missing fields?' },
        ],
      },
      'beta.json': { pack: 'sample-v1', category: 'beta', questions: [mc('beta-001', 'shared prompt?')] },
      'omega.json': { pack: 'sample-v1', category: 'omega', questions: [mc('omega-001', 'Unknown category?')] },
      'gamma.json': { pack: 'other-pack', category: 'gamma', questions: [mc('gamma-001', 'Wrong pack?')] },
      'delta.json': '{ not json',
      'epsilon.json': { pack: 'sample-v1', category: 'epsilon', questions: [] },
    });
    const { errors, packs } = validatePacks({ dir });
    const text = messages(errors);
    const expectations: [string, RegExp][] = [
      ['alpha-001', /exactly 4 options/],
      ['alpha-002', /TRUE_FALSE options must be exactly/],
      ['alpha-003', /not a valid option index/],
      ['alpha-004', /Duplicate option/],
      ['alpha-005', /prompt/],
      ['alpha-006', /options\.0/],
      ['alpha-007', /explanation/],
      ['alpha-008', /Duplicate id/],
      ['beta-009', /must start with "alpha-"/],
      ['alpha-010', /difficulty/],
      ['alpha-011', /answer/],
      ['alpha-012', /options|difficulty|type/],
      ['beta-001', /Duplicate prompt/],
    ];
    for (const [id, pattern] of expectations) {
      const forId = errors.filter((e) => e.questionId === id).map((e) => e.message).join(' | ');
      expect(forId, `${id}\n${text}`).toMatch(pattern);
    }
    expect(text).toMatch(/omega\.json.*not a category/);
    expect(text).toMatch(/gamma\.json.*"pack" is "other-pack"/);
    expect(text).toMatch(/delta\.json.*Invalid JSON/);
    expect(text).toMatch(/epsilon\.json.*no questions/);
    // Only the valid first "shared prompt" survives.
    expect(packs[0]!.questions.map((q) => q.sourceId)).toEqual(['alpha-008']);
    expect(() => loadPacks({ dir })).toThrow(ContentValidationError);
  });

  it('rejects manifests that do not have 9 unique categories or mismatch the folder', () => {
    const eight = { ...MANIFEST, categories: MANIFEST.categories.slice(0, 8) };
    expect(messages(validatePacks({ dir: makePacksDir({}, eight) }).errors)).toMatch(/exactly 9 categories/);
    const dup = { ...MANIFEST, categories: [...MANIFEST.categories.slice(0, 8), MANIFEST.categories[0]] };
    expect(messages(validatePacks({ dir: makePacksDir({}, dup) }).errors)).toMatch(/Duplicate category/);
    const renamed = { ...MANIFEST, key: 'renamed-v1' };
    expect(messages(validatePacks({ dir: makePacksDir({}, renamed) }).errors)).toMatch(/must match its folder/);
  });
});
