import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'bun:test';
import { Database } from 'bun:sqlite';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { initDatabase, closeDatabase } from '@myco/db/client.js';
import { createCanopyTools } from '@myco/agent/tools/canopy-tools.js';
import { ensureProjectManifest } from '@myco/config/project-manifest.js';
import type { VaultToolDeps } from '@myco/agent/tools/types.js';

function seedSchema(db: Database) {
  // Schema seed: hardcoded, no untrusted input
  const schema = `
    CREATE TABLE canopy_entries (
      project_id TEXT NOT NULL,
      path TEXT NOT NULL,
      language TEXT,
      exports_json TEXT,
      imports_json TEXT,
      top_comment TEXT,
      llm_description TEXT,
      llm_updated_at INTEGER,
      mechanical_updated_at INTEGER NOT NULL,
      embedded INTEGER DEFAULT 0,
      describe_attempts INTEGER NOT NULL DEFAULT 0,
      PRIMARY KEY (project_id, path)
    );
  `;
  db.exec(schema);
}

async function invoke(t: any, args: Record<string, unknown>): Promise<unknown> {
  const result = await t.handler(args);
  return JSON.parse(result.content[0].text);
}

describe('canopy_describe_next', () => {
  let projectRoot: string;
  let deps: VaultToolDeps;
  let projectId: string;
  let db: Database;

  beforeAll(() => {
    projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'canopy-tools-test-'));
    const vaultDir = path.join(projectRoot, '.myco');
    fs.mkdirSync(vaultDir, { recursive: true });
    const manifest = ensureProjectManifest(vaultDir, { projectName: 'canopy-tools-test' });
    projectId = manifest.project.id;
    deps = { agentId: 'a', runId: 'r', projectRoot, vaultDir } as VaultToolDeps;
  });

  afterAll(() => {
    fs.rmSync(projectRoot, { recursive: true, force: true });
  });

  beforeEach(() => {
    closeDatabase();
    db = initDatabase(':memory:');
    seedSchema(db);
  });

  it('returns up to limit pending rows when canopy_entry_path is unset', async () => {
    db.prepare('INSERT INTO canopy_entries (project_id, path, mechanical_updated_at) VALUES (?, ?, ?)').run(projectId, 'a.ts', 100);
    db.prepare('INSERT INTO canopy_entries (project_id, path, mechanical_updated_at) VALUES (?, ?, ?)').run(projectId, 'b.ts', 100);

    const tools = createCanopyTools(deps);
    const next = tools.find((t) => t.name === 'canopy_describe_next')!;
    const result = await invoke(next, { limit: 10 }) as { entries: any[] };
    expect(result.entries).toHaveLength(2);
  });

  it('returns the one row matching canopy_entry_path, bypassing pending predicate', async () => {
    db.prepare(
      'INSERT INTO canopy_entries (project_id, path, llm_description, llm_updated_at, mechanical_updated_at) VALUES (?, ?, ?, ?, ?)'
    ).run(projectId, 'src/foo.ts', 'old description', 200, 100);

    const tools = createCanopyTools(deps);
    const next = tools.find((t) => t.name === 'canopy_describe_next')!;
    const result = await invoke(next, { canopy_entry_path: 'src/foo.ts' }) as { entries: any[] };
    expect(result.entries).toHaveLength(1);
    expect(result.entries[0].path).toBe('src/foo.ts');
  });

  it('returns empty entries when canopy_entry_path matches no row', async () => {
    const tools = createCanopyTools(deps);
    const next = tools.find((t) => t.name === 'canopy_describe_next')!;
    const result = await invoke(next, { canopy_entry_path: 'does/not/exist.ts' }) as { entries: any[] };
    expect(result.entries).toHaveLength(0);
  });
});
