import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import express from 'express';
import request from 'supertest';
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { mountSubtaskFilesApi } from './subtask-files-api.js';
import type { Repository } from '../db/repository.js';

let taskWs: string;
let subWs: string;

function makeRepo(overrides: Partial<Repository> = {}): Repository {
  return {
    getLocalTask: vi.fn().mockResolvedValue({
      id: 1,
      ownerId: 'user-1',
      visibility: 'private',
      workspacePath: taskWs,
    }),
    getJob: vi.fn().mockResolvedValue({ id: 'job-9', worktreePath: subWs }),
    ...overrides,
  } as unknown as Repository;
}

function makeUser(overrides: Partial<Express.User> = {}): Express.User {
  return {
    id: 'user-1',
    email: 'u@example.com',
    name: 'User One',
    avatarUrl: null,
    role: 'user',
    status: 'active',
    orgIds: [],
    defaultVisibility: 'private',
    defaultVisibilityOrgId: null,
    ...overrides,
  };
}

function makeApp(repo: Repository, user?: Express.User): express.Application {
  const app = express();
  if (user) {
    app.use((req, _res, next) => {
      (req as unknown as { user: Express.User }).user = user;
      next();
    });
  }
  mountSubtaskFilesApi(app, repo);
  return app;
}

beforeEach(() => {
  taskWs = mkdtempSync(join(tmpdir(), 'subtask-files-task-'));
  subWs = join(taskWs, 'subtasks', '1');
  mkdirSync(join(subWs, 'output'), { recursive: true });
  mkdirSync(join(subWs, 'logs'), { recursive: true });
  writeFileSync(join(subWs, 'output', 'result.md'), '# result');
  writeFileSync(join(subWs, 'logs', 'activity.log'), 'log line');
  writeFileSync(join(subWs, 'secret-sibling.txt'), 'outside reachable?');
});

afterEach(() => {
  rmSync(taskWs, { recursive: true, force: true });
});

describe('GET /api/local/tasks/:id/subtasks/:jobId/files (listing)', () => {
  it('lists files grouped by category with output as legacy files', async () => {
    const app = makeApp(makeRepo(), makeUser());
    const res = await request(app).get('/api/local/tasks/1/subtasks/job-9/files');
    expect(res.status).toBe(200);
    expect(res.body.files).toEqual(['result.md']);
    expect(res.body.categories).toEqual({
      output: ['result.md'],
      logs: ['activity.log'],
    });
  });

  it('rejects an invalid task id with 400', async () => {
    const app = makeApp(makeRepo(), makeUser());
    const res = await request(app).get('/api/local/tasks/banana/subtasks/job-9/files');
    expect(res.status).toBe(400);
  });

  it('returns 404 for a missing task', async () => {
    const repo = makeRepo({ getLocalTask: vi.fn().mockResolvedValue(null) } as Partial<Repository>);
    const app = makeApp(repo, makeUser());
    const res = await request(app).get('/api/local/tasks/1/subtasks/job-9/files');
    expect(res.status).toBe(404);
  });

  it('returns 404 when the viewer cannot see the task (private, non-owner)', async () => {
    const app = makeApp(makeRepo(), makeUser({ id: 'someone-else' }));
    const res = await request(app).get('/api/local/tasks/1/subtasks/job-9/files');
    expect(res.status).toBe(404);
  });

  it('returns 404 when the job has no worktree', async () => {
    const repo = makeRepo({ getJob: vi.fn().mockResolvedValue(null) } as Partial<Repository>);
    const app = makeApp(repo, makeUser());
    const res = await request(app).get('/api/local/tasks/1/subtasks/job-9/files');
    expect(res.status).toBe(404);
    expect(res.body.error).toBe('Subtask not found');
  });

  it('returns 404 when the subtask worktree lives outside the task workspace', async () => {
    const foreign = mkdtempSync(join(tmpdir(), 'foreign-ws-'));
    try {
      const repo = makeRepo({
        getJob: vi.fn().mockResolvedValue({ id: 'job-9', worktreePath: foreign }),
      } as Partial<Repository>);
      const app = makeApp(repo, makeUser());
      const res = await request(app).get('/api/local/tasks/1/subtasks/job-9/files');
      expect(res.status).toBe(404);
    } finally {
      rmSync(foreign, { recursive: true, force: true });
    }
  });
});

describe('GET /api/local/tasks/:id/subtasks/:jobId/files/* (content)', () => {
  it('serves a file inside the subtask worktree', async () => {
    const app = makeApp(makeRepo(), makeUser());
    const res = await request(app).get('/api/local/tasks/1/subtasks/job-9/files/output/result.md');
    expect(res.status).toBe(200);
    expect(res.text).toBe('# result');
  });

  it('lists directory contents for a directory path', async () => {
    const app = makeApp(makeRepo(), makeUser());
    const res = await request(app).get('/api/local/tasks/1/subtasks/job-9/files/output');
    expect(res.status).toBe(200);
    expect(res.body.files).toEqual(['result.md']);
  });

  it('denies path traversal out of the worktree with 403', async () => {
    const app = makeApp(makeRepo(), makeUser());
    const res = await request(app).get(
      '/api/local/tasks/1/subtasks/job-9/files/output/..%2F..%2F..%2Fetc%2Fpasswd',
    );
    expect(res.status).toBe(403);
  });

  it('denies encoded traversal to a sibling directory sharing the prefix', async () => {
    // `<base>-x` must not pass the startsWith check.
    mkdirSync(`${subWs}-x`, { recursive: true });
    writeFileSync(join(`${subWs}-x`, 'leak.txt'), 'leak');
    const app = makeApp(makeRepo(), makeUser());
    const res = await request(app).get(
      '/api/local/tasks/1/subtasks/job-9/files/..%2F1-x%2Fleak.txt',
    );
    expect(res.status).toBe(403);
  });

  it('returns 404 for a file that does not exist', async () => {
    const app = makeApp(makeRepo(), makeUser());
    const res = await request(app).get('/api/local/tasks/1/subtasks/job-9/files/output/nope.txt');
    expect(res.status).toBe(404);
    expect(res.body.error).toBe('File not found');
  });

  it('returns 404 when the viewer cannot see the task', async () => {
    const app = makeApp(makeRepo(), makeUser({ id: 'someone-else' }));
    const res = await request(app).get('/api/local/tasks/1/subtasks/job-9/files/output/result.md');
    expect(res.status).toBe(404);
  });

  it('allows org members to fetch files of an org-visible task', async () => {
    const repo = makeRepo({
      getLocalTask: vi.fn().mockResolvedValue({
        id: 1,
        ownerId: 'owner-x',
        visibility: 'org',
        visibilityScopeOrgId: 'org-a',
        workspacePath: taskWs,
      }),
    } as Partial<Repository>);
    const app = makeApp(repo, makeUser({ id: 'member', orgIds: ['org-a'] }));
    const res = await request(app).get('/api/local/tasks/1/subtasks/job-9/files/output/result.md');
    expect(res.status).toBe(200);
    expect(res.text).toBe('# result');
  });
});
