/**
 * Regression guard for `console_session` JWT parity.
 *
 * A gateway that forwards an interactive WebSocket (host console or container
 * exec) to a remote node calls the remote's `POST /api/system/console-token`
 * endpoint and forwards the returned token in an `Authorization: Bearer`
 * header during the upgrade. Meanwhile the HTTP route `/api/system/console-
 * token` is what the gateway calls to mint that same token on its own remote.
 * Both mint calls go through `helpers/consoleSession.ts::mintConsoleSession`.
 *
 * This test guards against future drift between the HTTP route and the
 * helper: if somebody changes the route to mint a different claim shape,
 * the remote's upgrade handler would reject one set of tokens and the
 * product would silently lose remote-console support.
 */
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import request from 'supertest';
import jwt from 'jsonwebtoken';
import { setupTestDb, cleanupTestDb, TEST_JWT_SECRET, loginAsTestAdmin } from './helpers/setupTestDb';
import { mintConsoleSession } from '../helpers/consoleSession';
import { generateApiToken } from '../utils/apiTokenFormat';

describe('console_session token parity (HTTP route vs mint helper)', () => {
  let tmpDir: string;
  let app: import('express').Express;
  let adminCookie: string;

  beforeAll(async () => {
    tmpDir = await setupTestDb();
    ({ app } = await import('../index'));
    adminCookie = await loginAsTestAdmin(app);

    // POST /api/system/console-token is admin-gated (available on every tier).
    // Seed an active license only so other suites that share LicenseService
    // state stay stable if they expect paid.
    const { DatabaseService } = await import('../services/DatabaseService');
    DatabaseService.getInstance().setSystemState('license_status', 'active');
  });

  afterAll(() => {
    cleanupTestDb(tmpDir);
  });

  it('POST /api/system/console-token produces a token with the same shape as mintConsoleSession()', async () => {
    const directToken = mintConsoleSession({ path: 'host-console', actingAs: 'testadmin' });
    const directDecoded = jwt.verify(directToken, TEST_JWT_SECRET) as Record<string, unknown>;

    const res = await request(app)
      .post('/api/system/console-token')
      .set('Cookie', adminCookie)
      .send({ path: 'host-console' });
    expect(res.status).toBe(200);
    expect(typeof res.body.token).toBe('string');

    const routeDecoded = jwt.verify(res.body.token, TEST_JWT_SECRET) as Record<string, unknown>;

    // Identical scope so the remote's upgrade handler treats both the same.
    expect(routeDecoded.scope).toBe('console_session');
    expect(directDecoded.scope).toBe('console_session');
    expect(routeDecoded.path).toBe('host-console');
    expect(directDecoded.path).toBe('host-console');
    expect(typeof routeDecoded.jti).toBe('string');
    expect(typeof directDecoded.jti).toBe('string');
    expect(routeDecoded.acting_as).toBe('testadmin');
    expect(directDecoded.acting_as).toBe('testadmin');

    // Same claim keys: if somebody adds or drops a claim on one path but not
    // the other, remote upgrade behavior will diverge.
    expect(Object.keys(routeDecoded).sort()).toEqual(Object.keys(directDecoded).sort());

    // Same short TTL (60 seconds, plus or minus a second for test scheduling).
    const directTtl = (directDecoded.exp as number) - (directDecoded.iat as number);
    const routeTtl = (routeDecoded.exp as number) - (routeDecoded.iat as number);
    expect(directTtl).toBe(60);
    expect(routeTtl).toBe(60);
  });

  it('rejects non-admin callers of POST /api/system/console-token', async () => {
    // Build a valid session JWT for a non-admin user and confirm the route
    // is still admin-gated (regression: easy to drop the requireAdmin check
    // when tier gates are refactored).
    const viewerToken = jwt.sign({ username: 'nobody-exists' }, TEST_JWT_SECRET, { expiresIn: '1m' });
    const res = await request(app)
      .post('/api/system/console-token')
      .set('Authorization', `Bearer ${viewerToken}`);
    // Either 401 (user not found) or 403 (role check) is acceptable; the
    // critical invariant is "not 200 for a non-admin".
    expect(res.status).not.toBe(200);
    expect([401, 403]).toContain(res.status);
  });

  it('rejects API-token callers of POST /api/system/console-token (rejectApiTokenScope)', async () => {
    // Mint a JWT that claims the api_token scope but is not backed by a real
    // row in the database. authMiddleware accepts the sen_sk_ format and
    // rejects at the row-lookup step before the route handler runs, which
    // is the behavior we want: an API token should never be allowed to mint
    // a console session.
    const fakeApiToken = generateApiToken();
    const res = await request(app)
      .post('/api/system/console-token')
      .set('Authorization', `Bearer ${fakeApiToken}`);
    expect(res.status).toBe(401);
  });
});
