'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const { parseSchedule, scheduled, publicSchedule, answerMatches, createReadingAccess } = require('../reading-access'); const windows = parseSchedule(JSON.stringify({ 0: [['08:00', '01:00']], 1: [['07:00', '12:00'], ['13:00', '24:00']], 2: [['07:00', '24:00']], 3: [['07:00', '24:00']], 4: [['07:00', '24:00']], 5: [['07:00', '24:00']], 6: [['08:00', '01:00']], }), 'Europe/Berlin'); const at = iso => new Date(iso); test('multiple daily windows and weekend overnight carry in local time', () => { assert.equal(scheduled(windows, at('2026-10-12T08:00:00Z')), true); // Monday 10:00 assert.equal(scheduled(windows, at('2026-10-12T10:30:00Z')), false); // lunch gap 12:30 assert.equal(scheduled(windows, at('2026-10-12T11:30:00Z')), true); assert.equal(scheduled(windows, at('2026-10-10T22:30:00Z')), true); // Sunday 00:30 from Saturday assert.equal(scheduled(windows, at('2026-10-11T22:30:00Z')), true); // Monday 00:30 from Sunday assert.equal(scheduled(windows, at('2026-10-11T23:30:00Z')), false); // Monday 01:30 }); test('schedule config rejects malformed and zero-length windows', () => { assert.throws(() => parseSchedule('{"1":[["12:00","12:00"]]}', 'Europe/Berlin')); assert.throws(() => parseSchedule('{"7":[]}', 'Europe/Berlin')); assert.throws(() => parseSchedule('{"1":[["25:00","26:00"]]}', 'Europe/Berlin')); }); test('public schedule exposes only the configured read-only windows', () => { const view = publicSchedule(windows); assert.equal(view.enabled, true); assert.equal(view.timezone, 'Europe/Berlin'); assert.deepEqual(view.days[1], [['07:00', '12:00'], ['13:00', '24:00']]); assert.deepEqual(view.days[6], [['08:00', '01:00']]); assert.deepEqual(publicSchedule(null), { enabled: false, timezone: null, days: [] }); }); test('answers accept aliases and small typos but not unrelated words', () => { assert.equal(answerMatches(' luffy ', ['Monkey D. Luffy', 'Luffy']), true); assert.equal(answerMatches('Lufy', ['Luffy']), true); assert.equal(answerMatches('pirate', ['Luffy']), false); }); test('five ordered answers grant time, allow retries and reset on a new visit', () => { let clock = Date.parse('2026-10-14T01:00:00Z'), saves = 0; const grants = {}, bank = { moon: Array.from({ length: 5 }, (_, i) => ({ id: `q${i}`, prompt: `Question ${i}`, answers: [`answer${i}`], requires: ['v01/c0001'] })) }; let finished = false; const gate = createReadingAccess({ schedule: parseSchedule('{"0":[],"1":[],"2":[],"3":[],"4":[],"5":[],"6":[]}', 'UTC'), bank, grants, save: () => saves++, now: () => clock, getProgress: () => ({ 'v01/c0001': { done: finished } }), getMangaTitle: () => 'Moon Garden' }); assert.equal(gate.status('reader', true).quizAvailable, false); assert.throws(() => gate.start('reader')); finished = true; assert.equal(gate.status('reader', true).quizAvailable, true); const first = gate.start('reader'); assert.equal(gate.answer('reader', first.challenge, 'wrong').correct, false); assert.equal(gate.start('reader').number, 1); assert.throws(() => gate.answer('reader', first.challenge, 'answer0')); let current = gate.start('reader'); for (let i = 0; i < 5; i++) { const answer = bank.moon.find(q => q.prompt === current.prompt).answers[0]; current = gate.answer('reader', current.challenge, answer); } assert.equal(current.complete, true); assert.equal(grants.reader, clock + 3600000); assert.equal(saves, 1); assert.equal(gate.status('reader').allowed, true); clock += 3600000; assert.equal(gate.status('reader').allowed, false); });