/**
 * app-harness-e2e.test.ts — Integration test (Approach B: in-process)
 *
 * Proves the workspace-app token+file-API round-trip end-to-end:
 *   1. mint a real token  (mintAppHarnessToken)
 *   2. GET  /api/local/spaces/:id/files/content?path=output/note.md  — via __appToken query param
 *      → asserts: 200, seeded text returned
 *   3. POST /api/local/spaces/:id/files/write  — via x-app-token header
 *      → asserts: 200, bytes reported
 *   4. disk assertion: readFileSync(abs) === edited text
 *   5. wrong-space token → 403  (containment proof)
 *
 * Gap explicitly noted: this does NOT drive a real iframe/browser.  The
 * AppRunner postMessage bridge rendering layer is not exercised here — only
 * the token-auth middleware + file API routes are proven.  See task-10-report.md
 * for manual steps to run the full Playwright round-trip.
 *
 * Approach B was chosen because:
 *   - The server requires Ollama/LLM config that is not present in this sandbox.
 *   - Playwright Chromium launch requires bwrap which is disabled (CONTAINER=1
 *     workaround only applies to the project's own BrowseWeb tool, not bare vitest).
 *   - The token-auth middleware and file-API are fully independent of the LLM;
 *     supertest exercises the real Express stack against a real SQLite DB and
 *     real temp files — equivalent fidelity for the components under test.
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import express from 'express';
import request from 'supertest';
import {
  mkdtempSync,
  rmSync,
  mkdirSync,
  writeFileSync,
  readFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { randomUUID } from 'node:crypto';

import { Repository } from '../db/repository.js';
import { spaceFilesDir } from '../spaces/paths.js';
import { createSpaceApi } from '../bridge/space-api.js';
import { appHarnessAuthMiddleware } from '../bridge/app-harness-api.js';
import { mintAppHarnessToken, revokeAppHarnessToken, verifyAppHarnessToken } from '../bridge/app-harness-token.js';

// ─── helpers ──────────────────────────────────────────────────────────────────

/**
 * Build a minimal Express app that wires the middleware chain identically
 * to the production server:
 *   appHarnessAuthMiddleware  (populates req.user from token)
 *   → createSpaceApi router   (reads req.user via viewerOf)
 *
 * authActive=false: in no-auth mode viewerOf() falls back to the synthetic
 * 'local' admin when req.user is unset.  Setting authActive=true means
 * viewerOf() uses req.user exactly — so the harness middleware's synthetic
 * viewer is the sole principal, matching production harness behaviour.
 */
function buildApp(repo: Repository, worktreeDir: string) {
  const app = express();
  // Must be before the space router so req.user is populated first.
  app.use(appHarnessAuthMiddleware());
  app.use('/api/local/spaces', createSpaceApi({
    repo,
    dataRoot: worktreeDir,   // dataRoot only matters for non-file routes
    worktreeDir,
    authActive: true,        // force viewerOf() to honour req.user
  }));
  return app;
}

// ─── test suite ───────────────────────────────────────────────────────────────

