import { describe, it, expect } from 'vitest';
import { SUPPORTED_INSTALLABLE_TOOLS, SUPPORTED_UNINSTALLABLE_TOOLS, buildSdkCliInstallStatus, detectToolInstallStatus, detectToolStatuses, getToolCommandSpecs, getToolUninstallCommands, isVoltaShimPath } from './tool-installer.js';
describe('tool installer service', () => {
    it('returns structured status for supported tools', async () => {
        const statuses = await detectToolStatuses(['claude', 'opencode', 'codex']);
        expect(typeof statuses.claude.installed).toBe('boolean');
        expect(typeof statuses.opencode.installed).toBe('boolean');
        expect(typeof statuses.codex.installed).toBe('boolean');
        expect(statuses.claude.tool).toBe('claude');
        expect(statuses.opencode.tool).toBe('opencode');
        expect(statuses.codex.tool).toBe('codex');
    });
    it('deduplicates repeated tool names', async () => {
        const statuses = await detectToolStatuses(['claude', 'claude', 'opencode']);
        expect(Object.keys(statuses).sort()).toEqual(['claude', 'opencode']);
    });
    it('returns unsupported status for unknown tool', async () => {
        const status = await detectToolInstallStatus('unknown-tool');
        expect(status.installed).toBe(false);
        expect(status.error).toBe('unsupported tool');
    });
    it('uses a precise message when a tool command is missing', async () => {
        const status = await detectToolInstallStatus('claude');
        // error can be null (when installed) or string (when not installed)
        expect(status.error === null || typeof status.error === 'string').toBe(true);
        if (!status.installed && status.error) {
            expect(status.error).not.toBe('not installed or not executable');
        }
    });
    it('treats discovered command path as installed even when version is unavailable', async () => {
        const status = await detectToolInstallStatus('opencode');
        if (status.executable) {
            expect(status.installed).toBe(true);
        }
    });
    it('normalizes tool names to lowercase', async () => {
        const statuses = await detectToolStatuses(['CLAUDE', 'OpenCode']);
        expect(statuses.claude).toBeDefined();
        expect(statuses.opencode).toBeDefined();
    });
    it('handles empty tool array', async () => {
        const statuses = await detectToolStatuses([]);
        expect(statuses).toEqual({});
    });
    it('handles null input for detectToolStatuses', async () => {
        const statuses = await detectToolStatuses(null);
        expect(statuses).toEqual({});
    });
    it('handles undefined input for detectToolStatuses', async () => {
        const statuses = await detectToolStatuses(undefined);
        expect(statuses).toEqual({});
    });
    it('filters out empty and whitespace tool names', async () => {
        const statuses = await detectToolStatuses(['claude', '', '  ', 'opencode']);
        expect(Object.keys(statuses).sort()).toEqual(['claude', 'opencode']);
    });
    it('returns correct structure for tool status', async () => {
        const status = await detectToolInstallStatus('claude');
        expect(typeof status.tool).toBe('string');
        expect(typeof status.installed).toBe('boolean');
        expect(status.executable === null || typeof status.executable === 'string').toBe(true);
        expect(status.version === null || typeof status.version === 'string').toBe(true);
        expect(typeof status.installing).toBe('boolean');
        expect(status.error === null || typeof status.error === 'string').toBe(true);
    });
    it('supports claudecode as alias for claude', async () => {
        const status = await detectToolInstallStatus('claudecode');
        expect(status.tool).toBe('claudecode');
        // claudecode shares same detection as claude
        expect(typeof status.installed).toBe('boolean');
    });
    it('supports codex as an installable tool', async () => {
        expect(SUPPORTED_INSTALLABLE_TOOLS).toContain('codex');
        const status = await detectToolInstallStatus('codex');
        expect(status.tool).toBe('codex');
        expect(typeof status.installed).toBe('boolean');
        expect(status.error).not.toBe('unsupported tool');
    });
    it('detects OpenCode by SDK package instead of CLI availability', async () => {
        const status = await detectToolInstallStatus('opencode');
        expect(status.tool).toBe('opencode');
        expect(status.executable === null || status.executable === '@opencode-ai/sdk').toBe(true);
        if (!status.installed) {
            expect(status.error).toContain('@opencode-ai/sdk');
        }
    });
    it('detects Codex by SDK package instead of CLI availability', async () => {
        const status = await detectToolInstallStatus('codex');
        expect(status.tool).toBe('codex');
        expect(status.executable === null || status.executable === '@openai/codex-sdk').toBe(true);
        if (!status.installed) {
            expect(status.error).toContain('@openai/codex-sdk');
        }
    });
    it('supports all panel tools as uninstallable tools', () => {
        expect(SUPPORTED_UNINSTALLABLE_TOOLS).toEqual(expect.arrayContaining(['claude', 'opencode', 'codex']));
    });
    it('covers npm and native installer uninstall paths for OpenCode', () => {
        const commands = getToolUninstallCommands('opencode');
        expect(commands.some(command => command.includes('npm uninstall --prefix'))).toBe(true);
        expect(commands.some(command => command.includes('@opencode-ai/sdk'))).toBe(true);
        expect(commands.some(command => command.includes('opencode-ai'))).toBe(true);
        if (process.platform !== 'win32') {
            expect(commands).toContain('opencode uninstall --force');
            expect(commands).toContain('rm -f ~/.opencode/bin/opencode');
        }
    });
    it('covers bridge-local SDK uninstall path for Claude Code', () => {
        const commands = getToolUninstallCommands('claude');
        expect(commands.some(command => command.includes('npm uninstall --prefix'))).toBe(true);
        expect(commands.some(command => command.includes('@anthropic-ai/claude-agent-sdk'))).toBe(true);
    });
    it('installs Claude Code SDK and terminal CLI together', () => {
        const commands = getToolCommandSpecs('claude', 'install');
        expect(commands).toHaveLength(5);
        expect(commands[0].args).toEqual([
            'uninstall',
            '--prefix',
            expect.any(String),
            '@anthropic-ai/claude-agent-sdk',
            '@anthropic-ai/sdk'
        ]);
        expect(commands[1].args).toEqual([
            'install',
            '--prefix',
            expect.any(String),
            '@anthropic-ai/sdk@latest'
        ]);
        expect(commands[2].args).toEqual([
            'install',
            '--prefix',
            expect.any(String),
            '@anthropic-ai/claude-agent-sdk@latest'
        ]);
        expect(commands[3].args).toEqual([
            'install',
            '--prefix',
            expect.any(String),
            '@anthropic-ai/claude-agent-sdk@latest',
            '--legacy-peer-deps'
        ]);
        expect(commands[4].args).toEqual([
            'install',
            '-g',
            '@anthropic-ai/claude-code@latest'
        ]);
        expect(commands[3].display).toContain('--legacy-peer-deps');
        expect(commands[4].display).toBe('npm install -g @anthropic-ai/claude-code@latest');
    });
    it('installs Claude Code CLI for the claudecode alias too', () => {
        const commands = getToolCommandSpecs('claudecode', 'install');
        expect(commands.some(command => command.args.includes('@anthropic-ai/claude-agent-sdk@latest'))).toBe(true);
        expect(commands.some(command => command.args.includes('@anthropic-ai/sdk@latest'))).toBe(true);
        expect(commands.some(command => command.args.includes('--legacy-peer-deps'))).toBe(true);
        expect(commands.some(command => command.args.includes('@anthropic-ai/claude-code@latest'))).toBe(true);
    });
    it('marks Claude unavailable when SDK exists but terminal CLI is missing', () => {
        const status = buildSdkCliInstallStatus('claude', {
            executable: '@anthropic-ai/claude-agent-sdk',
            version: '0.2.87',
            error: null
        }, {
            executable: null,
            error: 'CLI package @anthropic-ai/claude-code is not installed or command is unavailable: command not installed'
        }, true);
        expect(status.installed).toBe(false);
        expect(status.executable).toBe('@anthropic-ai/claude-agent-sdk');
        expect(status.error).toContain('@anthropic-ai/claude-code');
    });
    it('covers bridge-local SDK uninstall path for Codex', () => {
        const commands = getToolUninstallCommands('codex');
        expect(commands.some(command => command.includes('npm uninstall --prefix'))).toBe(true);
        expect(commands.some(command => command.includes('@openai/codex-sdk'))).toBe(true);
    });
    it('executes npm-backed install and uninstall commands with prefix as an unquoted argument', () => {
        const expectedPackagesByTool = {
            claude: '@anthropic-ai/claude-agent-sdk',
            claudecode: '@anthropic-ai/claude-agent-sdk',
            opencode: '@opencode-ai/sdk',
            codex: '@openai/codex-sdk'
        };
        for (const [tool, packageName] of Object.entries(expectedPackagesByTool)) {
            for (const phase of ['install', 'uninstall']) {
                const commands = getToolCommandSpecs(tool, phase);
                const command = commands.find(spec => spec.args.some(arg => arg.startsWith(packageName))) || commands[0];
                const expectedCommandAction = command.args[0];
                expect(command.kind).toBe('execFile');
                expect(command.command).toMatch(/^npm(?:\.cmd)?$/);
                expect(command.args.slice(0, 3)).toEqual([expectedCommandAction, '--prefix', expect.any(String)]);
                expect(command.args[2]).not.toMatch(/^"|"$/);
                expect(command.args.some(arg => arg.startsWith(packageName))).toBe(true);
                expect(command.display).toContain(`npm ${expectedCommandAction} --prefix`);
            }
        }
    });
    it('recognizes Volta shim paths for package-manager-aware detection', () => {
        expect(isVoltaShimPath('C:\\Users\\tester\\AppData\\Local\\Volta\\bin\\codex.cmd')).toBe(true);
        expect(isVoltaShimPath('/home/tester/.volta/bin/codex')).toBe(true);
        expect(isVoltaShimPath('/Users/tester/.volta/bin/claude')).toBe(true);
        expect(isVoltaShimPath('/usr/local/bin/codex')).toBe(false);
    });
    it('detectToolStatuses returns status for all requested tools', async () => {
        const tools = ['claude', 'opencode', 'unknown-tool'];
        const statuses = await detectToolStatuses(tools);
        expect(statuses.claude).toBeDefined();
        expect(statuses.opencode).toBeDefined();
        // unknown-tool should also be in results with unsupported status
        expect(statuses['unknown-tool']).toBeDefined();
        expect(statuses['unknown-tool'].error).toBe('unsupported tool');
    });
    it('handles mixed case tool names', async () => {
        const statuses = await detectToolStatuses(['Claude', 'OPENCODE', 'ClaudeCode']);
        expect(statuses.claude).toBeDefined();
        expect(statuses.opencode).toBeDefined();
        expect(statuses.claudecode).toBeDefined();
    });
    it('sets installing flag to false by default', async () => {
        const status = await detectToolInstallStatus('claude');
        expect(status.installing).toBe(false);
    });
    it('returns consistent tool name in lowercase', async () => {
        const status = await detectToolInstallStatus('CLAUDE');
        expect(status.tool).toBe('claude');
    });
});
