'use strict';
/**
 * Integration tests: /api/lab — curriculum links (Phase 5).
 * Covers: related (auth), reverse lookup, admin add/delete, validation,
 * role-gating, textbook/topic existence checks, enabled-filtering of reverse.
 */
const { describe, it, before, after } = require('node:test');
const assert = require('node:assert/strict');
const { app, db, inject, getToken, cleanup } = require('./setup');

// Mount /api/lab on the shared test app.
app.use('/api/lab', require('../src/routes/lab'));

after(() => cleanup());

/**
 * Schema-robust insert: fills every NOT NULL column (without a default) that the
 * caller didn't provide with a safe placeholder, then inserts. Returns lastInsertRowid.
 * Protects the seed from schema drift (e.g. textbooks.html_path NOT NULL) introduced
 * by parallel sessions on this branch.
 */
function seedRow(table, provided) {
  const cols = db.prepare(`PRAGMA table_info(${table})`).all();
  const colNames = new Set(cols.map(c => c.name));
  // Keep ONLY keys that are real columns (drops fields absent in this schema —
  // robust to drift, e.g. topics may lack slug/subject_id on some branches).
  const row = {};
  for (const k of Object.keys(provided)) if (colNames.has(k)) row[k] = provided[k];
  // Fill any required (NOT NULL, no default) column the caller didn't provide.
  for (const c of cols) {
    if (c.pk) continue;
    if (c.name in row) continue;
    if (c.notnull && c.dflt_value === null) {
      row[c.name] = /INT|REAL|NUM/i.test(c.type) ? 0 : '';
    }
  }
  const names = Object.keys(row);
  const ph = names.map(() => '?').join(', ');
  const info = db.prepare(`INSERT INTO ${table} (${names.join(', ')}) VALUES (${ph})`)
    .run(...names.map(n => row[n]));
  return info.lastInsertRowid;
}

