/**
 * Authz + gating tests for /api/local/tasks/:taskId/package-requests(/:id/decide).
 *
 * These cover the paths that DO NOT trigger a real pip install (view gate,
 * write gate, feature gate, deny, already-decided) so the suite stays hermetic
 * — no network / no bwrap / no python3. The install/overlay mechanics are
 * unit-tested in engine/python-packages.test.ts, and the shared add path in
 * space-api.python-packages.test.ts.
 */
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { mkdtempSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { randomUUID } from 'node:crypto';
import express from 'express';
import request from 'supertest';
import { Repository, localTaskRepoName } from '../db/repository.js';
import { registerLocalTaskPackageRequestRoutes } from './local-tasks-package-requests-api.js';
import type { LocalTasksDeps } from './local-tasks-shared.js';
import type { PythonPackagesConfigShape } from '../config.js';

function asUser(u: { id: string; role: 'admin' | 'user' }): Express.User {
  return { id: u.id, role: u.role, orgIds: [] } as unknown as Express.User;
}

function makeApp(repo: Repository, currentUser: Express.User | undefined, pkgCfg?: PythonPackagesConfigShape) {
  const app = express();
  app.use((req: express.Request, _res, next) => { if (currentUser) (req as { user?: Express.User }).user = currentUser; next(); });
  const deps = {
    repo,
    userFolderRoot: tmpdir(),
    noAuthOwner: undefined,
    opts: { repo, authActive: true, getPythonPackagesConfig: () => pkgCfg },
  } as unknown as LocalTasksDeps;
  registerLocalTaskPackageRequestRoutes(app, deps);
  return app;
}

let repo: Repository;
let dir: string;

beforeEach(() => {
  dir = mkdtempSync(join(tmpdir(), 'pkgreq-api-'));
  repo = new Repository(join(dir, `${randomUUID()}.db`));
});
afterEach(() => {
  repo.close();
  rmSync(dir, { recursive: true, force: true });
});

async function seed() {
  const owner = repo.createUser({ email: 'o@e.com', name: 'O', role: 'user', status: 'active' });
  const viewer = repo.createUser({ email: 'v@e.com', name: 'V', role: 'user', status: 'active' });
  const stranger = repo.createUser({ email: 's@e.com', name: 'S', role: 'user', status: 'active' });
  // Private space so a non-member truly cannot see the task (→ 404), while a
  // viewer member can see but not edit (→ 403).
  const space = await repo.createSpace({ kind: 'case', title: 'T', ownerId: owner.id, visibility: 'private' });
  await repo.addSpaceMember({ spaceId: space.id, userId: viewer.id, role: 'viewer' });
  const task = await repo.createLocalTask({
    title: 't', body: 'b', pieceName: 'chat', ownerId: owner.id, visibility: 'private', spaceId: space.id,
  });
  const reqId = repo.recordPackageRequest({
    taskId: String(task.id), jobId: null, spaceId: space.id,
    spec: 'requests==2.32.3', normalizedName: 'requests', reason: 'http',
  });
  return { owner, viewer, stranger, space, task, reqId };
}

const ENABLED: PythonPackagesConfigShape = { enabled: true } as PythonPackagesConfigShape;
const DISABLED: PythonPackagesConfigShape = { enabled: false } as PythonPackagesConfigShape;

describe('package-requests API', () => {
  it('GET requires view permission (stranger gets 404/403)', async () => {
    const fx = await seed();
    const app = makeApp(repo, asUser(fx.stranger), ENABLED);
    const res = await request(app).get(`/api/local/tasks/${fx.task.id}/package-requests`);
    expect([403, 404]).toContain(res.status);
  });

  it('GET lists requests for the owner', async () => {
    const fx = await seed();
    const app = makeApp(repo, asUser(fx.owner), ENABLED);
    const res = await request(app).get(`/api/local/tasks/${fx.task.id}/package-requests`);
    expect(res.status).toBe(200);
    expect(res.body.packageRequests).toHaveLength(1);
    expect(res.body.packageRequests[0].spec).toBe('requests==2.32.3');
  });

  it('decide rejects a bad decision value', async () => {
    const fx = await seed();
    const app = makeApp(repo, asUser(fx.owner), ENABLED);
    const res = await request(app).post(`/api/local/tasks/${fx.task.id}/package-requests/${fx.reqId}/decide`).send({ decision: 'maybe' });
    expect(res.status).toBe(400);
  });

  it('a space viewer (editor未満) cannot decide → 403', async () => {
    const fx = await seed();
    const app = makeApp(repo, asUser(fx.viewer), ENABLED);
    const res = await request(app).post(`/api/local/tasks/${fx.task.id}/package-requests/${fx.reqId}/decide`).send({ decision: 'approve' });
    expect(res.status).toBe(403);
    // untouched
    expect(repo.getPackageRequest(fx.reqId)?.status).toBe('pending');
  });

  it('a non-member cannot decide → 404 (task not visible)', async () => {
    const fx = await seed();
    const app = makeApp(repo, asUser(fx.stranger), ENABLED);
    const res = await request(app).post(`/api/local/tasks/${fx.task.id}/package-requests/${fx.reqId}/decide`).send({ decision: 'deny' });
    expect(res.status).toBe(404);
  });

  it('owner can deny → request denied, resume no-ops without a job', async () => {
    const fx = await seed();
    const app = makeApp(repo, asUser(fx.owner), ENABLED);
    const res = await request(app).post(`/api/local/tasks/${fx.task.id}/package-requests/${fx.reqId}/decide`).send({ decision: 'deny' });
    expect(res.status).toBe(200);
    expect(res.body.decision).toBe('deny');
    expect(repo.getPackageRequest(fx.reqId)?.status).toBe('denied');
  });

  it('deciding an already-decided request → 409', async () => {
    const fx = await seed();
    repo.decidePackageRequest(fx.reqId, { status: 'denied', decidedBy: fx.owner.id });
    const app = makeApp(repo, asUser(fx.owner), ENABLED);
    const res = await request(app).post(`/api/local/tasks/${fx.task.id}/package-requests/${fx.reqId}/decide`).send({ decision: 'approve' });
    expect(res.status).toBe(409);
  });

  it('approve with the feature disabled → 400 (no install attempted)', async () => {
    const fx = await seed();
    const app = makeApp(repo, asUser(fx.owner), DISABLED);
    const res = await request(app).post(`/api/local/tasks/${fx.task.id}/package-requests/${fx.reqId}/decide`).send({ decision: 'approve' });
    expect(res.status).toBe(400);
    expect(repo.getPackageRequest(fx.reqId)?.status).toBe('pending');
  });

  it('admin (not a member) can decide', async () => {
    const fx = await seed();
    const admin = repo.createUser({ email: 'a@e.com', name: 'A', role: 'admin', status: 'active' });
    const app = makeApp(repo, asUser(admin), ENABLED);
    const res = await request(app).post(`/api/local/tasks/${fx.task.id}/package-requests/${fx.reqId}/decide`).send({ decision: 'deny' });
    expect(res.status).toBe(200);
    expect(repo.getPackageRequest(fx.reqId)?.status).toBe('denied');
  });

  it('deny re-queues a job parked with wait_reason=package_request (park/resume contract)', async () => {
    // Regression guard for the worker-park contract: a package-approval pause
    // MUST be persisted as wait_reason='package_request' for resume to work.
    const fx = await seed();
    const job = await repo.createJob({
      repo: localTaskRepoName(fx.task.id), issueNumber: fx.task.id,
      instruction: 'x', pieceName: 'chat', ownerId: fx.owner.id, spaceId: fx.space.id,
    });
    repo.getDb().prepare(`UPDATE jobs SET status='waiting_human', wait_reason='package_request' WHERE id = ?`).run(job.id);
    const reqId = repo.recordPackageRequest({
      taskId: String(fx.task.id), jobId: job.id, spaceId: fx.space.id,
      spec: 'httpx', normalizedName: 'httpx',
    });
    const app = makeApp(repo, asUser(fx.owner), ENABLED);
    const res = await request(app).post(`/api/local/tasks/${fx.task.id}/package-requests/${reqId}/decide`).send({ decision: 'deny' });
    expect(res.status).toBe(200);
    expect(res.body.resumed).toBe(true);
    expect((await repo.getJob(job.id))?.status).toBe('queued');
  });

  it('does NOT resume a job parked without wait_reason=package_request', async () => {
    // The wait_reason gate must be exact: a job parked for a different reason is
    // never re-queued by resumePackageRequestJob.
    const fx = await seed();
    const job = await repo.createJob({
      repo: localTaskRepoName(fx.task.id), issueNumber: fx.task.id,
      instruction: 'x', pieceName: 'chat', ownerId: fx.owner.id, spaceId: fx.space.id,
    });
    repo.getDb().prepare(`UPDATE jobs SET status='waiting_human', wait_reason='tool_request' WHERE id = ?`).run(job.id);
    const reqId = repo.recordPackageRequest({
      taskId: String(fx.task.id), jobId: job.id, spaceId: fx.space.id, spec: 'httpx', normalizedName: 'httpx',
    });
    const app = makeApp(repo, asUser(fx.owner), ENABLED);
    const res = await request(app).post(`/api/local/tasks/${fx.task.id}/package-requests/${reqId}/decide`).send({ decision: 'deny' });
    expect(res.status).toBe(200);
    expect(res.body.resumed).toBe(false); // wait_reason mismatch → not re-queued
    expect((await repo.getJob(job.id))?.status).toBe('waiting_human');
  });
});
