import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { randomUUID } from 'node:crypto';
import { Repository } from './repository.js';
import { PieceCatalog } from '../engine/piece-catalog.js';

function freshRepo() {
  const dir = mkdtempSync(join(tmpdir(), 'spaces-res-'));
  const repo = new Repository(join(dir, `${randomUUID()}.db`));
  return { repo, dir };
}

describe('space_id columns', () => {
  let repo: Repository; let dir: string;
  beforeEach(() => { ({ repo, dir } = freshRepo()); });
  afterEach(() => { rmSync(dir, { recursive: true, force: true }); });

  it('jobs and scheduled_tasks have a space_id column', () => {
    for (const t of ['jobs', 'scheduled_tasks']) {
      // @ts-expect-error private db handle
      const cols = repo.db.prepare(`PRAGMA table_info('${t}')`).all() as Array<{ name: string }>;
      expect(cols.map(c => c.name)).toContain('space_id');
    }
  });

  it('createLocalTask persists and reads back space_id', async () => {
    const space = await repo.createSpace({ kind: 'case', title: 'c', ownerId: 'u1' });
    const task = await repo.createLocalTask({ title: 't', body: 'b', ownerId: 'u1', spaceId: space.id });
    expect(task.spaceId).toBe(space.id);
    const loaded = await repo.getLocalTask(task.id);
    expect(loaded?.spaceId).toBe(space.id);
  });

  it('createScheduledTask persists and reads back space_id (not a dead wire)', async () => {
    const space = await repo.createSpace({ kind: 'case', title: 'c', ownerId: 'u1' });
    const st = await repo.createScheduledTask({
      body: 'do it', cronExpression: '0 9 * * *', nextRunAt: '2026-06-19T09:00:00Z',
      ownerId: 'u1', spaceId: space.id,
    });
    expect(st.spaceId).toBe(space.id);
  });
});

describe('resolveTaskFolderContext', () => {
  let repo: Repository; let dir: string;
  beforeEach(() => { ({ repo, dir } = freshRepo()); });
  afterEach(() => { rmSync(dir, { recursive: true, force: true }); });

  it('null space_id resolves to the owner personal space folder (legacy data/users/{owner})', async () => {
    const task = await repo.createLocalTask({ title: 't', body: '', ownerId: 'owner-7' });
    const fc = await repo.resolveTaskFolderContext(task.id, '/srv/data/users');
    expect(fc).toEqual({ rootDir: '/srv/data/users', leafId: 'owner-7' });
  });

  it('a case space task resolves to data/spaces/{spaceId}', async () => {
    const space = await repo.createSpace({ kind: 'case', title: 'c', ownerId: 'owner-7' });
    const task = await repo.createLocalTask({ title: 't', body: '', ownerId: 'owner-7', spaceId: space.id });
    const fc = await repo.resolveTaskFolderContext(task.id, '/srv/data/users');
    expect(fc).toEqual({ rootDir: '/srv/data/spaces', leafId: space.id });
  });

  it('null owner + null space resolves to local personal space', async () => {
    const task = await repo.createLocalTask({ title: 't', body: '' });
    const fc = await repo.resolveTaskFolderContext(task.id, '/srv/data/users');
    expect(fc).toEqual({ rootDir: '/srv/data/users', leafId: 'local' });
  });
});

// End-to-end smoke for Task 5: the worker resolves a local task's space folder
// (resolveTaskFolderContext) and uses it as the custom-piece load dir
// (PieceCatalog.getForFolder). This wires the two units the worker chains.
describe('worker folder resolution smoke (space task → custom piece)', () => {
  let repo: Repository; let dir: string;
  beforeEach(() => { ({ repo, dir } = freshRepo()); });
  afterEach(() => { rmSync(dir, { recursive: true, force: true }); });

  it('a case-space task resolves to a folder whose custom pieces load via getForFolder', async () => {
    const userFolderRoot = join(dir, 'data', 'users');
    const space = await repo.createSpace({ kind: 'case', title: 'Case A', ownerId: 'owner-1' });
    const task = await repo.createLocalTask({ title: 't', body: '', ownerId: 'owner-1', spaceId: space.id });

    const fc = await repo.resolveTaskFolderContext(task.id, userFolderRoot);
    // case space => data/spaces/{spaceId}
    expect(fc).toEqual({ rootDir: join(dir, 'data', 'spaces'), leafId: space.id });

    // Drop a custom piece into the resolved space folder's pieces/ dir.
    const piecesDir = join(fc.rootDir, fc.leafId, 'pieces');
    mkdirSync(piecesDir, { recursive: true });
    writeFileSync(join(piecesDir, 'case-only.yaml'), 'name: case-only\ndescription: case scoped\nmovements: []\n', 'utf-8');

    // Built-in dir with one piece so the base layer exists.
    const builtinDir = join(dir, 'builtins');
    mkdirSync(builtinDir, { recursive: true });
    writeFileSync(join(builtinDir, 'chat.yaml'), 'name: chat\ndescription: builtin\nmovements: []\n', 'utf-8');

    const catalog = new PieceCatalog(builtinDir, userFolderRoot);
    const entries = catalog.getForFolder(fc.rootDir, fc.leafId);
    expect(entries.some(e => e.name === 'case-only' && e.source === 'custom')).toBe(true);
    expect(entries.some(e => e.name === 'chat')).toBe(true);

    // A separate personal task (null space) must NOT see the space's custom piece.
    const personalTask = await repo.createLocalTask({ title: 'p', body: '', ownerId: 'owner-1' });
    const personalFc = await repo.resolveTaskFolderContext(personalTask.id, userFolderRoot);
    const personalEntries = catalog.getForFolder(personalFc.rootDir, personalFc.leafId);
    expect(personalEntries.some(e => e.name === 'case-only')).toBe(false);
  });

  it('subtask job inherits parent job space_id via createJob', async () => {
    const space = await repo.createSpace({ kind: 'case', title: 'c', ownerId: 'o' });
    const parent = await repo.createJob({ repo: 'local/task-1', issueNumber: 1, instruction: 'x', spaceId: space.id });
    expect(parent.spaceId).toBe(space.id);
    const child = await repo.createJob({ repo: `subtask/${parent.id}`, issueNumber: 1, instruction: 'y', parentJobId: parent.id, spaceId: parent.spaceId });
    expect(child.spaceId).toBe(space.id);
    // sanity: dirname helper available (keeps import used)
    expect(dirname('/a/b')).toBe('/a');
  });
});
