// Integration test for per-project RBAC: the grant-management API on the
// projects router + scoped enforcement on GET /projects and GET /projects/:id.
//
// NOTE: the routers use the singleton pool (src/db/pool.js), which reads
// DATABASE_URL. We point DATABASE_URL at the throwaway TEST_DATABASE_URL and
// seed through that same singleton pool so the router and the test share one
// database. Skips cleanly when TEST_DATABASE_URL is unset.
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';

// Build an app that injects a chosen user as req.user (simulating requireAuth),
// then mounts the real projects router with the same admin gate index.js uses.
async function appWithProjects(user) {
  process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
  process.env.AUTH_ENABLED = 'true';
  const { default: projectsRouter } = await import('../../src/routes/projects.js?v=' + Date.now());
  const app = express();
  app.use(express.json());
  app.use((req, _res, next) => { req.user = user; next(); });
  app.use('/api/v1/projects', projectsRouter);
  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 seedBaseline(pool) {
  const alpha = (await pool.query(`INSERT INTO projects (name, s3_prefix) VALUES ('Alpha','alpha') RETURNING id`)).rows[0].id;
  const beta  = (await pool.query(`INSERT INTO projects (name, s3_prefix) VALUES ('Beta','beta') RETURNING id`)).rows[0].id;
  const admin = { id: (await pool.query(`INSERT INTO users (username, password_hash, role) VALUES ('adm','x','admin') RETURNING id`)).rows[0].id, role: 'admin' };
  const scoped = { id: (await pool.query(`INSERT INTO users (username, password_hash, role) VALUES ('vu','x','viewer') RETURNING id`)).rows[0].id, role: 'viewer' };
  return { alpha, beta, admin, scoped };
}

test('admin grants a project, scoped user then sees only it; revoke removes access', { skip: SKIP }, async () => {
  const pool = await setupTestDb();
  process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
  try {
    const { alpha, beta, admin, scoped } = await seedBaseline(pool);

    // Scoped viewer initially sees nothing.
    let s = await appWithProjects(scoped);
    let list = await (await fetch(s.baseUrl + '/api/v1/projects')).json();
    assert.equal(list.length, 0, 'scoped user should see no projects before any grant');
    // And cannot read Alpha directly.
    let direct = await fetch(s.baseUrl + '/api/v1/projects/' + alpha);
    assert.equal(direct.status, 403);
    await s.close();

    // Admin grants the scoped user 'view' on Alpha.
    const a = await appWithProjects(admin);
    const grant = await fetch(a.baseUrl + '/api/v1/projects/' + alpha + '/access', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ subject_type: 'user', subject_id: scoped.id, level: 'view' }),
    });
    assert.equal(grant.status, 201);
    const grantList = await (await fetch(a.baseUrl + '/api/v1/projects/' + alpha + '/access')).json();
    assert.equal(grantList.length, 1);
    assert.equal(grantList[0].subject_id, scoped.id);
    await a.close();

    // Scoped viewer now sees exactly Alpha (not Beta), can GET it, cannot PATCH (view-only).
    s = await appWithProjects(scoped);
    list = await (await fetch(s.baseUrl + '/api/v1/projects')).json();
    assert.deepEqual(list.map(p => p.id), [alpha]);
    assert.equal((await fetch(s.baseUrl + '/api/v1/projects/' + alpha)).status, 200);
    assert.equal((await fetch(s.baseUrl + '/api/v1/projects/' + beta)).status, 403);
    const patch = await fetch(s.baseUrl + '/api/v1/projects/' + alpha, {
      method: 'PATCH', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ description: 'hacked' }),
    });
    assert.equal(patch.status, 403, 'view-level grant must not allow edit');
    await s.close();

    // Admin revokes; scoped viewer goes back to seeing nothing.
    const a2 = await appWithProjects(admin);
    const del = await fetch(a2.baseUrl + '/api/v1/projects/' + alpha + '/access/user/' + scoped.id, { method: 'DELETE' });
    assert.equal(del.status, 204);
    await a2.close();

    s = await appWithProjects(scoped);
    list = await (await fetch(s.baseUrl + '/api/v1/projects')).json();
    assert.equal(list.length, 0);
    await s.close();
  } finally { await pool.end(); }
});

test('non-admin cannot reach the grant-management API', { skip: SKIP }, async () => {
  const pool = await setupTestDb();
  process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
  try {
    const { alpha, scoped } = await seedBaseline(pool);
    const s = await appWithProjects(scoped);
    // requireAdmin sits on the access sub-routes; a viewer is 403.
    const r = await fetch(s.baseUrl + '/api/v1/projects/' + alpha + '/access');
    assert.equal(r.status, 403);
    await s.close();
  } finally { await pool.end(); }
});

test('edit-level grant allows PATCH', { skip: SKIP }, async () => {
  const pool = await setupTestDb();
  process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
  try {
    const { alpha, admin, scoped } = await seedBaseline(pool);
    const a = await appWithProjects(admin);
    await fetch(a.baseUrl + '/api/v1/projects/' + alpha + '/access', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ subject_type: 'user', subject_id: scoped.id, level: 'edit' }),
    });
    await a.close();

    const s = await appWithProjects(scoped);
    const patch = await fetch(s.baseUrl + '/api/v1/projects/' + alpha, {
      method: 'PATCH', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ description: 'updated by editor' }),
    });
    assert.equal(patch.status, 200);
    await s.close();
  } finally { await pool.end(); }
});
