"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_test_1 = require("node:test");
const strict_1 = __importDefault(require("node:assert/strict"));
const fs = __importStar(require("node:fs"));
const os = __importStar(require("node:os"));
const path = __importStar(require("node:path"));
const logger_1 = require("../logger");
const tasks_1 = require("../tasks");
const tasks_2 = require("../commands/tasks");
function stubProvider(opts = {}) {
    const calls = [];
    return {
        calls,
        fetchTasks: async () => [],
        fetchTasksByStatus: async () => [],
        postComment: async () => { },
        getComments: async () => [],
        updateStatus: async () => { },
        createTask: async (params) => {
            calls.push(params);
            if (opts.fail)
                throw new Error('createTask failed');
            return { id: `remote-${calls.length}`, url: `https://example.test/${calls.length}` };
        },
    };
}
function cfg(overrides = {}) {
    return {
        provider: 'clickup',
        clickupTag: 'code-tag',
        nonCodeTag: '',
        ...overrides,
    };
}
// ─── tasksFilePath ────────────────────────────────────────────────────────────
(0, node_test_1.describe)('tasksFilePath', () => {
    (0, node_test_1.it)('returns aidev.tasks.json inside the given directory', () => {
        const p = (0, tasks_1.tasksFilePath)(path.join(os.tmpdir(), 'proj'));
        strict_1.default.equal(path.basename(p), 'aidev.tasks.json');
        strict_1.default.equal(path.dirname(p), path.join(os.tmpdir(), 'proj'));
    });
    (0, node_test_1.it)('defaults to the current working directory', () => {
        strict_1.default.equal((0, tasks_1.tasksFilePath)(), path.join(process.cwd(), 'aidev.tasks.json'));
    });
});
// ─── readTasksFile / writeTasksFile ───────────────────────────────────────────
(0, node_test_1.describe)('readTasksFile / writeTasksFile', () => {
    let tmpDir;
    let origCwd;
    (0, node_test_1.beforeEach)(() => {
        tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-tasks-io-'));
        origCwd = process.cwd();
        process.chdir(tmpDir);
    });
    (0, node_test_1.afterEach)(() => {
        process.chdir(origCwd);
        fs.rmSync(tmpDir, { recursive: true, force: true });
    });
    (0, node_test_1.it)('returns [] when no aidev.tasks.json exists', () => {
        strict_1.default.deepEqual((0, tasks_1.readTasksFile)(), []);
    });
    (0, node_test_1.it)('returns [] when file contains invalid JSON', () => {
        fs.writeFileSync('aidev.tasks.json', 'not json at all', 'utf8');
        strict_1.default.deepEqual((0, tasks_1.readTasksFile)(), []);
    });
    (0, node_test_1.it)('returns [] when file contains non-array JSON', () => {
        fs.writeFileSync('aidev.tasks.json', '{"not":"an array"}', 'utf8');
        strict_1.default.deepEqual((0, tasks_1.readTasksFile)(), []);
    });
    (0, node_test_1.it)('round-trips an array of tasks', () => {
        const tasks = [
            { id: 'a', title: 'one', description: 'd1', type: 'code' },
            { id: 'b', title: 'two', description: 'd2', type: 'non-code', cron: '0 9 * * *' },
        ];
        (0, tasks_1.writeTasksFile)(tasks);
        strict_1.default.deepEqual((0, tasks_1.readTasksFile)(), tasks);
    });
    (0, node_test_1.it)('writes pretty-printed JSON with a trailing newline', () => {
        const tasks = [
            { id: 'a', title: 'one', description: '', type: 'code' },
        ];
        (0, tasks_1.writeTasksFile)(tasks);
        const content = fs.readFileSync('aidev.tasks.json', 'utf8');
        strict_1.default.ok(content.startsWith('[\n'));
        strict_1.default.ok(content.endsWith('\n'));
        strict_1.default.ok(content.includes('  "id"'), 'should be indented');
    });
    (0, node_test_1.it)('honours an explicit directory argument', () => {
        const sub = fs.mkdtempSync(path.join(tmpDir, 'sub-'));
        const tasks = [
            { id: 'z', title: 'in sub', description: '', type: 'code' },
        ];
        (0, tasks_1.writeTasksFile)(tasks, sub);
        strict_1.default.deepEqual((0, tasks_1.readTasksFile)(sub), tasks);
        // The file should not leak into cwd
        strict_1.default.equal(fs.existsSync(path.join(tmpDir, 'aidev.tasks.json')), false);
    });
});
// ─── processLocalTasks ────────────────────────────────────────────────────────
(0, node_test_1.describe)('processLocalTasks', () => {
    let tmpDir;
    let origCwd;
    (0, node_test_1.beforeEach)(() => {
        tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-processtasks-'));
        origCwd = process.cwd();
        process.chdir(tmpDir);
        // Keep test output quiet — logger writes to console + file.
        node_test_1.mock.method(logger_1.logger, 'info', () => { });
        node_test_1.mock.method(logger_1.logger, 'success', () => { });
        node_test_1.mock.method(logger_1.logger, 'warn', () => { });
        node_test_1.mock.method(logger_1.logger, 'error', () => { });
    });
    (0, node_test_1.afterEach)(() => {
        process.chdir(origCwd);
        fs.rmSync(tmpDir, { recursive: true, force: true });
        node_test_1.mock.restoreAll();
    });
    (0, node_test_1.it)('returns zero counts and does not create the file when no tasks are queued', async () => {
        const provider = stubProvider();
        const result = await (0, tasks_1.processLocalTasks)(cfg(), provider);
        strict_1.default.deepEqual(result, { pushed: 0, skipped: 0 });
        strict_1.default.equal(provider.calls.length, 0);
        strict_1.default.equal(fs.existsSync('aidev.tasks.json'), false);
    });
    (0, node_test_1.it)('pushes a one-shot task and removes it from the file', async () => {
        const task = {
            id: 'abc',
            title: 'Refactor auth',
            description: 'Split middleware',
            type: 'code',
        };
        (0, tasks_1.writeTasksFile)([task]);
        const provider = stubProvider();
        const result = await (0, tasks_1.processLocalTasks)(cfg(), provider);
        strict_1.default.deepEqual(result, { pushed: 1, skipped: 0 });
        strict_1.default.equal(provider.calls.length, 1);
        strict_1.default.equal(provider.calls[0].title, 'Refactor auth');
        strict_1.default.equal(provider.calls[0].description, 'Split middleware');
        strict_1.default.deepEqual(provider.calls[0].tags, ['code-tag']);
        strict_1.default.deepEqual((0, tasks_1.readTasksFile)(), []);
    });
    (0, node_test_1.it)('prepends the configured code tag and appends per-task tags', async () => {
        const task = {
            id: 'abc',
            title: 't',
            description: '',
            type: 'code',
            tags: ['frontend', 'urgent'],
        };
        (0, tasks_1.writeTasksFile)([task]);
        const provider = stubProvider();
        await (0, tasks_1.processLocalTasks)(cfg(), provider);
        strict_1.default.deepEqual(provider.calls[0].tags, ['code-tag', 'frontend', 'urgent']);
    });
    (0, node_test_1.it)('passes priority and listId through to the provider', async () => {
        const task = {
            id: 'abc',
            title: 't',
            description: '',
            type: 'code',
            priority: 2,
            listId: 'LIST-42',
        };
        (0, tasks_1.writeTasksFile)([task]);
        const provider = stubProvider();
        await (0, tasks_1.processLocalTasks)(cfg(), provider);
        strict_1.default.equal(provider.calls[0].priority, 2);
        strict_1.default.equal(provider.calls[0].listId, 'LIST-42');
    });
    (0, node_test_1.it)('converts ISO dueDate to epoch milliseconds', async () => {
        const task = {
            id: 'abc',
            title: 't',
            description: '',
            type: 'code',
            dueDate: '2026-05-01',
        };
        (0, tasks_1.writeTasksFile)([task]);
        const provider = stubProvider();
        await (0, tasks_1.processLocalTasks)(cfg(), provider);
        strict_1.default.equal(typeof provider.calls[0].dueDate, 'number');
        strict_1.default.equal(provider.calls[0].dueDate, new Date('2026-05-01').getTime());
    });
    (0, node_test_1.it)('routes non-code tasks to the non-code provider with its tag', async () => {
        const tasks = [
            { id: '1', title: 'code task', description: '', type: 'code' },
            { id: '2', title: 'nc task', description: '', type: 'non-code' },
        ];
        (0, tasks_1.writeTasksFile)(tasks);
        const code = stubProvider();
        const nonCode = stubProvider();
        const result = await (0, tasks_1.processLocalTasks)(cfg({ clickupTag: 'code-tag', nonCodeTag: 'nc-tag' }), code, nonCode);
        strict_1.default.deepEqual(result, { pushed: 2, skipped: 0 });
        strict_1.default.equal(code.calls.length, 1);
        strict_1.default.equal(code.calls[0].title, 'code task');
        strict_1.default.deepEqual(code.calls[0].tags, ['code-tag']);
        strict_1.default.equal(nonCode.calls.length, 1);
        strict_1.default.equal(nonCode.calls[0].title, 'nc task');
        strict_1.default.deepEqual(nonCode.calls[0].tags, ['nc-tag']);
        strict_1.default.deepEqual((0, tasks_1.readTasksFile)(), []);
    });
    (0, node_test_1.it)('falls back to the code provider when no non-code provider is given, but still uses the non-code tag', async () => {
        const task = {
            id: '1', title: 'nc', description: '', type: 'non-code',
        };
        (0, tasks_1.writeTasksFile)([task]);
        const code = stubProvider();
        await (0, tasks_1.processLocalTasks)(cfg({ clickupTag: 'code-tag', nonCodeTag: 'nc-tag' }), code);
        strict_1.default.equal(code.calls.length, 1);
        strict_1.default.deepEqual(code.calls[0].tags, ['nc-tag']);
    });
    (0, node_test_1.it)('falls back to the code tag for non-code tasks when nonCodeTag is empty', async () => {
        const task = {
            id: '1', title: 'nc', description: '', type: 'non-code',
        };
        (0, tasks_1.writeTasksFile)([task]);
        const code = stubProvider();
        await (0, tasks_1.processLocalTasks)(cfg({ clickupTag: 'code-tag', nonCodeTag: '' }), code);
        strict_1.default.deepEqual(code.calls[0].tags, ['code-tag']);
    });
    (0, node_test_1.it)('keeps a cron task in the file and stamps lastPushedAt after a successful push', async () => {
        const task = {
            id: 'cron1',
            title: 'Daily',
            description: '',
            type: 'code',
            cron: '* * * * *', // always fires
        };
        (0, tasks_1.writeTasksFile)([task]);
        const provider = stubProvider();
        const before = Date.now();
        const result = await (0, tasks_1.processLocalTasks)(cfg(), provider);
        const after = Date.now();
        strict_1.default.deepEqual(result, { pushed: 1, skipped: 0 });
        const remaining = (0, tasks_1.readTasksFile)();
        strict_1.default.equal(remaining.length, 1);
        strict_1.default.equal(remaining[0].id, 'cron1');
        strict_1.default.ok(typeof remaining[0].lastPushedAt === 'number');
        strict_1.default.ok(remaining[0].lastPushedAt >= before);
        strict_1.default.ok(remaining[0].lastPushedAt <= after);
    });
    (0, node_test_1.it)('skips a cron task whose schedule has not fired since lastPushedAt', async () => {
        // Cron that only fires at 03:17 AM on Jan 1 — effectively never during the
        // 48h lookback from any given test run.
        const task = {
            id: 'cron2',
            title: 'Yearly',
            description: '',
            type: 'code',
            cron: '17 3 1 1 *',
            lastPushedAt: Date.now(),
        };
        (0, tasks_1.writeTasksFile)([task]);
        const provider = stubProvider();
        const result = await (0, tasks_1.processLocalTasks)(cfg(), provider);
        strict_1.default.deepEqual(result, { pushed: 0, skipped: 1 });
        strict_1.default.equal(provider.calls.length, 0);
        const remaining = (0, tasks_1.readTasksFile)();
        strict_1.default.equal(remaining.length, 1);
        strict_1.default.equal(remaining[0].lastPushedAt, task.lastPushedAt);
    });
    (0, node_test_1.it)('fires a fresh cron task (no lastPushedAt) on the first run even with a rare schedule', async () => {
        const task = {
            id: 'cron3',
            title: 'Fresh cron',
            description: '',
            type: 'code',
            cron: '17 3 1 1 *',
        };
        (0, tasks_1.writeTasksFile)([task]);
        const provider = stubProvider();
        const result = await (0, tasks_1.processLocalTasks)(cfg(), provider);
        strict_1.default.deepEqual(result, { pushed: 1, skipped: 0 });
        strict_1.default.equal(provider.calls.length, 1);
        const remaining = (0, tasks_1.readTasksFile)();
        strict_1.default.equal(remaining.length, 1);
        strict_1.default.ok(typeof remaining[0].lastPushedAt === 'number');
    });
    (0, node_test_1.it)('keeps a failed task in the file and counts it as skipped', async () => {
        const task = {
            id: 'fail',
            title: 'Will fail',
            description: '',
            type: 'code',
        };
        (0, tasks_1.writeTasksFile)([task]);
        const provider = stubProvider({ fail: true });
        const result = await (0, tasks_1.processLocalTasks)(cfg(), provider);
        strict_1.default.deepEqual(result, { pushed: 0, skipped: 1 });
        const remaining = (0, tasks_1.readTasksFile)();
        strict_1.default.equal(remaining.length, 1);
        strict_1.default.equal(remaining[0].id, 'fail');
        strict_1.default.equal(remaining[0].lastPushedAt, undefined);
    });
    (0, node_test_1.it)('processes a mix of success and failure: pushes successes, retains failures', async () => {
        const tasks = [
            { id: 'ok', title: 'ok', description: '', type: 'code' },
            { id: 'bad', title: 'bad', description: '', type: 'code' },
        ];
        (0, tasks_1.writeTasksFile)(tasks);
        let callCount = 0;
        const provider = {
            calls: [],
            fetchTasks: async () => [],
            fetchTasksByStatus: async () => [],
            postComment: async () => { },
            getComments: async () => [],
            updateStatus: async () => { },
            createTask: async (params) => {
                provider.calls.push(params);
                callCount++;
                if (params.title === 'bad')
                    throw new Error('boom');
                return { id: `r${callCount}`, url: `u${callCount}` };
            },
        };
        const result = await (0, tasks_1.processLocalTasks)(cfg(), provider);
        strict_1.default.deepEqual(result, { pushed: 1, skipped: 1 });
        const remaining = (0, tasks_1.readTasksFile)();
        strict_1.default.equal(remaining.length, 1);
        strict_1.default.equal(remaining[0].id, 'bad');
    });
});
// ─── tasksPushCommand (end-to-end against LocalProvider) ─────────────────────
(0, node_test_1.describe)('tasksPushCommand', () => {
    let tmpDir;
    let origCwd;
    const envKeys = [
        'PROVIDER', 'CLICKUP_API_KEY', 'CLICKUP_TEAM_ID', 'CLICKUP_TAG',
        'NON_CODE_TAG', 'AIDEV_ENV_EXTEND', 'AGENTS',
    ];
    const saved = {};
    (0, node_test_1.beforeEach)(() => {
        for (const k of envKeys) {
            saved[k] = process.env[k];
            delete process.env[k];
        }
        tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aidev-push-cmd-'));
        origCwd = process.cwd();
        process.chdir(tmpDir);
        fs.writeFileSync(path.join(tmpDir, '.env.aidev'), 'PROVIDER=local\n', 'utf8');
        node_test_1.mock.method(logger_1.logger, 'info', () => { });
        node_test_1.mock.method(logger_1.logger, 'success', () => { });
        node_test_1.mock.method(logger_1.logger, 'warn', () => { });
        node_test_1.mock.method(logger_1.logger, 'error', () => { });
        node_test_1.mock.method(logger_1.logger, 'debug', () => { });
    });
    (0, node_test_1.afterEach)(() => {
        process.chdir(origCwd);
        fs.rmSync(tmpDir, { recursive: true, force: true });
        for (const k of envKeys) {
            if (saved[k] !== undefined)
                process.env[k] = saved[k];
            else
                delete process.env[k];
        }
        node_test_1.mock.restoreAll();
    });
    (0, node_test_1.it)('returns early when the tasks file is absent (no config load required)', async () => {
        const warnCalls = [];
        node_test_1.mock.method(logger_1.logger, 'warn', (m) => {
            warnCalls.push(m);
        });
        // No .env.aidev is read because we exit before loadConfig.
        fs.unlinkSync(path.join(tmpDir, '.env.aidev'));
        await (0, tasks_2.tasksPushCommand)();
        strict_1.default.ok(warnCalls.some((m) => m.includes('No local tasks found')));
        // Nothing was created.
        strict_1.default.equal(fs.existsSync(path.join(tmpDir, '.aidev')), false);
    });
    (0, node_test_1.it)('publishes queued tasks via the configured provider and clears one-shot entries', async () => {
        const tasks = [
            { id: 'a', title: 'Push me', description: 'body', type: 'code' },
        ];
        (0, tasks_1.writeTasksFile)(tasks);
        await (0, tasks_2.tasksPushCommand)();
        // LocalProvider writes to .aidev/tasks/open/<id>-<slug>.md
        const openDir = path.join(tmpDir, '.aidev', 'tasks', 'open');
        strict_1.default.ok(fs.existsSync(openDir), 'open folder should exist');
        const created = fs.readdirSync(openDir).filter((f) => f.endsWith('.md'));
        strict_1.default.equal(created.length, 1);
        const content = fs.readFileSync(path.join(openDir, created[0]), 'utf8');
        strict_1.default.ok(content.includes('title: Push me'));
        strict_1.default.ok(content.includes('body'));
        // One-shot entry removed from the queue.
        strict_1.default.deepEqual((0, tasks_1.readTasksFile)(), []);
    });
    (0, node_test_1.it)('preserves cron entries after publishing them', async () => {
        const tasks = [
            {
                id: 'cr', title: 'Recurring', description: '', type: 'code',
                cron: '* * * * *',
            },
        ];
        (0, tasks_1.writeTasksFile)(tasks);
        await (0, tasks_2.tasksPushCommand)();
        const remaining = (0, tasks_1.readTasksFile)();
        strict_1.default.equal(remaining.length, 1);
        strict_1.default.equal(remaining[0].id, 'cr');
        strict_1.default.ok(typeof remaining[0].lastPushedAt === 'number');
    });
});
//# sourceMappingURL=tasks.test.js.map