describe('workspace-app E2E: token-auth + file API round-trip', () => {
  let dir: string;
  let worktreeDir: string;
  let repo: Repository;
  let spaceId: string;
  let ownerId: string;
  let notePath: string;      // absolute path on disk for assertions
  let token: string;

  const NOTE_SEED    = '# My Note\n\noriginal content';
  const NOTE_EDITED  = '# My Note\n\nedited by workspace-app E2E test';

  beforeEach(async () => {
    dir        = mkdtempSync(join(tmpdir(), 'ws-app-e2e-'));
    worktreeDir = join(dir, 'wt');
    ownerId    = 'user-e2e-' + randomUUID();

    // Real SQLite database — same path used by production.
    repo = new Repository(join(dir, `${randomUUID()}.db`));

    // Create a real space owned by ownerId.
    const space = await repo.createSpace({
      kind: 'case',
      title: 'E2E Test Space',
      ownerId,
    });
    spaceId = space.id;

    // Seed output/note.md in the space's files dir.
    const filesRoot = spaceFilesDir(worktreeDir, spaceId);
    mkdirSync(join(filesRoot, 'output'), { recursive: true });
    mkdirSync(join(filesRoot, 'apps', 'note-editor'), { recursive: true });
    notePath = join(filesRoot, 'output', 'note.md');
    writeFileSync(notePath, NOTE_SEED);

    // Place a minimal note-editor app HTML (proves apps/ dir structure).
    writeFileSync(
      join(filesRoot, 'apps', 'note-editor', 'index.html'),
      '<!DOCTYPE html><html><body>note-editor stub</body></html>',
    );

    // Mint a real harness token scoped to this user + space.
    token = mintAppHarnessToken({ userId: ownerId, spaceId });
  });

  afterEach(() => {
    revokeAppHarnessToken(token);
    rmSync(dir, { recursive: true, force: true });
  });

  // ── 1. Read seeded file via __appToken query param ─────────────────────────
  it('GET files/content via __appToken returns seeded note.md', async () => {
    const app = buildApp(repo, worktreeDir);

    const res = await request(app)
      .get(`/api/local/spaces/${spaceId}/files/content`)
      .query({ path: 'output/note.md', __appToken: token });

    expect(res.status).toBe(200);
    expect(res.text).toBe(NOTE_SEED);
  });

  // ── 2. Write via x-app-token header rewrites disk ─────────────────────────
  it('POST files/write via x-app-token header rewrites output/note.md on disk', async () => {
    const app = buildApp(repo, worktreeDir);

    const res = await request(app)
      .post(`/api/local/spaces/${spaceId}/files/write`)
      .set('x-app-token', token)
      .send({ path: 'output/note.md', content: NOTE_EDITED });

    expect(res.status).toBe(200);
    expect(res.body.bytes).toBe(Buffer.byteLength(NOTE_EDITED));

    // ── KEY ASSERTION: disk state changed ───────────────────────────────────
    const diskContent = readFileSync(notePath, 'utf-8');
    expect(diskContent).toBe(NOTE_EDITED);
  });

  // ── 3. Full round-trip: read → edit → write → read-back ──────────────────
  it('full round-trip: read seeded → write edited → GET returns new content', async () => {
    const app = buildApp(repo, worktreeDir);

    // Step A — read original
    const readRes = await request(app)
      .get(`/api/local/spaces/${spaceId}/files/content`)
      .query({ path: 'output/note.md', __appToken: token });
    expect(readRes.status).toBe(200);
    expect(readRes.text).toBe(NOTE_SEED);

    // Step B — write edited
    const writeRes = await request(app)
      .post(`/api/local/spaces/${spaceId}/files/write`)
      .set('x-app-token', token)
      .send({ path: 'output/note.md', content: NOTE_EDITED });
    expect(writeRes.status).toBe(200);

    // Step C — disk assertion
    expect(readFileSync(notePath, 'utf-8')).toBe(NOTE_EDITED);

    // Step D — read-back via API to confirm the chain is coherent
    const readBackRes = await request(app)
      .get(`/api/local/spaces/${spaceId}/files/content`)
      .query({ path: 'output/note.md', __appToken: token });
    expect(readBackRes.status).toBe(200);
    expect(readBackRes.text).toBe(NOTE_EDITED);
  });

  // ── 4. Wrong-space token → 403 (containment) ─────────────────────────────
  it('wrong-space token is rejected with 403 (containment)', async () => {
    const app = buildApp(repo, worktreeDir);

    // Mint a token scoped to a *different* space id.
    const otherSpaceId = 'space-other-' + randomUUID();
    const wrongToken = mintAppHarnessToken({ userId: ownerId, spaceId: otherSpaceId });

    try {
      const res = await request(app)
        .get(`/api/local/spaces/${spaceId}/files/content`)
        .query({ path: 'output/note.md', __appToken: wrongToken });

      expect(res.status).toBe(403);
    } finally {
      revokeAppHarnessToken(wrongToken);
    }
  });

  // ── 5. Write outside output/ is rejected ─────────────────────────────────
  it('write to a path outside apps/ or output/ is rejected with 403', async () => {
    const app = buildApp(repo, worktreeDir);

    const res = await request(app)
      .post(`/api/local/spaces/${spaceId}/files/write`)
      .set('x-app-token', token)
      .send({ path: 'AGENTS.md', content: 'malicious override' });

    expect(res.status).toBe(403);
    // Disk must be untouched (AGENTS.md should not exist under filesRoot).
    const filesRoot = spaceFilesDir(worktreeDir, spaceId);
    expect(() => readFileSync(join(filesRoot, 'AGENTS.md'))).toThrow();
  });

  // ── 6. Expired token is rejected ─────────────────────────────────────────
  it('expired token is rejected with 403', { timeout: 10_000 }, async () => {
    const app = buildApp(repo, worktreeDir);

    const expiredToken = mintAppHarnessToken(
      { userId: ownerId, spaceId },
      1, // 1 ms TTL → expires immediately
    );
    // Spin until TTL expires (matches pattern in app-harness-token.test.ts).
    await new Promise<void>((resolve) => {
      const poll = () => {
        if (verifyAppHarnessToken(expiredToken) === null) { resolve(); } else { setTimeout(poll, 5); }
      };
      setTimeout(poll, 5);
    });

    const res = await request(app)
      .get(`/api/local/spaces/${spaceId}/files/content`)
      .query({ path: 'output/note.md', __appToken: expiredToken });

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