"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;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
/*
 * Story 3.4 — HostInstaller uninstall regression test.
 *
 * Asserts:
 * 1. Install then uninstall — every Story-3.3 artifact (5 agents, 4 commands, MCP snippet) is removed
 * 2. Re-uninstall is idempotent — all actions are 'skipped-not-found'
 * 3. User-edit preservation via .bak — install → user edits → reinstall (creates .bak) → uninstall
 *    restores the user's edit by renaming .bak back over the destination (NOT deleted)
 * 4. Untouched user files — files in .claude/agents/ without the openlife- prefix survive
 * 5. uninstallForGeminiCli throws HostNotYetSupportedError
 * 6. uninstallForCodex throws HostNotYetSupportedError
 * 7. Dispatcher uninstall('claude-code', ...) works and rejects unsupported hosts
 * 8. CLI command `openlife system uninstall --host claude-code` is wired and prints JSON
 */
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const os = __importStar(require("os"));
const child_process_1 = require("child_process");
const HostInstaller_1 = require("./cli/HostInstaller");
function assert(cond, msg) {
    if (!cond)
        throw new Error(`ASSERTION_FAILED: ${msg}`);
}
function mkTemp(prefix = 'openlife-hostuninstaller-test-') {
    return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
const EXPECTED_AGENTS = [
    'openlife-maestro.md',
    'openlife-lyra.md',
    'openlife-forge.md',
    'openlife-atlas.md',
    'openlife-genesis.md'
];
const EXPECTED_COMMANDS = ['status.md', 'ask.md', 'doctor.md', 'dream.md'];
function testUninstallRemovesAllInstalledArtifacts() {
    const tempRoot = mkTemp();
    try {
        const installer = new HostInstaller_1.HostInstaller();
        installer.installForClaudeCode({ targetRoot: tempRoot });
        // Sanity: install really put the files there.
        for (const agent of EXPECTED_AGENTS) {
            assert(fs.existsSync(path.join(tempRoot, '.claude', 'agents', agent)), `precondition: ${agent} should exist after install`);
        }
        // Simulate pre-existing .openlife/ runtime state (created by InstallFlow, not HostInstaller).
        // This ensures the uninstaller doesn't wipe it.
        const stateDirPre = path.join(tempRoot, '.openlife');
        if (!fs.existsSync(stateDirPre))
            fs.mkdirSync(stateDirPre, { recursive: true });
        const result = installer.uninstallForClaudeCode({ targetRoot: tempRoot });
        assert(result.host === 'claude-code', `host should be claude-code; got ${result.host}`);
        assert(result.targetRoot === tempRoot, 'targetRoot should round-trip');
        assert(Array.isArray(result.files) && result.files.length > 0, 'files[] should be populated');
        // Every agent file should be gone.
        for (const agent of EXPECTED_AGENTS) {
            const dst = path.join(tempRoot, '.claude', 'agents', agent);
            assert(!fs.existsSync(dst), `agent should be removed: ${dst}`);
        }
        // Every command file should be gone.
        for (const cmd of EXPECTED_COMMANDS) {
            const dst = path.join(tempRoot, '.claude', 'commands', 'openlife', cmd);
            assert(!fs.existsSync(dst), `command should be removed: ${dst}`);
        }
        // MCP snippet gone.
        const mcpDst = path.join(tempRoot, '.openlife', 'install-mcp-snippet.json');
        assert(!fs.existsSync(mcpDst), `mcp snippet should be removed: ${mcpDst}`);
        // .openlife/ itself MUST survive (runtime state).
        const stateDir = path.join(tempRoot, '.openlife');
        assert(fs.existsSync(stateDir), '.openlife/ runtime state directory must NOT be removed');
        // The openlife/ commands namespace directory should be removed (it was all ours and is now empty).
        const cmdsNs = path.join(tempRoot, '.claude', 'commands', 'openlife');
        assert(!fs.existsSync(cmdsNs), `empty openlife/ commands namespace should be removed: ${cmdsNs}`);
        // Count dynamically from dist-templates/claude-code/. The installer and
        // uninstaller are symmetric and recursive: every .md under commands/openlife/
        // (top-level + nested agents/ and flow/ subnamespaces) is installed and then
        // removed, plus the starter agents and the MCP snippet.
        const distTemplateRoot = path.join(__dirname, '..', 'dist-templates', 'claude-code');
        const countMdRecursive = (dir) => {
            if (!fs.existsSync(dir))
                return 0;
            let n = 0;
            for (const entry of fs.readdirSync(dir)) {
                const full = path.join(dir, entry);
                if (fs.statSync(full).isDirectory())
                    n += countMdRecursive(full);
                else if (entry.endsWith('.md'))
                    n++;
            }
            return n;
        };
        const agentCount = fs.existsSync(path.join(distTemplateRoot, 'agents'))
            ? fs.readdirSync(path.join(distTemplateRoot, 'agents')).filter((f) => f.endsWith('.md')).length
            : 0;
        const commandCount = countMdRecursive(path.join(distTemplateRoot, 'commands', 'openlife'));
        // MCP snippet is gated behind OPENLIFE_PREVIEW_MCP=1, so not installed/removed by default.
        const expectedRemoved = agentCount + commandCount;
        const removed = result.files.filter((f) => f.action === 'removed').length;
        assert(removed === expectedRemoved, `expected ${expectedRemoved} 'removed' actions (${agentCount} agents + ${commandCount} commands); got ${removed}: ` +
            JSON.stringify(result.files.map((f) => `${path.basename(f.destination)}:${f.action}`)));
    }
    finally {
        fs.rmSync(tempRoot, { recursive: true, force: true });
    }
}
function testIdempotentReUninstall() {
    const tempRoot = mkTemp();
    try {
        const installer = new HostInstaller_1.HostInstaller();
        installer.installForClaudeCode({ targetRoot: tempRoot });
        installer.uninstallForClaudeCode({ targetRoot: tempRoot });
        const second = installer.uninstallForClaudeCode({ targetRoot: tempRoot });
        // On the second uninstall, everything should be 'skipped-not-found'.
        const skipped = second.files.filter((f) => f.action === 'skipped-not-found').length;
        assert(skipped === second.files.length, `all actions should be 'skipped-not-found' on re-uninstall; got ${skipped}/${second.files.length}: ` +
            JSON.stringify(second.files.map((f) => f.action)));
    }
    finally {
        fs.rmSync(tempRoot, { recursive: true, force: true });
    }
}
function testUserEditPreservedViaBakRestore() {
    const tempRoot = mkTemp();
    try {
        const installer = new HostInstaller_1.HostInstaller();
        installer.installForClaudeCode({ targetRoot: tempRoot });
        // User edits the agent. Then reinstalling will move the edit to .bak and overwrite.
        const target = path.join(tempRoot, '.claude', 'agents', 'openlife-maestro.md');
        const userEditContent = '# user-edited content — must survive uninstall\n';
        fs.writeFileSync(target, userEditContent, 'utf-8');
        const second = installer.installForClaudeCode({ targetRoot: tempRoot });
        const updateAction = second.files.find((f) => f.destination === target);
        assert(updateAction !== undefined && updateAction.action === 'updated', `precondition: reinstall after user-edit should report 'updated'; got ${JSON.stringify(updateAction)}`);
        const bakPath = `${target}.bak`;
        assert(fs.existsSync(bakPath), `precondition: .bak should exist at ${bakPath}`);
        // Uninstall — .bak should be renamed back over the destination; user's edit survives.
        const result = installer.uninstallForClaudeCode({ targetRoot: tempRoot });
        const restoredAction = result.files.find((f) => f.destination === target);
        assert(restoredAction !== undefined, `expected an action for ${target} in uninstall result`);
        assert(restoredAction.action === 'restored-from-bak', `expected 'restored-from-bak'; got '${restoredAction.action}'`);
        assert(restoredAction.backupPath === bakPath, `restoredAction.backupPath should be ${bakPath}; got ${restoredAction.backupPath}`);
        // The destination must exist again WITH the user's content.
        assert(fs.existsSync(target), `restored file should exist at ${target}`);
        const finalContent = fs.readFileSync(target, 'utf-8');
        assert(finalContent === userEditContent, `user's edit must survive uninstall; got: ${JSON.stringify(finalContent)}`);
        // And the .bak should be gone (it was renamed, not deleted).
        assert(!fs.existsSync(bakPath), `.bak should be consumed by rename; still present at ${bakPath}`);
    }
    finally {
        fs.rmSync(tempRoot, { recursive: true, force: true });
    }
}
function testUntouchedUserFilesSurvive() {
    const tempRoot = mkTemp();
    try {
        const installer = new HostInstaller_1.HostInstaller();
        installer.installForClaudeCode({ targetRoot: tempRoot });
        // Drop a user-owned file (no openlife- prefix) in .claude/agents/.
        const userFile = path.join(tempRoot, '.claude', 'agents', 'user-custom.md');
        fs.writeFileSync(userFile, '# my custom agent — leave me alone\n', 'utf-8');
        installer.uninstallForClaudeCode({ targetRoot: tempRoot });
        assert(fs.existsSync(userFile), `user-owned file MUST NOT be removed: ${userFile}`);
        const content = fs.readFileSync(userFile, 'utf-8');
        assert(content === '# my custom agent — leave me alone\n', `user file content must be untouched; got: ${JSON.stringify(content)}`);
        // And the parent .claude/agents/ should still exist because it contains user-custom.md.
        assert(fs.existsSync(path.join(tempRoot, '.claude', 'agents')), '.claude/agents/ should survive if it still contains user files');
    }
    finally {
        fs.rmSync(tempRoot, { recursive: true, force: true });
    }
}
function testGeminiCliUninstallReversible() {
    // Sprint 2 / Story 9.3 — gemini-cli uninstall is now a real reversible
    // operation. The detailed coverage lives in
    // test_host_installers_gemini_codex.ts; here we just assert it no longer
    // throws HostNotYetSupportedError.
    const tempRoot = mkTemp();
    try {
        const installer = new HostInstaller_1.HostInstaller();
        installer.installForGeminiCli({ targetRoot: tempRoot });
        let caught = null;
        try {
            installer.uninstallForGeminiCli({ targetRoot: tempRoot });
        }
        catch (err) {
            caught = err;
        }
        assert(!(caught instanceof HostInstaller_1.HostNotYetSupportedError), 'gemini-cli uninstall should not throw HostNotYetSupportedError (Story 9.3)');
    }
    finally {
        fs.rmSync(tempRoot, { recursive: true, force: true });
    }
}
function testCodexUninstallReversible() {
    // Mirror of the gemini-cli check above (Story 9.4).
    const tempRoot = mkTemp();
    try {
        const installer = new HostInstaller_1.HostInstaller();
        installer.installForCodex({ targetRoot: tempRoot });
        let caught = null;
        try {
            installer.uninstallForCodex({ targetRoot: tempRoot });
        }
        catch (err) {
            caught = err;
        }
        assert(!(caught instanceof HostInstaller_1.HostNotYetSupportedError), 'codex uninstall should not throw HostNotYetSupportedError (Story 9.4)');
    }
    finally {
        fs.rmSync(tempRoot, { recursive: true, force: true });
    }
}
function testDispatcherUninstallDispatchesClaudeCode() {
    const tempRoot = mkTemp();
    try {
        const installer = new HostInstaller_1.HostInstaller();
        installer.install('claude-code', { targetRoot: tempRoot });
        const result = installer.uninstall('claude-code', { targetRoot: tempRoot });
        assert(result.host === 'claude-code', `dispatcher should produce claude-code result; got ${result.host}`);
        assert(!fs.existsSync(path.join(tempRoot, '.claude', 'agents', 'openlife-maestro.md')), 'dispatcher should have removed agent files');
        // Sprint 2 / Stories 9.3 + 9.4 — dispatcher delegates uninstall to real
        // implementations for gemini-cli and codex. We install + uninstall a
        // fresh tempRoot to verify the dispatcher does not throw.
        const gtempRoot = mkTemp();
        try {
            installer.installForGeminiCli({ targetRoot: gtempRoot });
            const geminiResult = installer.uninstall('gemini-cli', { targetRoot: gtempRoot });
            assert(geminiResult.host === 'gemini-cli', `dispatcher should produce gemini-cli uninstall result; got ${geminiResult.host}`);
        }
        finally {
            fs.rmSync(gtempRoot, { recursive: true, force: true });
        }
        const ctempRoot = mkTemp();
        try {
            installer.installForCodex({ targetRoot: ctempRoot });
            const codexResult = installer.uninstall('codex', { targetRoot: ctempRoot });
            assert(codexResult.host === 'codex', `dispatcher should produce codex uninstall result; got ${codexResult.host}`);
        }
        finally {
            fs.rmSync(ctempRoot, { recursive: true, force: true });
        }
    }
    finally {
        fs.rmSync(tempRoot, { recursive: true, force: true });
    }
}
function testCliCommandWired() {
    // Spawn the CLI in a temp dir as cwd, after installing into it.
    // We DO NOT rely on end-to-end correctness here — unit tests above cover that. This test
    // only verifies the command is registered and prints a parseable JSON envelope.
    const tempRoot = mkTemp();
    try {
        const installer = new HostInstaller_1.HostInstaller();
        installer.installForClaudeCode({ targetRoot: tempRoot });
        const distIndex = path.resolve(__dirname, 'index.js');
        assert(fs.existsSync(distIndex), `precondition: ${distIndex} must exist (run npm run build)`);
        const proc = (0, child_process_1.spawnSync)(process.execPath, [distIndex, 'system', 'uninstall', '--host', 'claude-code'], { cwd: tempRoot, encoding: 'utf-8', timeout: 30000 });
        assert(proc.status === 0, `CLI exit code should be 0; got ${proc.status}. stderr: ${proc.stderr}`);
        // stdout should be valid JSON with ok:true.
        const stdout = proc.stdout || '';
        let parsed;
        try {
            parsed = JSON.parse(stdout);
        }
        catch (e) {
            throw new Error(`CLI stdout should be JSON; got: ${stdout.slice(0, 400)}`);
        }
        assert(parsed.ok === true, `JSON should have ok:true; got: ${JSON.stringify(parsed).slice(0, 200)}`);
        assert(parsed.host === 'claude-code', `JSON.host should be claude-code; got ${parsed.host}`);
        assert(Array.isArray(parsed.files), 'JSON.files should be an array');
    }
    finally {
        fs.rmSync(tempRoot, { recursive: true, force: true });
    }
}
async function main() {
    testUninstallRemovesAllInstalledArtifacts();
    testIdempotentReUninstall();
    testUserEditPreservedViaBakRestore();
    testUntouchedUserFilesSurvive();
    testGeminiCliUninstallReversible();
    testCodexUninstallReversible();
    testDispatcherUninstallDispatchesClaudeCode();
    testCliCommandWired();
    console.log('TEST_HOST_UNINSTALLER_OK');
}
main().catch((err) => {
    console.error('TEST_HOST_UNINSTALLER_FAIL:', err?.message || err);
    process.exit(1);
});
