import { afterEach, describe, expect, it } from 'bun:test';
import { mkdtemp, mkdir, rm, writeFile } from 'fs/promises';
import { tmpdir } from 'os';
import { join } from 'path';
import { loadDrizzleMeta, loadFeatures, loadProjectLib, loadProjectEmailTemplates } from './file-loader.js';
const tempDirs = [];
async function createTempDir() {
    const dir = await mkdtemp(join(tmpdir(), 'quickback-cli-meta-'));
    tempDirs.push(dir);
    return dir;
}
afterEach(async () => {
    await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
});
describe('loadDrizzleMeta', () => {
    // User story: As a developer with separate auth/files/webhooks databases, I need the CLI
    // to send both the migration SQL chain and the meta journals so the compiler can generate
    // incremental migrations safely and sanitize historical D1 migrations when needed.
    it('loads scoped drizzle meta directories from quickback/drizzle', async () => {
        const projectRoot = await createTempDir();
        await mkdir(join(projectRoot, 'quickback', 'drizzle', 'auth', 'meta'), { recursive: true });
        await mkdir(join(projectRoot, 'quickback', 'drizzle', 'files', 'meta'), { recursive: true });
        await mkdir(join(projectRoot, 'quickback', 'drizzle', 'webhooks', 'meta'), { recursive: true });
        await writeFile(join(projectRoot, 'quickback', 'drizzle', 'auth', '0000_initial.sql'), '-- auth');
        await writeFile(join(projectRoot, 'quickback', 'drizzle', 'auth', 'meta', '_journal.json'), '{"auth":true}');
        await writeFile(join(projectRoot, 'quickback', 'drizzle', 'files', '0000_initial.sql'), '-- files');
        await writeFile(join(projectRoot, 'quickback', 'drizzle', 'files', 'meta', '_journal.json'), '{"files":true}');
        await writeFile(join(projectRoot, 'quickback', 'drizzle', 'webhooks', '0000_initial.sql'), '-- webhooks');
        await writeFile(join(projectRoot, 'quickback', 'drizzle', 'webhooks', 'meta', '_journal.json'), '{"webhooks":true}');
        const meta = await loadDrizzleMeta(projectRoot);
        expect(meta).toBeDefined();
        expect(Object.keys(meta || {}).sort()).toEqual([
            'quickback/drizzle/auth/0000_initial.sql',
            'quickback/drizzle/auth/meta/_journal.json',
            'quickback/drizzle/files/0000_initial.sql',
            'quickback/drizzle/files/meta/_journal.json',
            'quickback/drizzle/webhooks/0000_initial.sql',
            'quickback/drizzle/webhooks/meta/_journal.json',
        ]);
        expect(meta?.['quickback/drizzle/auth/0000_initial.sql']).toBe('-- auth');
        expect(meta?.['quickback/drizzle/auth/meta/_journal.json']).toBe('{"auth":true}');
        expect(meta?.['quickback/drizzle/files/0000_initial.sql']).toBe('-- files');
        expect(meta?.['quickback/drizzle/files/meta/_journal.json']).toBe('{"files":true}');
        expect(meta?.['quickback/drizzle/webhooks/0000_initial.sql']).toBe('-- webhooks');
        expect(meta?.['quickback/drizzle/webhooks/meta/_journal.json']).toBe('{"webhooks":true}');
    });
    // User story: The retired mixed auth+features migration layout (`quickback/drizzle/meta`)
    // must not be accepted silently now that auth and features are always split.
    it('returns undefined for legacy quickback/drizzle/meta without scoped folders', async () => {
        const projectRoot = await createTempDir();
        await mkdir(join(projectRoot, 'quickback', 'drizzle', 'meta'), { recursive: true });
        await writeFile(join(projectRoot, 'quickback', 'drizzle', '0000_initial.sql'), '-- legacy');
        await writeFile(join(projectRoot, 'quickback', 'drizzle', 'meta', '_journal.json'), '{"legacy":true}');
        const meta = await loadDrizzleMeta(projectRoot);
        expect(meta).toBeUndefined();
    });
    // User story: As a developer in a fresh repo, no meta payload should be sent
    // when no migration state exists yet.
    it('returns undefined when no quickback/drizzle exists', async () => {
        const projectRoot = await createTempDir();
        const meta = await loadDrizzleMeta(projectRoot);
        expect(meta).toBeUndefined();
    });
    // User story: As a developer who hasn't run the first compile yet, an empty `quickback/`
    // folder (definitions only) should not be mistaken for an existing migration state.
    it('returns undefined when quickback/ exists but has no drizzle dir', async () => {
        const projectRoot = await createTempDir();
        await mkdir(join(projectRoot, 'quickback'), { recursive: true });
        await writeFile(join(projectRoot, 'quickback', 'placeholder.ts'), '// definitions');
        const meta = await loadDrizzleMeta(projectRoot);
        expect(meta).toBeUndefined();
    });
    // User story: Project-root `drizzle/` is never read — Quickback owns migration state
    // inside `quickback/drizzle/` and any stray root folder must be ignored to prevent
    // stale meta from polluting the wire payload.
    it('ignores project-root drizzle/ even when present', async () => {
        const projectRoot = await createTempDir();
        await mkdir(join(projectRoot, 'drizzle', 'auth', 'meta'), { recursive: true });
        await writeFile(join(projectRoot, 'drizzle', 'auth', 'meta', '_journal.json'), '{"stray":true}');
        const meta = await loadDrizzleMeta(projectRoot);
        expect(meta).toBeUndefined();
    });
});
// ─────────────────────────────────────────────────────────────────
// Fixtures
// ─────────────────────────────────────────────────────────────────
const FEATURE_PROFILES_TS = `
import { feature, q } from "@quickback/compiler";
export default feature("profiles", {
  columns: { id: q.id(), name: q.text().required() },
});
`;
const FEATURE_EPISODES_TS = `
import { feature, q } from "@quickback/compiler";
export default feature("episodes", {
  columns: { id: q.id(), title: q.text().required() },
});
`;
const NEW_SHAPE_RECORD_ACTION = `
import { defineAction } from "../.quickback/define-action";
import { z } from "zod";
import { profiles } from "../profiles";

export default defineAction({
  description: "Publish a profile",
  input: z.object({}),
  access: { roles: ["admin"] },
  execute: async ({ db, record }) => record,
});
`;
const NEW_SHAPE_STANDALONE_ACTION = `
import { defineAction } from "../.quickback/define-action";
import { z } from "zod";

export default defineAction({
  description: "Twilio webhook",
  path: "/twilioIncoming",
  method: "POST",
  input: z.object({ Body: z.string() }),
  access: { roles: ["PUBLIC"] },
  execute: async () => ({}),
});
`;
const NEW_SHAPE_NESTED_ACTION = `
import { defineAction } from "../../.quickback/define-action.profiles";
import { z } from "zod";

export default defineAction({
  description: "Publish a profile",
  input: z.object({}),
  access: { roles: ["admin"] },
  execute: async ({ record }) => record,
});
`;
// ─────────────────────────────────────────────────────────────────
// Happy path — new shape (one file per action)
// ─────────────────────────────────────────────────────────────────
describe('loadFeatures — one-file-per-action shape', () => {
    it('discovers flat actions/<name>.ts files into actionFiles', async () => {
        const root = await createTempDir();
        const featureDir = join(root, 'profiles');
        await mkdir(join(featureDir, 'actions'), { recursive: true });
        await writeFile(join(featureDir, 'profiles.ts'), FEATURE_PROFILES_TS);
        await writeFile(join(featureDir, 'actions', 'publish.ts'), NEW_SHAPE_RECORD_ACTION);
        await writeFile(join(featureDir, 'actions', 'webhook.ts'), NEW_SHAPE_STANDALONE_ACTION);
        const features = await loadFeatures(root);
        expect(features).toHaveLength(1);
        const profiles = features[0];
        expect(Object.keys(profiles.actionFiles).sort()).toEqual([
            'actions/publish.ts',
            'actions/webhook.ts',
        ]);
        expect(profiles.actionFiles['actions/publish.ts']).toContain('export default defineAction');
    });
    it('discovers nested actions/<table>/<name>.ts files', async () => {
        const root = await createTempDir();
        const featureDir = join(root, 'podcast');
        await mkdir(join(featureDir, 'actions', 'profiles'), { recursive: true });
        await writeFile(join(featureDir, 'profiles.ts'), FEATURE_PROFILES_TS);
        await writeFile(join(featureDir, 'episodes.ts'), FEATURE_EPISODES_TS);
        await writeFile(join(featureDir, 'actions', 'profiles', 'publish.ts'), NEW_SHAPE_NESTED_ACTION);
        const features = await loadFeatures(root);
        const podcast = features[0];
        expect(Object.keys(podcast.actionFiles)).toEqual(['actions/profiles/publish.ts']);
    });
    it('picks up feature-local lib/ helpers as featureLibFiles', async () => {
        const root = await createTempDir();
        const featureDir = join(root, 'records');
        await mkdir(join(featureDir, 'actions'), { recursive: true });
        await mkdir(join(featureDir, 'lib', 'sub'), { recursive: true });
        await writeFile(join(featureDir, 'records.ts'), FEATURE_PROFILES_TS);
        await writeFile(join(featureDir, 'actions', 'focus.ts'), NEW_SHAPE_RECORD_ACTION);
        await writeFile(join(featureDir, 'lib', 'inputs.ts'), 'export const isoDate = "...";');
        await writeFile(join(featureDir, 'lib', 'sub', 'helper.ts'), 'export const helper = () => 2;');
        const features = await loadFeatures(root);
        const records = features[0];
        expect(Object.keys(records.featureLibFiles).sort()).toEqual([
            'lib/inputs.ts',
            'lib/sub/helper.ts',
        ]);
        expect(records.featureLibFiles['lib/sub/helper.ts']).toContain('helper');
    });
    it('supports tableless features (only actions/, no table file)', async () => {
        const root = await createTempDir();
        const featureDir = join(root, 'export');
        await mkdir(join(featureDir, 'actions'), { recursive: true });
        await writeFile(join(featureDir, 'actions', 'pipeline-csv.ts'), NEW_SHAPE_STANDALONE_ACTION);
        const features = await loadFeatures(root);
        const exportFeature = features[0];
        expect(exportFeature.actionFiles).toBeDefined();
        expect(Object.keys(exportFeature.actionFiles)).toEqual(['actions/pipeline-csv.ts']);
        expect(exportFeature.tables).toEqual([]);
    });
});
// ─────────────────────────────────────────────────────────────────
// Migration errors — every retired primitive rejects with a clear message
// ─────────────────────────────────────────────────────────────────
describe('loadFeatures — retired primitives', () => {
    const LEGACY_DEFINE_ACTIONS = `
import { z } from "zod";
import { defineActions } from "@quickback/compiler";
import profiles from "./profiles";
export default defineActions(profiles, {
  publish: { input: z.object({}), access: { roles: ["owner"] }, handler: "./handlers/publish" },
});
`;
    it('rejects bundled actions.ts with a migration message', async () => {
        const root = await createTempDir();
        const featureDir = join(root, 'profiles');
        await mkdir(featureDir, { recursive: true });
        await writeFile(join(featureDir, 'profiles.ts'), FEATURE_PROFILES_TS);
        await writeFile(join(featureDir, 'actions.ts'), LEGACY_DEFINE_ACTIONS);
        await expect(loadFeatures(root)).rejects.toThrow(/Bundled `actions\.ts` is retired/);
    });
    it('rejects _feature.ts with a migration message', async () => {
        const root = await createTempDir();
        const featureDir = join(root, 'profiles');
        await mkdir(featureDir, { recursive: true });
        await writeFile(join(featureDir, 'profiles.ts'), FEATURE_PROFILES_TS);
        await writeFile(join(featureDir, '_feature.ts'), 'export default { actions: {} };');
        await expect(loadFeatures(root)).rejects.toThrow(/`_feature\.ts` is retired/);
    });
    it('rejects handlers/ directory with a migration message', async () => {
        const root = await createTempDir();
        const featureDir = join(root, 'profiles');
        await mkdir(join(featureDir, 'handlers'), { recursive: true });
        await writeFile(join(featureDir, 'profiles.ts'), FEATURE_PROFILES_TS);
        await writeFile(join(featureDir, 'handlers', 'publish.ts'), 'export const execute = async () => {};');
        await expect(loadFeatures(root)).rejects.toThrow(/`handlers\/` directory is retired/);
    });
    it('rejects colocated defineActions(...) inside a table file', async () => {
        const root = await createTempDir();
        const featureDir = join(root, 'profiles');
        await mkdir(featureDir, { recursive: true });
        await writeFile(join(featureDir, 'profiles.ts'), FEATURE_PROFILES_TS + '\n' + LEGACY_DEFINE_ACTIONS);
        await expect(loadFeatures(root)).rejects.toThrow(/Colocated `defineActions\(\.\.\.\)` inside `profiles\.ts` is retired/);
    });
    it('rejects defineActions(...) inside an actions/<flat>.ts shard', async () => {
        const root = await createTempDir();
        const featureDir = join(root, 'profiles');
        await mkdir(join(featureDir, 'actions'), { recursive: true });
        await writeFile(join(featureDir, 'profiles.ts'), FEATURE_PROFILES_TS);
        await writeFile(join(featureDir, 'actions', 'profiles.ts'), LEGACY_DEFINE_ACTIONS);
        await expect(loadFeatures(root)).rejects.toThrow(/uses the retired `defineActions\(\.\.\.\)` \(plural\)/);
    });
    it('rejects retired filename patterns *-actions.ts and *.actions.ts', async () => {
        const root = await createTempDir();
        const featureDir = join(root, 'profiles');
        await mkdir(featureDir, { recursive: true });
        await writeFile(join(featureDir, 'profiles.ts'), FEATURE_PROFILES_TS);
        await writeFile(join(featureDir, 'profiles-actions.ts'), '// stub');
        await writeFile(join(featureDir, 'episodes.actions.ts'), '// stub');
        await expect(loadFeatures(root)).rejects.toThrow(/Filename pattern `(profiles-actions|episodes\.actions)\.ts` is retired/);
    });
    it('batches multiple retired-primitive conflicts into one error', async () => {
        const root = await createTempDir();
        // Feature A: actions.ts + handlers/
        const featureA = join(root, 'profiles');
        await mkdir(join(featureA, 'handlers'), { recursive: true });
        await writeFile(join(featureA, 'profiles.ts'), FEATURE_PROFILES_TS);
        await writeFile(join(featureA, 'actions.ts'), LEGACY_DEFINE_ACTIONS);
        await writeFile(join(featureA, 'handlers', 'publish.ts'), 'export const execute = async () => {};');
        // Feature B: _feature.ts
        const featureB = join(root, 'shows');
        await mkdir(featureB, { recursive: true });
        await writeFile(join(featureB, 'shows.ts'), FEATURE_PROFILES_TS.replace(/profiles/g, 'shows'));
        await writeFile(join(featureB, '_feature.ts'), 'export default { actions: {} };');
        await expect(loadFeatures(root)).rejects.toThrow(/Action discovery found 3 conflict/);
    });
});
// ─────────────────────────────────────────────────────────────────
// loadProjectLib — quickback/lib/ at the project root
// ─────────────────────────────────────────────────────────────────
describe('loadProjectLib', () => {
    it('loads quickback/lib/**.ts as flat keyed paths', async () => {
        const projectRoot = await createTempDir();
        await mkdir(join(projectRoot, 'quickback', 'lib', 'nested'), { recursive: true });
        await writeFile(join(projectRoot, 'quickback', 'lib', 'inputs.ts'), 'export const a = 1;');
        await writeFile(join(projectRoot, 'quickback', 'lib', 'ids.ts'), 'export const b = 2;');
        await writeFile(join(projectRoot, 'quickback', 'lib', 'nested', 'utils.ts'), 'export const c = 3;');
        const lib = await loadProjectLib(projectRoot);
        expect(Object.keys(lib).sort()).toEqual([
            'lib/ids.ts',
            'lib/inputs.ts',
            'lib/nested/utils.ts',
        ]);
        expect(lib['lib/inputs.ts']).toBe('export const a = 1;');
    });
    it('returns empty object when quickback/lib does not exist', async () => {
        const projectRoot = await createTempDir();
        const lib = await loadProjectLib(projectRoot);
        expect(lib).toEqual({});
    });
});
// ─────────────────────────────────────────────────────────────────
// loadProjectEmailTemplates — quickback/email-templates/ at the project root
// ─────────────────────────────────────────────────────────────────
describe('loadProjectEmailTemplates', () => {
    it('loads quickback/email-templates/**.ts as flat keyed paths', async () => {
        const projectRoot = await createTempDir();
        await mkdir(join(projectRoot, 'quickback', 'email-templates'), { recursive: true });
        await writeFile(join(projectRoot, 'quickback', 'email-templates', 'welcome.ts'), 'export default () => ({ subject: "x", html: "x", text: "x" });');
        await writeFile(join(projectRoot, 'quickback', 'email-templates', 'forget-password.ts'), 'export default () => ({ subject: "y", html: "y", text: "y" });');
        const templates = await loadProjectEmailTemplates(projectRoot);
        expect(Object.keys(templates).sort()).toEqual([
            'email-templates/forget-password.ts',
            'email-templates/welcome.ts',
        ]);
    });
    it('returns empty object when quickback/email-templates does not exist', async () => {
        const projectRoot = await createTempDir();
        const templates = await loadProjectEmailTemplates(projectRoot);
        expect(templates).toEqual({});
    });
});
//# sourceMappingURL=file-loader.test.js.map