const test = require('node:test'); const assert = require('node:assert/strict'); const Database = require('better-sqlite3'); const fs = require('fs'); const path = require('path'); const { applyEpgOrder, migrateEpgPriorities, nextEpgPriority, sortEpgSources, validateCompleteOrder } = require('../server/services/epgSourceOrdering'); const { chooseBestMatch, findBestMatch } = require('../server/services/epgMatchingService'); const { ensureBuiltinSources } = require('../server/services/builtinEpgRegistry'); const source = (id, type = 'epg', extra = {}) => ({ id, type, enabled: true, ...extra }); test('priority migration is idempotent and appends built-ins after existing custom EPG sources', () => { const data = { sources: [source(1, 'epg', { name: 'Custom' })], nextId: 2 }; ensureBuiltinSources(data); const first = data.sources.map(item => [item.id, item.epgPriority]); ensureBuiltinSources(data); assert.deepEqual(data.sources.map(item => [item.id, item.epgPriority]), first); assert.equal(data.sources[0].epgPriority, 0); assert.ok(data.sources.filter(item => item.builtinKey).every(item => item.epgPriority > 0)); assert.equal(nextEpgPriority(data.sources), 30); migrateEpgPriorities(data); }); test('complete EPG order normalizes priorities for enabled and disabled built-in/custom sources', () => { const all = [source(1), source(2, 'm3u'), source(3, 'epg', { enabled: false, builtinKey: 'built-in' })]; assert.equal(validateCompleteOrder(all, [3, 1]), null); const ordered = applyEpgOrder(all, [3, 1], 'now'); assert.deepEqual(ordered.map(item => [item.id, item.epgPriority]), [[3, 0], [1, 10]]); assert.equal(all.find(item => item.id === 3).updated_at, 'now'); }); test('EPG order validation rejects duplicates, unknown, non-EPG and incomplete lists', () => { const all = [source(1), source(2), source(3, 'm3u')]; assert.equal(validateCompleteOrder(all, [1, 1]).code, 'DUPLICATE_SOURCE_ID'); assert.equal(validateCompleteOrder(all, [1, 9]).code, 'UNKNOWN_SOURCE_ID'); assert.equal(validateCompleteOrder(all, [1, 3]).code, 'SOURCE_NOT_EPG'); assert.equal(validateCompleteOrder(all, [1]).code, 'INCOMPLETE_EPG_ORDER'); }); test('enabled EPG sources are sorted ascending and disabled sources are excluded from reads', () => { const all = [source(1, 'epg', { epgPriority: 20 }), source(2, 'epg', { epgPriority: 0, enabled: false }), source(3, 'epg', { epgPriority: 10 })]; assert.deepEqual(sortEpgSources(all).map(item => item.id), [2, 3, 1]); assert.deepEqual(sortEpgSources(all, { enabledOnly: true }).map(item => item.id), [3, 1]); }); test('match score wins before source priority and priority breaks equal scores', () => { const channel = { displayName: 'News France', variants: [{ tvgId: 'news.fr', name: 'News France' }] }; const sources = [source(1, 'epg', { epgPriority: 0 }), source(2, 'epg', { epgPriority: 10 })]; const strongLater = chooseBestMatch(channel, [ { sourceId: 1, id: 'other', name: 'News' }, { sourceId: 2, id: 'news.fr', name: 'Different' } ], sources); assert.equal(strongLater.sourceId, 2); const tied = chooseBestMatch(channel, [ { sourceId: 2, id: 'news.fr', name: 'News France' }, { sourceId: 1, id: 'news.fr', name: 'News France' } ], sources); assert.equal(tied.sourceId, 1); }); test('EPG matching ignores disabled sources and returns the current programme from the selected source', () => { const db = new Database(':memory:'); db.exec(` CREATE TABLE playlist_items (source_id INTEGER, item_id TEXT, type TEXT, name TEXT); CREATE TABLE epg_programs (source_id INTEGER, channel_id TEXT, start_time INTEGER, end_time INTEGER, title TEXT, description TEXT); INSERT INTO playlist_items VALUES (1, 'news.fr', 'epg_channel', 'News France'); INSERT INTO playlist_items VALUES (2, 'news.fr', 'epg_channel', 'News France'); INSERT INTO epg_programs VALUES (2, 'news.fr', 900, 1100, 'Current news', 'Headlines'); `); const match = findBestMatch(db, { displayName: 'News France', variants: [{ tvgId: 'news.fr' }] }, [ source(1, 'epg', { epgPriority: 0, enabled: false }), source(2, 'epg', { epgPriority: 10 }) ], 1000); assert.equal(match.sourceId, 2); assert.equal(match.currentProgram.title, 'Current news'); }); test('sources router exposes the complete-order endpoint', () => { const text = fs.readFileSync(path.join(__dirname, '..', 'server', 'routes', 'sources.js'), 'utf8'); assert.match(text, /router\.put\('\/epg\/order'/); assert.match(text, /sources\.reorderEpg\(req\.body\?\.orderedIds\)/); });