import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { cleanTmpDir } from '../test-utils.js';
import { ejectCommand, scrubPostCheckoutHook, scrubPostMergeHook } from './eject.js';
import { SKILL_MARKER_END, SKILL_MARKER_START } from './init-templates.js';
function makeTmpDir() {
    return fs.mkdtempSync(path.join(os.tmpdir(), 'totem-eject-'));
}
describe('ejectCommand', () => {
    let cwd;
    let originalCwd;
    beforeEach(() => {
        cwd = makeTmpDir();
        originalCwd = process.cwd();
        process.chdir(cwd);
        // Create .git so it looks like a repo
        fs.mkdirSync(path.join(cwd, '.git', 'hooks'), { recursive: true });
    });
    afterEach(() => {
        process.chdir(originalCwd);
        cleanTmpDir(cwd);
    });
    it('removes post-merge hook when it only contains Totem content', async () => {
        const hookPath = path.join(cwd, '.git', 'hooks', 'post-merge');
        fs.writeFileSync(hookPath, '#!/bin/sh\n# [totem] post-merge hook — background re-index after pull/merge.\n\necho "[totem] Triggering background re-index..."\n(pnpm exec totem sync --incremental > .git/totem-sync.log 2>&1) &\n');
        await ejectCommand({ force: true });
        expect(fs.existsSync(hookPath)).toBe(false);
    });
    it('preserves non-Totem content in post-merge hook', async () => {
        const hookPath = path.join(cwd, '.git', 'hooks', 'post-merge');
        fs.writeFileSync(hookPath, '#!/bin/sh\necho "my custom hook"\n# [totem] post-merge hook — background re-index after pull/merge.\n\necho "[totem] Triggering background re-index..."\n(pnpm exec totem sync --incremental > .git/totem-sync.log 2>&1) &\n');
        await ejectCommand({ force: true });
        expect(fs.existsSync(hookPath)).toBe(true);
        const content = fs.readFileSync(hookPath, 'utf-8');
        expect(content).toContain('my custom hook');
        expect(content).not.toContain('[totem]');
    });
    it('removes scaffolded files with Totem marker', async () => {
        const hookDir = path.join(cwd, '.gemini', 'hooks');
        fs.mkdirSync(hookDir, { recursive: true });
        fs.writeFileSync(path.join(hookDir, 'SessionStart.js'), '// [totem] auto-generated — Gemini CLI SessionStart hook\nconsole.log("hi");');
        await ejectCommand({ force: true });
        expect(fs.existsSync(path.join(hookDir, 'SessionStart.js'))).toBe(false);
    });
    it('does not remove files without Totem marker', async () => {
        const hookDir = path.join(cwd, '.gemini', 'hooks');
        fs.mkdirSync(hookDir, { recursive: true });
        fs.writeFileSync(path.join(hookDir, 'SessionStart.js'), 'console.log("user hook");');
        await ejectCommand({ force: true });
        expect(fs.existsSync(path.join(hookDir, 'SessionStart.js'))).toBe(true);
    });
    it('scrubs AI reflex block from CLAUDE.md', async () => {
        const claudePath = path.join(cwd, 'CLAUDE.md');
        fs.writeFileSync(claudePath, '# My Project\n\nSome instructions.\n\n## Totem AI Integration (Auto-Generated)\nYou have access to the Totem MCP.\n\n### Memory Reflexes\n1. Pull before planning.\n');
        await ejectCommand({ force: true });
        const content = fs.readFileSync(claudePath, 'utf-8');
        expect(content).toContain('# My Project');
        expect(content).toContain('Some instructions.');
        expect(content).not.toContain('Totem AI Integration');
        expect(content).not.toContain('Memory Reflexes');
    });
    it('deletes .lancedb, .totem, and totem.config.ts', async () => {
        fs.mkdirSync(path.join(cwd, '.lancedb'), { recursive: true });
        fs.mkdirSync(path.join(cwd, '.totem'), { recursive: true });
        fs.writeFileSync(path.join(cwd, 'totem.config.ts'), 'export default {};');
        await ejectCommand({ force: true });
        expect(fs.existsSync(path.join(cwd, '.lancedb'))).toBe(false);
        expect(fs.existsSync(path.join(cwd, '.totem'))).toBe(false);
        expect(fs.existsSync(path.join(cwd, 'totem.config.ts'))).toBe(false);
    });
    it('scrubs Claude settings.local.json PreToolUse entry', async () => {
        const settingsDir = path.join(cwd, '.claude');
        fs.mkdirSync(settingsDir, { recursive: true });
        const settings = {
            permissions: { allow: ['Bash'] },
            hooks: {
                PreToolUse: [
                    {
                        matcher: 'Bash',
                        hooks: [{ type: 'command', command: 'node .totem/hooks/shield-gate.cjs' }],
                    },
                    { matcher: 'Write', hooks: [{ type: 'command', command: 'echo custom' }] },
                ],
            },
        };
        fs.writeFileSync(path.join(settingsDir, 'settings.local.json'), JSON.stringify(settings));
        await ejectCommand({ force: true });
        const updated = JSON.parse(fs.readFileSync(path.join(settingsDir, 'settings.local.json'), 'utf-8'));
        expect(updated.hooks.PreToolUse).toHaveLength(1);
        expect(updated.hooks.PreToolUse[0].matcher).toBe('Write');
        expect(updated.permissions).toBeDefined();
    });
    it('handles clean project with nothing to remove', async () => {
        // No Totem artifacts exist
        await ejectCommand({ force: true });
        // Should not throw
    });
    // Phase C slice 1 (mmnto-ai/totem#1845) added committed `.claude/settings.json`
    // SessionStart eject parity. Same PR closes mmnto-ai/totem#1852 by also
    // scrubbing the Phase B PreWriteShield entry from the same file. Both
    // entries must be removed without disturbing user-defined hooks.
    it('scrubs SessionStart entry from committed Claude settings.json', async () => {
        const settingsDir = path.join(cwd, '.claude');
        fs.mkdirSync(settingsDir, { recursive: true });
        fs.writeFileSync(path.join(settingsDir, 'settings.json'), JSON.stringify({
            hooks: {
                SessionStart: [
                    { hooks: [{ type: 'command', command: 'node .claude/hooks/SessionStart.cjs' }] },
                    { hooks: [{ type: 'command', command: 'echo "user-defined"' }] },
                ],
            },
        }, null, 2));
        await ejectCommand({ force: true });
        const updated = JSON.parse(fs.readFileSync(path.join(settingsDir, 'settings.json'), 'utf-8'));
        expect(updated.hooks.SessionStart).toHaveLength(1);
        expect(updated.hooks.SessionStart[0].hooks[0].command).toBe('echo "user-defined"');
    });
    it('scrubs PreWriteShield entry from committed Claude settings.json (closes #1852)', async () => {
        const settingsDir = path.join(cwd, '.claude');
        fs.mkdirSync(settingsDir, { recursive: true });
        fs.writeFileSync(path.join(settingsDir, 'settings.json'), JSON.stringify({
            hooks: {
                PreToolUse: [
                    {
                        matcher: 'Write|Edit',
                        hooks: [{ type: 'command', command: 'node .claude/hooks/PreWriteShield.cjs' }],
                    },
                    {
                        matcher: 'Write',
                        hooks: [{ type: 'command', command: 'echo "user-defined"' }],
                    },
                ],
            },
        }, null, 2));
        await ejectCommand({ force: true });
        const updated = JSON.parse(fs.readFileSync(path.join(settingsDir, 'settings.json'), 'utf-8'));
        expect(updated.hooks.PreToolUse).toHaveLength(1);
        expect(updated.hooks.PreToolUse[0].matcher).toBe('Write');
    });
    it('scrubs both PreWriteShield and SessionStart entries in one pass, leaving file empty', async () => {
        const settingsDir = path.join(cwd, '.claude');
        fs.mkdirSync(settingsDir, { recursive: true });
        fs.writeFileSync(path.join(settingsDir, 'settings.json'), JSON.stringify({
            hooks: {
                PreToolUse: [
                    {
                        matcher: 'Write|Edit',
                        hooks: [{ type: 'command', command: 'node .claude/hooks/PreWriteShield.cjs' }],
                    },
                ],
                SessionStart: [
                    { hooks: [{ type: 'command', command: 'node .claude/hooks/SessionStart.cjs' }] },
                ],
            },
        }, null, 2));
        await ejectCommand({ force: true });
        // Both entries gone → both arrays empty → both keys deleted → hooks
        // empty → hooks key deleted → file empty {} → file unlinked.
        expect(fs.existsSync(path.join(settingsDir, 'settings.json'))).toBe(false);
    });
    it('removes scaffolded .claude/hooks/SessionStart.cjs and PreWriteShield.cjs files', async () => {
        const hookDir = path.join(cwd, '.claude', 'hooks');
        fs.mkdirSync(hookDir, { recursive: true });
        fs.writeFileSync(path.join(hookDir, 'SessionStart.cjs'), '// [totem] auto-generated — Claude Code SessionStart hook\nconsole.log("hi");');
        fs.writeFileSync(path.join(hookDir, 'PreWriteShield.cjs'), '// [totem] auto-generated — Claude Code PreWriteShield hook\nconsole.log("hi");');
        await ejectCommand({ force: true });
        expect(fs.existsSync(path.join(hookDir, 'SessionStart.cjs'))).toBe(false);
        expect(fs.existsSync(path.join(hookDir, 'PreWriteShield.cjs'))).toBe(false);
    });
    it('does not remove .claude/hooks/SessionStart.cjs if user-authored (no Totem marker)', async () => {
        const hookDir = path.join(cwd, '.claude', 'hooks');
        fs.mkdirSync(hookDir, { recursive: true });
        fs.writeFileSync(path.join(hookDir, 'SessionStart.cjs'), 'console.log("user-authored, no marker");');
        await ejectCommand({ force: true });
        expect(fs.existsSync(path.join(hookDir, 'SessionStart.cjs'))).toBe(true);
    });
    it('skips committed Claude settings.json when JSON root is not an object (e.g., array, string, null)', async () => {
        const settingsDir = path.join(cwd, '.claude');
        fs.mkdirSync(settingsDir, { recursive: true });
        // A `.claude/settings.json` consisting of just `[]` parses successfully
        // but isn't a config object — eject must skip it cleanly without
        // crashing on the implicit Object property access.
        fs.writeFileSync(path.join(settingsDir, 'settings.json'), '[]');
        await ejectCommand({ force: true });
        // File should still exist (eject skipped, didn't mutate)
        expect(fs.existsSync(path.join(settingsDir, 'settings.json'))).toBe(true);
        expect(fs.readFileSync(path.join(settingsDir, 'settings.json'), 'utf-8')).toBe('[]');
    });
    it('skips committed Claude settings.json when hooks key is malformed (string instead of object)', async () => {
        const settingsDir = path.join(cwd, '.claude');
        fs.mkdirSync(settingsDir, { recursive: true });
        fs.writeFileSync(path.join(settingsDir, 'settings.json'), JSON.stringify({ hooks: 'unexpected-string' }));
        await ejectCommand({ force: true });
        // File should still exist (eject skipped, didn't crash)
        expect(fs.existsSync(path.join(settingsDir, 'settings.json'))).toBe(true);
    });
    it('skips entries when hooks.PreToolUse is a non-array (e.g., null) without crashing', async () => {
        const settingsDir = path.join(cwd, '.claude');
        fs.mkdirSync(settingsDir, { recursive: true });
        fs.writeFileSync(path.join(settingsDir, 'settings.json'), JSON.stringify({
            hooks: {
                PreToolUse: null,
                SessionStart: [
                    { hooks: [{ type: 'command', command: 'node .claude/hooks/SessionStart.cjs' }] },
                ],
            },
        }));
        await ejectCommand({ force: true });
        // SessionStart still scrubbed even though PreToolUse was malformed.
        const updated = JSON.parse(fs.readFileSync(path.join(settingsDir, 'settings.json'), 'utf-8'));
        // After mutation: SessionStart removed, PreToolUse:null preserved as
        // unexpected-shape input. The file may or may not exist depending on
        // pruning; if it does, it must not contain the SessionStart entry.
        expect(updated.hooks?.SessionStart).toBeUndefined();
    });
    it('preserves user-defined permissions and unrelated keys in committed Claude settings.json', async () => {
        const settingsDir = path.join(cwd, '.claude');
        fs.mkdirSync(settingsDir, { recursive: true });
        fs.writeFileSync(path.join(settingsDir, 'settings.json'), JSON.stringify({
            permissions: { allow: ['Bash'] },
            hooks: {
                SessionStart: [
                    { hooks: [{ type: 'command', command: 'node .claude/hooks/SessionStart.cjs' }] },
                ],
            },
        }, null, 2));
        await ejectCommand({ force: true });
        const updated = JSON.parse(fs.readFileSync(path.join(settingsDir, 'settings.json'), 'utf-8'));
        expect(updated.permissions).toBeDefined();
        expect(updated.permissions.allow).toEqual(['Bash']);
        expect(updated.hooks).toBeUndefined();
    });
    it('removes post-merge hook with new conditional format (if/fi block)', async () => {
        const hookPath = path.join(cwd, '.git', 'hooks', 'post-merge');
        fs.writeFileSync(hookPath, `#!/bin/sh
# [totem] post-merge hook — background re-index after pull/merge.

# Only sync when lessons changed (suppress errors if ORIG_HEAD is missing)
if git diff-tree -r --name-only ORIG_HEAD HEAD 2>/dev/null | grep -q '\\.totem/lessons/'; then
  (pnpm exec totem sync --incremental --quiet > .git/totem-sync.log 2>&1) &
fi
# [totem] end post-merge
`);
        await ejectCommand({ force: true });
        expect(fs.existsSync(hookPath)).toBe(false);
    });
    it('preserves non-Totem content when scrubbing new conditional format', async () => {
        const hookPath = path.join(cwd, '.git', 'hooks', 'post-merge');
        fs.writeFileSync(hookPath, `#!/bin/sh
echo "my custom hook"
# [totem] post-merge hook — background re-index after pull/merge.

# Only sync when lessons changed (suppress errors if ORIG_HEAD is missing)
if git diff-tree -r --name-only ORIG_HEAD HEAD 2>/dev/null | grep -q '\\.totem/lessons/'; then
  (pnpm exec totem sync --incremental --quiet > .git/totem-sync.log 2>&1) &
fi
# [totem] end post-merge
`);
        await ejectCommand({ force: true });
        expect(fs.existsSync(hookPath)).toBe(true);
        const content = fs.readFileSync(hookPath, 'utf-8');
        expect(content).toContain('my custom hook');
        expect(content).not.toContain('[totem]');
        expect(content).not.toContain('ORIG_HEAD');
        expect(content).not.toContain('fi');
    });
});
// ─── scrubPostMergeHook (direct unit tests) ─────────
describe('scrubPostMergeHook', () => {
    let cwd;
    beforeEach(() => {
        cwd = makeTmpDir();
        fs.mkdirSync(path.join(cwd, '.git', 'hooks'), { recursive: true });
    });
    afterEach(() => {
        cleanTmpDir(cwd);
    });
    it('scrubs new conditional hook format (if/fi block)', () => {
        const hookPath = path.join(cwd, '.git', 'hooks', 'post-merge');
        fs.writeFileSync(hookPath, `#!/bin/sh
# [totem] post-merge hook — background re-index after pull/merge.

# Only sync when lessons changed (suppress errors if ORIG_HEAD is missing)
if git diff-tree -r --name-only ORIG_HEAD HEAD 2>/dev/null | grep -q '\\.totem/lessons/'; then
  (pnpm exec totem sync --incremental --quiet > .git/totem-sync.log 2>&1) &
fi
# [totem] end post-merge
`);
        const summary = { removed: [], scrubbed: [], skipped: [] };
        scrubPostMergeHook(cwd, summary);
        expect(fs.existsSync(hookPath)).toBe(false);
        expect(summary.removed).toContain('.git/hooks/post-merge');
    });
    it('scrubs old unconditional hook format', () => {
        const hookPath = path.join(cwd, '.git', 'hooks', 'post-merge');
        fs.writeFileSync(hookPath, '#!/bin/sh\n# [totem] post-merge hook — background re-index after pull/merge.\n\necho "[totem] Triggering background re-index..."\n(pnpm exec totem sync --incremental > .git/totem-sync.log 2>&1) &\n');
        const summary = { removed: [], scrubbed: [], skipped: [] };
        scrubPostMergeHook(cwd, summary);
        expect(fs.existsSync(hookPath)).toBe(false);
        expect(summary.removed).toContain('.git/hooks/post-merge');
    });
    it('preserves non-totem content when scrubbing', () => {
        const hookPath = path.join(cwd, '.git', 'hooks', 'post-merge');
        fs.writeFileSync(hookPath, `#!/bin/sh
echo "deploy notification"
# [totem] post-merge hook — background re-index after pull/merge.

# Only sync when lessons changed (suppress errors if ORIG_HEAD is missing)
if git diff-tree -r --name-only ORIG_HEAD HEAD 2>/dev/null | grep -q '\\.totem/lessons/'; then
  (pnpm exec totem sync --incremental --quiet > .git/totem-sync.log 2>&1) &
fi
# [totem] end post-merge
`);
        const summary = { removed: [], scrubbed: [], skipped: [] };
        scrubPostMergeHook(cwd, summary);
        expect(fs.existsSync(hookPath)).toBe(true);
        const content = fs.readFileSync(hookPath, 'utf-8');
        expect(content).toContain('deploy notification');
        expect(content).not.toContain('[totem]');
        expect(summary.scrubbed).toContain('.git/hooks/post-merge');
    });
});
// ─── scrubPostCheckoutHook (direct unit tests) ──────
describe('scrubPostCheckoutHook', () => {
    let cwd;
    beforeEach(() => {
        cwd = makeTmpDir();
        fs.mkdirSync(path.join(cwd, '.git', 'hooks'), { recursive: true });
    });
    afterEach(() => {
        cleanTmpDir(cwd);
    });
    it('removes post-checkout hook with end marker', () => {
        const hookPath = path.join(cwd, '.git', 'hooks', 'post-checkout');
        fs.writeFileSync(hookPath, `#!/bin/sh
# [totem] post-checkout hook — background re-index on branch switch.

# $1 = previous HEAD, $2 = new HEAD, $3 = checkout type (1=branch, 0=file)
# Skip file checkouts — only sync on branch switches
if [ "$3" = "0" ]; then
  exit 0
fi

# Only sync when .totem/ files differ between branches
if git diff --name-only "$1" "$2" 2>/dev/null | grep -q '\\.totem/'; then
  (pnpm exec totem sync --incremental --quiet > .git/totem-sync.log 2>&1) &
fi
# [totem] end post-checkout
`);
        const summary = { removed: [], scrubbed: [], skipped: [] };
        scrubPostCheckoutHook(cwd, summary);
        expect(fs.existsSync(hookPath)).toBe(false);
        expect(summary.removed).toContain('.git/hooks/post-checkout');
    });
    it('preserves non-Totem content in post-checkout hook', () => {
        const hookPath = path.join(cwd, '.git', 'hooks', 'post-checkout');
        fs.writeFileSync(hookPath, `#!/bin/sh
echo "deploy notification"
# [totem] post-checkout hook — background re-index on branch switch.

# $1 = previous HEAD, $2 = new HEAD, $3 = checkout type (1=branch, 0=file)
if [ "$3" = "0" ]; then
  exit 0
fi

if git diff --name-only "$1" "$2" 2>/dev/null | grep -q '\\.totem/'; then
  (pnpm exec totem sync --incremental --quiet > .git/totem-sync.log 2>&1) &
fi
# [totem] end post-checkout
`);
        const summary = { removed: [], scrubbed: [], skipped: [] };
        scrubPostCheckoutHook(cwd, summary);
        expect(fs.existsSync(hookPath)).toBe(true);
        const content = fs.readFileSync(hookPath, 'utf-8');
        expect(content).toContain('deploy notification');
        expect(content).not.toContain('[totem]');
        expect(summary.scrubbed).toContain('.git/hooks/post-checkout');
    });
});
describe('eject — distributed Claude skills (mmnto-ai/totem#1890 Phase C slice 3)', () => {
    let cwd;
    let originalCwd;
    beforeEach(() => {
        cwd = makeTmpDir();
        originalCwd = process.cwd();
        process.chdir(cwd);
        fs.mkdirSync(path.join(cwd, '.git', 'hooks'), { recursive: true });
    });
    afterEach(() => {
        process.chdir(originalCwd);
        cleanTmpDir(cwd);
    });
    function writeSkill(name, body) {
        const filePath = path.join(cwd, '.claude', 'skills', name, 'SKILL.md');
        fs.mkdirSync(path.dirname(filePath), { recursive: true });
        fs.writeFileSync(filePath, body, 'utf-8');
        return filePath;
    }
    it('removes signoff + review-reply SKILL.md files when markers are present', async () => {
        const signoffPath = writeSkill('signoff', `---\nname: signoff\n---\n\n${SKILL_MARKER_START}\nbody\n${SKILL_MARKER_END}\n`);
        const reviewReplyPath = writeSkill('review-reply', `---\nname: review-reply\n---\n\n${SKILL_MARKER_START}\nbody\n${SKILL_MARKER_END}\n`);
        await ejectCommand({ force: true });
        expect(fs.existsSync(signoffPath)).toBe(false);
        expect(fs.existsSync(reviewReplyPath)).toBe(false);
    });
    it('prunes empty per-skill directories and the skills root after removal', async () => {
        writeSkill('signoff', `---\nname: signoff\n---\n\n${SKILL_MARKER_START}\nbody\n${SKILL_MARKER_END}\n`);
        writeSkill('review-reply', `---\nname: review-reply\n---\n\n${SKILL_MARKER_START}\nbody\n${SKILL_MARKER_END}\n`);
        await ejectCommand({ force: true });
        expect(fs.existsSync(path.join(cwd, '.claude', 'skills', 'signoff'))).toBe(false);
        expect(fs.existsSync(path.join(cwd, '.claude', 'skills', 'review-reply'))).toBe(false);
        expect(fs.existsSync(path.join(cwd, '.claude', 'skills'))).toBe(false);
    });
    it('preserves user-authored skill files without markers', async () => {
        const customPath = writeSkill('signoff', '# my custom signoff\n\ndo it differently\n');
        await ejectCommand({ force: true });
        expect(fs.existsSync(customPath)).toBe(true);
        expect(fs.readFileSync(customPath, 'utf-8')).toContain('do it differently');
    });
    it('preserves the skills root if a non-distributed skill remains', async () => {
        writeSkill('signoff', `---\nname: signoff\n---\n\n${SKILL_MARKER_START}\nbody\n${SKILL_MARKER_END}\n`);
        // User has their own skill not managed by totem
        const customSkill = writeSkill('panel-audit', '# Custom panel audit skill\n');
        await ejectCommand({ force: true });
        expect(fs.existsSync(path.join(cwd, '.claude', 'skills', 'signoff'))).toBe(false);
        expect(fs.existsSync(customSkill)).toBe(true);
        expect(fs.existsSync(path.join(cwd, '.claude', 'skills'))).toBe(true);
    });
});
//# sourceMappingURL=eject.test.js.map