/**
 * reflection-api.space-authz.test.ts
 *
 * REFL-056 — space-aware authorization NEGATIVE path.
 *
 * resolveStore() gates two ways depending on the operation:
 *   - read  (GET /history, GET /history/:id)  → forEdit=false: only requires the
 *     space be VISIBLE to the viewer (membership OR ownership).
 *   - write (POST /history/:id/revert)         → forEdit=true: additionally requires
 *     canEditInSpace (admin / root owner / member role owner|editor). A read-only
 *     'viewer' member must be REJECTED with 404 (no existence leak).
 *
 * The existing reflection-api.test.ts covers:
 *   - non-MEMBER → 404 on history (space not visible at all).
 *   - editor/owner member → happy path.
 * It does NOT cover the distinct cross-principal gap: a member WITH view rights
 * but WITHOUT edit rights ('viewer' role) hitting revert → 404. That is the
 * confused-deputy class a whole-branch review catches but per-unit tests miss.
 *
 * Fixture style copied from reflection-api.test.ts (real Repository + temp FS
 * snapshots, supertest, x-test-user-id header → synthetic req.user). authActive
 * is TRUE here so membership/role gating actually runs (no-auth = admin-all).
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import express from 'express';
import request from 'supertest';
import { mkdtempSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { Repository } from '../db/repository.js';
import { writeSnapshot, type WriteSnapshotMeta } from '../engine/reflection/snapshot.js';
import { createReflectionApi } from './reflection-api.js';

function makeMeta(spaceId: string, overrides: Partial<WriteSnapshotMeta> = {}): WriteSnapshotMeta {
  return {
    originalJobId: 'j-authz-001',
    userId: spaceId, // snapshot leaf = space id (Space-as-Principal store)
    pieceName: 'chat',
    outcome: 'applied',
    reasoning: 'Space-scoped learning.',
    modelUsed: 'qwen2.5:3b',
    tokensIn: 100,
    tokensOut: 20,
    ratingAtTime: null,
    memoryChanges: 1,
    pieceEdited: false,
    ...overrides,
  };
}

// dataDir points at <tmp>/users so the spaces store is the sibling <tmp>/spaces,
// matching how resolveSpaceFolder derives the spaces tree from the users root.
function buildApp(usersDir: string, repo: Repository) {
  const app = express();
  app.use(express.json());
  app.use((req, _res, next) => {
    const testUserId = req.headers['x-test-user-id'] as string | undefined;
    if (testUserId) {
      (req as any).user = {
        id: testUserId,
        role: testUserId === 'admin' ? 'admin' : 'user',
        orgIds: [],
      };
    }
    next();
  });
  // authActive:true → membership + canEditInSpace gating is exercised.
  app.use('/api/local/reflection', createReflectionApi({ dataDir: usersDir, repo, authActive: true }));
  return app;
}

describe('reflection-api space authz (REFL-056)', () => {
  let tmpDir: string;
  let usersDir: string;
  let spacesDir: string;
  let repo: Repository;
  let app: express.Application;

  beforeEach(() => {
    tmpDir = mkdtempSync(join(tmpdir(), 'reflect-authz-'));
    usersDir = join(tmpDir, 'users');
    spacesDir = join(tmpDir, 'spaces');
    repo = new Repository(join(tmpDir, 'test.db'));
    app = buildApp(usersDir, repo);
  });

  afterEach(() => {
    repo.close();
    rmSync(tmpDir, { recursive: true, force: true });
  });

  /**
   * Seed: a private case space owned by `owner`, with `member` added as a
   * read-only 'viewer'. One snapshot written under the space store.
   * Returns the principal/member ids + snapshotId.
   */
  async function seedSpaceWithViewer() {
    // addSpaceMember has an FK to users(id) → create real user rows.
    const owner = repo.createUser({ email: 'owner@x.com', name: 'owner', role: 'user', status: 'active' });
    const member = repo.createUser({ email: 'member@x.com', name: 'member', role: 'user', status: 'active' });
    const space = await repo.createSpace({ kind: 'case', title: 'Case', ownerId: owner.id, visibility: 'private' });
    // Read-only role — can SEE the space, cannot edit/revert.
    await repo.addSpaceMember({ spaceId: space.id, userId: member.id, role: 'viewer', invitedBy: owner.id });

    const { snapshotId } = await writeSnapshot(
      { dataDir: spacesDir }, {}, {},
      makeMeta(space.id),
      undefined, undefined, new Date('2026-05-12T10:00:00Z'),
    );
    return { owner, member, space, snapshotId };
  }

  it('a read-only viewer member CAN list space history (read is membership-gated only)', async () => {
    const { member, space } = await seedSpaceWithViewer();
    const res = await request(app)
      .get(`/api/local/reflection/history?spaceId=${space.id}`)
      .set('x-test-user-id', member.id);
    // Read path: forEdit=false → only visibility required. Viewer is a member → 200.
    expect(res.status).toBe(200);
    expect(res.body.items.length).toBe(1);
  });

  it('a read-only viewer member CANNOT revert a space snapshot → 404 (no leak)', async () => {
    const { member, space, snapshotId } = await seedSpaceWithViewer();
    const res = await request(app)
      .post(`/api/local/reflection/history/${snapshotId}/revert?spaceId=${space.id}`)
      .set('x-test-user-id', member.id);
    // Write path: forEdit=true → canEditInSpace('viewer') === false → resolveStore
    // returns null → 404. Must NOT 403 (no existence leak) and must NOT 200.
    expect(res.status).toBe(404);
    expect(res.body).toEqual({ error: 'not_found' });
  });

  it('the space owner (edit rights) CAN revert the same snapshot → 200', async () => {
    const { owner, space, snapshotId } = await seedSpaceWithViewer();
    const res = await request(app)
      .post(`/api/local/reflection/history/${snapshotId}/revert?spaceId=${space.id}`)
      .set('x-test-user-id', owner.id);
    expect(res.status).toBe(200);
    expect(res.body.reverted).toBe(true);
  });

  it('an editor member (edit rights) CAN revert → 200', async () => {
    const { space, snapshotId } = await seedSpaceWithViewer();
    const editor = repo.createUser({ email: 'editor@x.com', name: 'editor', role: 'user', status: 'active' });
    await repo.addSpaceMember({ spaceId: space.id, userId: editor.id, role: 'editor', invitedBy: 'owner' });
    const res = await request(app)
      .post(`/api/local/reflection/history/${snapshotId}/revert?spaceId=${space.id}`)
      .set('x-test-user-id', editor.id);
    expect(res.status).toBe(200);
    expect(res.body.reverted).toBe(true);
  });

  it('a complete non-member sees neither history nor revert → 404 both', async () => {
    const { space, snapshotId } = await seedSpaceWithViewer();
    const hist = await request(app)
      .get(`/api/local/reflection/history?spaceId=${space.id}`)
      .set('x-test-user-id', 'u-stranger');
    expect(hist.status).toBe(404);

    const revert = await request(app)
      .post(`/api/local/reflection/history/${snapshotId}/revert?spaceId=${space.id}`)
      .set('x-test-user-id', 'u-stranger');
    expect(revert.status).toBe(404);
  });
});