describe('/api/lab curriculum links', () => {
  let adminToken, studentToken, tbSlug, topicId;

  before(async () => {
    adminToken = (await getToken('admin')).token;
    studentToken = (await getToken('student')).token;
    // Seed a textbook + topic to link against (schema-robust — fills NOT NULL cols).
    tbSlug = 'phys-test';
    seedRow('textbooks', { slug: tbSlug, title: 'Физика тест', subject: 'physics', grade: 9, is_active: 1 });
    const subjId = seedRow('subjects', { name: 'LinkTest Subj', slug: 'linktest-subj' });
    topicId = seedRow('topics', { subject_id: subjId, name: 'Колебания тест', slug: 'kolebaniya-test' });
  });

  it('GET /related requires auth (401)', async () => {
    const res = await inject('GET', '/api/lab/sims/pendulum/related', null, null);
    assert.equal(res.status, 401, `got ${res.status}`);
  });

  it('GET /related returns empty link buckets for a sim with no links', async () => {
    const res = await inject('GET', '/api/lab/sims/pendulum/related', null, studentToken);
    assert.equal(res.status, 200, `got ${res.status}`);
    assert.equal(res.body.sim.id, 'pendulum');
    assert.deepEqual(res.body.links.textbook, []);
    assert.deepEqual(res.body.links.topic, []);
  });

  it('POST /links is admin-only (student → 403)', async () => {
    const res = await inject('POST', '/api/lab/sims/pendulum/links',
      { kind: 'textbook', ref_id: tbSlug }, studentToken);
    assert.equal(res.status, 403, `got ${res.status}`);
  });

  it('admin can add a textbook link; label resolved from textbooks', async () => {
    const res = await inject('POST', '/api/lab/sims/pendulum/links',
      { kind: 'textbook', ref_id: tbSlug }, adminToken);
    assert.equal(res.status, 200, `got ${res.status}`);
    assert.equal(res.body.link.kind, 'textbook');
    assert.equal(res.body.link.ref_id, tbSlug);
    assert.equal(res.body.link.label, 'Физика тест', 'label resolved from textbook title');
    assert.ok(res.body.link.href.includes(tbSlug), 'href points to textbook');
  });

  it('related now shows the textbook link', async () => {
    const res = await inject('GET', '/api/lab/sims/pendulum/related', null, studentToken);
    assert.equal(res.status, 200);
    assert.equal(res.body.links.textbook.length, 1);
    assert.equal(res.body.links.textbook[0].ref_id, tbSlug);
  });

  it('admin can add a topic link; label resolved from topics', async () => {
    const res = await inject('POST', '/api/lab/sims/pendulum/links',
      { kind: 'topic', ref_id: String(topicId) }, adminToken);
    assert.equal(res.status, 200, `got ${res.status}`);
    assert.equal(res.body.link.label, 'Колебания тест');
  });

  it('duplicate link → 409', async () => {
    const res = await inject('POST', '/api/lab/sims/pendulum/links',
      { kind: 'textbook', ref_id: tbSlug }, adminToken);
    assert.equal(res.status, 409, `got ${res.status}`);
  });

  it('validation: bad kind → 400', async () => {
    const res = await inject('POST', '/api/lab/sims/pendulum/links',
      { kind: 'nope', ref_id: 'x' }, adminToken);
    assert.equal(res.status, 400);
  });

  it('validation: missing ref_id → 400', async () => {
    const res = await inject('POST', '/api/lab/sims/pendulum/links',
      { kind: 'textbook' }, adminToken);
    assert.equal(res.status, 400);
  });

  it('validation: unknown textbook → 404', async () => {
    const res = await inject('POST', '/api/lab/sims/pendulum/links',
      { kind: 'textbook', ref_id: 'ghost-book' }, adminToken);
    assert.equal(res.status, 404);
  });

  it('validation: unknown topic → 404', async () => {
    const res = await inject('POST', '/api/lab/sims/pendulum/links',
      { kind: 'topic', ref_id: '999999' }, adminToken);
    assert.equal(res.status, 404);
  });

  it('POST link to unknown sim → 404', async () => {
    const res = await inject('POST', '/api/lab/sims/ghostsim/links',
      { kind: 'textbook', ref_id: tbSlug }, adminToken);
    assert.equal(res.status, 404);
  });

  it('reverse lookup: GET /links?kind=textbook&ref_id= returns linked enabled sims', async () => {
    const res = await inject('GET',
      `/api/lab/links?kind=textbook&ref_id=${encodeURIComponent(tbSlug)}`, null, studentToken);
    assert.equal(res.status, 200, `got ${res.status}`);
    assert.ok(res.body.sims.some(s => s.id === 'pendulum'), 'pendulum in reverse lookup');
  });

  it('reverse lookup excludes disabled sims', async () => {
    await inject('PATCH', '/api/lab/sims/pendulum', { enabled: false }, adminToken);
    const res = await inject('GET',
      `/api/lab/links?kind=textbook&ref_id=${encodeURIComponent(tbSlug)}`, null, studentToken);
    assert.ok(!res.body.sims.some(s => s.id === 'pendulum'), 'disabled pendulum excluded');
    await inject('PATCH', '/api/lab/sims/pendulum', { enabled: true }, adminToken); // restore
  });

  it('batch reverse lookup: GET /links/all?kind=textbook groups by ref_id', async () => {
    const res = await inject('GET', '/api/lab/links/all?kind=textbook', null, studentToken);
    assert.equal(res.status, 200, `got ${res.status}`);
    assert.ok(res.body.byRef, 'byRef present');
    assert.ok(Array.isArray(res.body.byRef[tbSlug]), `byRef[${tbSlug}] is array`);
    assert.ok(res.body.byRef[tbSlug].some(s => s.id === 'pendulum'), 'pendulum grouped under tbSlug');
  });

  it('batch reverse lookup: bad kind → 400', async () => {
    const res = await inject('GET', '/api/lab/links/all?kind=nope', null, studentToken);
    assert.equal(res.status, 400);
  });

  it('batch reverse lookup requires auth (401)', async () => {
    const res = await inject('GET', '/api/lab/links/all?kind=textbook', null, null);
    assert.equal(res.status, 401);
  });

  it('reverse lookup: bad kind → 400', async () => {
    const res = await inject('GET', '/api/lab/links?kind=nope&ref_id=x', null, studentToken);
    assert.equal(res.status, 400);
  });

  it('admin can delete a link; related reflects removal', async () => {
    // find the textbook link id
    const rel = await inject('GET', '/api/lab/sims/pendulum/related', null, adminToken);
    const linkId = rel.body.links.textbook[0].id;
    const del = await inject('DELETE', `/api/lab/sims/pendulum/links/${linkId}`, null, adminToken);
    assert.equal(del.status, 200, `got ${del.status}`);
    const rel2 = await inject('GET', '/api/lab/sims/pendulum/related', null, adminToken);
    assert.equal(rel2.body.links.textbook.length, 0, 'textbook link gone');
  });

  it('delete unknown link → 404', async () => {
    const res = await inject('DELETE', '/api/lab/sims/pendulum/links/999999', null, adminToken);
    assert.equal(res.status, 404);
  });

  it('delete is admin-only (student → 403)', async () => {
    const res = await inject('DELETE', '/api/lab/sims/pendulum/links/1', null, studentToken);
    assert.equal(res.status, 403);
  });
});
