// RBAC coverage for sequences: list/create assert on the query/body project,
// /:id asserts view, mutators assert edit. Skips without TEST_DATABASE_URL.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import express from 'express';
import { isTestDbConfigured, setupTestDb } from '../helpers/setup-db.js';

const SKIP = !isTestDbConfigured() && 'TEST_DATABASE_URL not set';

async function appWithSequences(user) {
  process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
  process.env.AUTH_ENABLED = 'true';
  const { default: sequencesRouter } = await import('../../src/routes/sequences.js?v=' + Date.now());
  const app = express();
  app.use(express.json());
  app.use((req, _res, next) => { req.user = user; next(); });
  app.use('/api/v1/sequences', sequencesRouter);
  return new Promise(r => {
    const srv = app.listen(0, '127.0.0.1', () =>
      r({ baseUrl: 'http://127.0.0.1:' + srv.address().port, close: () => new Promise(rs => srv.close(rs)) }));
  });
}

async function seed(pool) {
  const proj = async (n) => (await pool.query(`INSERT INTO projects (name, s3_prefix) VALUES ($1,$1) RETURNING id`, [n])).rows[0].id;
  const alpha = await proj('Alpha');
  const beta  = await proj('Beta');
  const seqB = (await pool.query(`INSERT INTO sequences (project_id, name) VALUES ($1,'B seq') RETURNING id`, [beta])).rows[0].id;
  const viewer = { id: (await pool.query(`INSERT INTO users (username, password_hash, role) VALUES ('vu','x','viewer') RETURNING id`)).rows[0].id, role: 'viewer' };
  const editor = { id: (await pool.query(`INSERT INTO users (username, password_hash, role) VALUES ('ed','x','editor') RETURNING id`)).rows[0].id, role: 'editor' };
  await pool.query(`INSERT INTO project_access (project_id, subject_type, subject_id, level) VALUES ($1,'user',$2,'view')`, [alpha, viewer.id]);
  await pool.query(`INSERT INTO project_access (project_id, subject_type, subject_id, level) VALUES ($1,'user',$2,'edit')`, [alpha, editor.id]);
  return { alpha, beta, seqB, viewer, editor };
}

const asset = (pool, pid, name) => pool.query(
  `INSERT INTO assets (project_id, filename, display_name, media_type, status) VALUES ($1,$2,$2,'video','ready') RETURNING id`,
  [pid, name]).then(r => r.rows[0].id);

test('GET /sequences?project_id 403s on an ungranted project', { skip: SKIP }, async () => {
  const pool = await setupTestDb();
  process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
  try {
    const { alpha, beta, viewer } = await seed(pool);
    const s = await appWithSequences(viewer);
    assert.equal((await fetch(s.baseUrl + '/api/v1/sequences?project_id=' + alpha)).status, 200);
    assert.equal((await fetch(s.baseUrl + '/api/v1/sequences?project_id=' + beta)).status, 403);
    await s.close();
  } finally { await pool.end(); }
});

test('POST /sequences requires edit; viewer denied, editor allowed; /:id view enforced', { skip: SKIP }, async () => {
  const pool = await setupTestDb();
  process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
  try {
    const { alpha, seqB, viewer, editor } = await seed(pool);

    // viewer (view-only on Alpha) cannot create.
    let s = await appWithSequences(viewer);
    let r = await fetch(s.baseUrl + '/api/v1/sequences', {
      method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project_id: alpha, name: 'X' }) });
    assert.equal(r.status, 403);
    // viewer cannot read a sequence in an ungranted project (Beta).
    assert.equal((await fetch(s.baseUrl + '/api/v1/sequences/' + seqB)).status, 403);
    await s.close();

    // editor can create in Alpha and then PUT it.
    let e = await appWithSequences(editor);
    r = await fetch(e.baseUrl + '/api/v1/sequences', {
      method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project_id: alpha, name: 'Mine' }) });
    assert.equal(r.status, 201);
    const seqId = (await r.json()).id;
    const put = await fetch(e.baseUrl + '/api/v1/sequences/' + seqId, {
      method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Renamed' }) });
    assert.equal(put.status, 200);
    await e.close();
  } finally { await pool.end(); }
});

test('PUT /:id/clips rejects assets from outside the sequence project (cross-project leak guard)', { skip: SKIP }, async () => {
  const pool = await setupTestDb();
  process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
  try {
    const { alpha, beta, editor } = await seed(pool);
    const seqA = (await pool.query(`INSERT INTO sequences (project_id, name) VALUES ($1,'A seq') RETURNING id`, [alpha])).rows[0].id;
    const assetA = await asset(pool, alpha, 'a-clip');
    const assetB = await asset(pool, beta,  'b-clip');  // editor has NO access to project Beta

    const e = await appWithSequences(editor);
    const clip = (aid) => ({ asset_id: aid, track: 0, timeline_in_frames: 0, timeline_out_frames: 10, source_in_frames: 0, source_out_frames: 10 });

    // Same-project asset is accepted.
    let r = await fetch(e.baseUrl + '/api/v1/sequences/' + seqA + '/clips', {
      method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify([clip(assetA)]) });
    assert.equal(r.status, 200);

    // Splicing in a Beta asset must be rejected — it would leak B's media via GET /:id.
    r = await fetch(e.baseUrl + '/api/v1/sequences/' + seqA + '/clips', {
      method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify([clip(assetA), clip(assetB)]) });
    assert.equal(r.status, 400, 'foreign-project asset must be rejected');
    await e.close();
  } finally { await pool.end(); }
});
