/**
 * app-harness-api.test.ts — TDD for appHarnessAuthMiddleware (Task 3)
 *
 * 3 required cases:
 *  1. Valid token + matching space → req.user is synthetic viewer for that userId
 *  2. Token scope spaceId ≠ requested spaceId → 403
 *  3. No token at all → next() with req.user untouched
 *
 * Additional security cases:
 *  4. Present but invalid/garbage token → 403 (fail-closed)
 *  5. Valid token but wrong space URL → 403
 */
import { describe, it, expect, beforeEach } from 'vitest';
import express, { type Request, type Response } from 'express';
import supertest from 'supertest';
import { mintAppHarnessToken, verifyAppHarnessToken } from './app-harness-token.js';
import { appHarnessAuthMiddleware } from './app-harness-api.js';

// Helper: build a minimal Express app that exercises the middleware
function buildApp() {
  const app = express();
  app.use(appHarnessAuthMiddleware());

  // Echo route: respond with serialized req.user (or null if unset)
  app.get('/api/local/spaces/:id/files', (req: Request, res: Response) => {
    res.json({ user: (req as any).user ?? null });
  });

  // Generic catch-all to confirm non-space routes pass through
  app.get('/api/local/tasks', (req: Request, res: Response) => {
    res.json({ user: (req as any).user ?? null });
  });

  return app;
}

describe('appHarnessAuthMiddleware', () => {
  const SPACE_ID = 'space-abc-123';
  const USER_ID = 'user-xyz-456';

  // ── Case 1: valid token + matching space ───────────────────────────────────
  it('injects synthetic viewer when token scope matches requested space', async () => {
    const app = buildApp();
    const token = mintAppHarnessToken({ userId: USER_ID, spaceId: SPACE_ID });

    const res = await supertest(app)
      .get(`/api/local/spaces/${SPACE_ID}/files`)
      .set('x-app-token', token);

    expect(res.status).toBe(200);
    const user = res.body.user;
    expect(user).not.toBeNull();
    expect(user.id).toBe(USER_ID);
    expect(user.role).toBe('user');          // NOT elevated to admin
    expect(user.status).toBe('active');
    expect(Array.isArray(user.orgIds)).toBe(true);
  });

  // Also works via query param __appToken
  it('injects viewer when token passed as __appToken query param', async () => {
    const app = buildApp();
    const token = mintAppHarnessToken({ userId: USER_ID, spaceId: SPACE_ID });

    const res = await supertest(app)
      .get(`/api/local/spaces/${SPACE_ID}/files?__appToken=${token}`);

    expect(res.status).toBe(200);
    expect(res.body.user?.id).toBe(USER_ID);
  });

  // ── Case 2: space mismatch → 403 ──────────────────────────────────────────
  it('returns 403 when token scope space does not match requested space', async () => {
    const app = buildApp();
    const token = mintAppHarnessToken({ userId: USER_ID, spaceId: 'space-OTHER' });

    const res = await supertest(app)
      .get(`/api/local/spaces/${SPACE_ID}/files`)
      .set('x-app-token', token);

    expect(res.status).toBe(403);
    expect((res.body as any).user).toBeUndefined(); // no viewer injected
  });

  // ── Case 3: no token → pass through untouched ─────────────────────────────
  it('passes through with req.user untouched when no token is present', async () => {
    const app = buildApp();

    const res = await supertest(app)
      .get(`/api/local/spaces/${SPACE_ID}/files`);

    // Route handles the request normally (200), req.user is null (not set by middleware)
    expect(res.status).toBe(200);
    expect(res.body.user).toBeNull();
  });

  // ── Security: garbage token → 403, not silent pass-through ────────────────
  it('returns 403 for an invalid/garbage token (fail-closed)', async () => {
    const app = buildApp();

    const res = await supertest(app)
      .get(`/api/local/spaces/${SPACE_ID}/files`)
      .set('x-app-token', 'garbage-invalid-token-abc');

    expect(res.status).toBe(403);
  });

  // ── Non-space routes are unaffected by the middleware ─────────────────────
  it('ignores token on non-space routes (no space id in path)', async () => {
    const app = buildApp();
    const token = mintAppHarnessToken({ userId: USER_ID, spaceId: SPACE_ID });

    const res = await supertest(app)
      .get('/api/local/tasks')
      .set('x-app-token', token);

    // Token present but path has no space id → middleware treats it as no-token route
    // and either passes through or 403 — but must NOT crash. The important thing:
    // if it passes through, user stays null.
    // Per spec: cannot extract space id → fail-closed (403)
    expect(res.status).toBe(403);
  });

  // ── Security: expired token → 403, fail-closed at middleware ───────────────
  it('returns 403 for a present but expired token (fail-closed)', async () => {
    const app = buildApp();
    // Mint with 1ms TTL
    const token = mintAppHarnessToken({ userId: USER_ID, spaceId: SPACE_ID }, 1);

    // Verify the token is fresh and valid before we start the test
    expect(verifyAppHarnessToken(token)).not.toBeNull();

    // Wait past the 1ms TTL
    const until = Date.now() + 5;
    while (Date.now() < until) { /* spin briefly */ }

    // Now the token should be expired
    expect(verifyAppHarnessToken(token)).toBeNull();

    // Middleware should reject it with 403 (fail-closed)
    const res = await supertest(app)
      .get(`/api/local/spaces/${SPACE_ID}/files`)
      .set('x-app-token', token);

    expect(res.status).toBe(403);
    expect((res.body as any).user).toBeUndefined(); // no viewer injected
  });
});
