/**
 * Instance 路由单元测试
 */
import { describe, it, expect, vi, afterEach } from 'vitest';
import { EventEmitter } from 'node:events';
const spawnMock = vi.hoisted(() => vi.fn());
vi.mock('axios', () => ({
    default: {
        get: vi.fn(),
        post: vi.fn(),
    },
}));
vi.mock('../services/auto-register.js', () => ({
    getConfiguredConnectionKeys: vi.fn(() => []),
    requestRuntimeAccessTokenRefreshForServer: vi.fn(),
}));
vi.mock('../services/runtime-binding.js', () => ({
    getRuntimeAccessToken: vi.fn(() => 'stale-runtime-token-123456'),
    loadRuntimeBinding: vi.fn(() => ({ status: 'paired', userId: 'user-1', schedulerBaseUrl: 'http://server.local:7380' })),
    normalizeSchedulerBaseUrl: vi.fn((value) => (value ? String(value).replace(/\/+$/, '') : undefined)),
}));
vi.mock('../services/lifecycle-state.js', () => ({
    createBridgeLifecycleOperation: vi.fn((input) => ({
        operationId: 'operation-1',
        requestedAt: '2026-01-01T00:00:00.000Z',
        updatedAt: '2026-01-01T00:00:00.000Z',
        ...input,
    })),
    deliverPendingBridgeLifecycleNotifications: vi.fn(),
    upsertBridgeLifecycleOperation: vi.fn(),
}));
vi.mock('../config.js', async (importOriginal) => ({
    ...(await importOriginal()),
    getRuntimeHomeDir: vi.fn(() => process.cwd()),
}));
vi.mock('node:child_process', async (importOriginal) => ({
    ...(await importOriginal()),
    spawn: spawnMock,
}));
afterEach(() => {
    vi.useRealTimers();
    vi.clearAllMocks();
    delete process.env.AWS_RUNTIME_SCHEDULER_BASE_URL;
    delete process.env.AWS_TEST_HOME;
});
function mockSpawnResult(exitCode, stdoutText = '', stderrText = '') {
    spawnMock.mockImplementationOnce(() => {
        const child = new EventEmitter();
        child.stdout = new EventEmitter();
        child.stderr = new EventEmitter();
        process.nextTick(() => {
            if (stdoutText) {
                child.stdout.emit('data', Buffer.from(stdoutText));
            }
            if (stderrText) {
                child.stderr.emit('data', Buffer.from(stderrText));
            }
            child.emit('close', exitCode);
        });
        return child;
    });
}
function createMockResponse() {
    const json = vi.fn();
    const status = vi.fn(() => ({ json }));
    return { json, status };
}
describe('instance route validation', () => {
    it('builds correct ping response when healthy', () => {
        const buildPingResponse = (schedulerStatus, schedulerBaseUrl) => ({
            ok: true,
            runtimeBridge: 'healthy',
            schedulerReachable: schedulerStatus >= 200 && schedulerStatus < 300,
            schedulerBaseUrl,
        });
        const healthyResponse = buildPingResponse(200, 'http://localhost:8080');
        expect(healthyResponse.ok).toBe(true);
        expect(healthyResponse.schedulerReachable).toBe(true);
        const unhealthyResponse = buildPingResponse(502, 'http://localhost:8080');
        expect(unhealthyResponse.schedulerReachable).toBe(false);
    });
    it('requires agentId and workspacePath', () => {
        const validateInitRequest = (body) => {
            if (!body.agentId || !body.workspacePath)
                return { valid: false, error: 'agentId and workspacePath are required' };
            return { valid: true };
        };
        expect(validateInitRequest({}).valid).toBe(false);
        expect(validateInitRequest({ agentId: 'agent-1', workspacePath: '/path' }).valid).toBe(true);
    });
    it('builds correct init options', () => {
        const buildInitOptions = (body) => ({
            skillEnabled: body.skillEnabled,
            mcpEnabled: body.mcpEnabled,
            ccSwitchEnabledTools: body.ccSwitchEnabledTools,
        });
        const options = buildInitOptions({
            skillEnabled: true,
            mcpEnabled: false,
            ccSwitchEnabledTools: ['claude', 'opencode'],
        });
        expect(options.skillEnabled).toBe(true);
        expect(options.mcpEnabled).toBe(false);
        expect(options.ccSwitchEnabledTools).toEqual(['claude', 'opencode']);
    });
    it('requires agentId for state query', () => {
        const validateStateRequest = (body) => {
            if (!body.agentId)
                return { valid: false, error: 'agentId is required' };
            return { valid: true };
        };
        expect(validateStateRequest({}).valid).toBe(false);
        expect(validateStateRequest({ agentId: 'agent-1' }).valid).toBe(true);
    });
    it('always detects panel tool statuses even when legacy enabled tools omit codex', async () => {
        const { buildToolStatusDetectionTargets } = await import('./instance.js');
        expect(buildToolStatusDetectionTargets(['claude', 'opencode'])).toEqual([
            'claude',
            'opencode',
            'acode',
            'codex',
        ]);
    }, 15_000);
    it('accepts codex for cc-switch tool installation validation', async () => {
        const { SUPPORTED_INSTALLABLE_TOOLS } = await import('../services/tool-installer.js');
        const normalizeInstallableTools = (tools) => {
            const supported = new Set(SUPPORTED_INSTALLABLE_TOOLS);
            const requestedTools = Array.isArray(tools)
                ? tools.map((tool) => String(tool || '').trim().toLowerCase()).filter(Boolean)
                : [];
            return requestedTools.filter((tool) => supported.has(tool));
        };
        expect(normalizeInstallableTools(['Codex'])).toEqual(['codex']);
        expect(SUPPORTED_INSTALLABLE_TOOLS).toContain('codex');
    });
    it('accepts panel tools for cc-switch tool uninstallation validation', async () => {
        const { SUPPORTED_UNINSTALLABLE_TOOLS } = await import('../services/tool-installer.js');
        const normalizeUninstallableTools = (tools) => {
            const supported = new Set(SUPPORTED_UNINSTALLABLE_TOOLS);
            const requestedTools = Array.isArray(tools)
                ? tools.map((tool) => String(tool || '').trim().toLowerCase()).filter(Boolean)
                : [];
            return requestedTools.filter((tool) => supported.has(tool));
        };
        expect(normalizeUninstallableTools(['Claude', 'OpenCode', 'Codex'])).toEqual([
            'claude',
            'opencode',
            'codex',
        ]);
        expect(normalizeUninstallableTools(['unknown-tool'])).toEqual([]);
    });
    it('reports uninstall failure when a requested tool remains installed', async () => {
        const { buildUninstallFailureMessage } = await import('./instance.js');
        expect(buildUninstallFailureMessage(['opencode'], {
            opencode: {
                tool: 'opencode',
                installed: true,
                executable: '/home/user/.opencode/bin/opencode',
                version: '1.14.39',
                installing: false,
                error: 'uninstall completed but command is still available',
            },
        })).toContain('opencode: uninstall completed but command is still available');
    });
    it('summarizes tool status for route diagnostics', async () => {
        const { summarizeToolStatus } = await import('./instance.js');
        expect(summarizeToolStatus({
            opencode: {
                tool: 'opencode',
                installed: false,
                executable: null,
                version: null,
                installing: false,
                error: 'SDK package @opencode-ai/sdk is not installed in aws-runtime-bridge',
            },
            claude: {
                tool: 'claude',
                installed: true,
                executable: '@anthropic-ai/claude-agent-sdk',
                version: '0.2.87',
                installing: false,
                error: null,
            },
        })).toEqual({
            opencode: {
                installed: false,
                executable: null,
                version: null,
                error: 'SDK package @opencode-ai/sdk is not installed in aws-runtime-bridge',
            },
            claude: {
                installed: true,
                executable: '@anthropic-ai/claude-agent-sdk',
                version: '0.2.87',
                error: null,
            },
        });
    });
    it('does not report uninstall failure when requested tools are no longer installed', async () => {
        const { buildUninstallFailureMessage } = await import('./instance.js');
        expect(buildUninstallFailureMessage(['claude'], {
            claude: {
                tool: 'claude',
                installed: false,
                executable: null,
                version: null,
                installing: false,
                error: 'command not installed',
            },
        })).toBe('');
    });
    it('allows connection check when no connection key is configured', async () => {
        const { getConfiguredConnectionKeys } = await import('../services/auto-register.js');
        vi.mocked(getConfiguredConnectionKeys).mockReturnValue([]);
        const { buildConnectionCheckResponse } = await import('./instance.js');
        expect(buildConnectionCheckResponse('')).toEqual({
            status: 200,
            body: {
                ok: true,
                runtimeBridge: 'healthy',
                connectionKeyMatched: true,
                connectionKeyRequired: false,
            },
        });
    });
    it('requires matching connection key digest when a connection key is configured', async () => {
        const { createHash } = await import('node:crypto');
        const { getConfiguredConnectionKeys } = await import('../services/auto-register.js');
        vi.mocked(getConfiguredConnectionKeys).mockReturnValue(['secret-key']);
        const { buildConnectionCheckResponse } = await import('./instance.js');
        const digest = createHash('md5').update('secret-key', 'utf8').digest('hex');
        expect(buildConnectionCheckResponse(digest).status).toBe(200);
        expect(buildConnectionCheckResponse('00000000000000000000000000000000')).toEqual({
            status: 401,
            body: {
                ok: false,
                error: 'connection_key_mismatch',
                connectionKeyMatched: false,
                connectionKeyRequired: true,
            },
        });
    });
    it('explains localhost scheduler ping failures with environment variable guidance', async () => {
        const { buildSchedulerPingFailureResponse } = await import('./instance.js');
        const response = buildSchedulerPingFailureResponse(new Error('connect ECONNREFUSED 127.0.0.1:8080'), 'http://localhost:8080');
        expect(response).toEqual({
            ok: false,
            error: 'connect ECONNREFUSED 127.0.0.1:8080',
            failureStage: 'scheduler_ping',
            runtimeBridge: 'healthy',
            schedulerBaseUrl: 'http://localhost:8080',
            hint: expect.stringContaining('AWS_RUNTIME_SCHEDULER_BASE_URL'),
        });
        expect(response.hint).toContain('http://<server-host>:7380');
    });
    it('explains non-localhost scheduler ping failures as reachability or token issues', async () => {
        const { buildSchedulerPingFailureResponse } = await import('./instance.js');
        const response = buildSchedulerPingFailureResponse(new Error('Request failed with status code 401'), 'http://10.0.0.8:7380');
        expect(response.schedulerBaseUrl).toBe('http://10.0.0.8:7380');
        expect(response.failureStage).toBe('scheduler_ping');
        expect(response.runtimeBridge).toBe('healthy');
        expect(response.hint).toContain('运行时访问令牌仍有效');
    });
    it('refreshes runtime token and retries scheduler ping after 401', async () => {
        process.env.AWS_RUNTIME_SCHEDULER_BASE_URL = 'http://localhost:8080';
        const { default: axios } = await import('axios');
        const { requestRuntimeAccessTokenRefreshForServer } = await import('../services/auto-register.js');
        const { instanceRouter } = await import('./instance.js');
        vi.mocked(requestRuntimeAccessTokenRefreshForServer).mockResolvedValue({
            success: true,
            runtimeAccessToken: 'fresh-runtime-token-123456',
            updated: true,
        });
        vi.mocked(axios.get)
            .mockRejectedValueOnce({ response: { status: 401 }, message: 'Request failed with status code 401' })
            .mockResolvedValueOnce({ status: 200 });
        const handler = instanceRouter.stack.find((layer) => layer.route?.path === '/ping')?.route?.stack[1]?.handle;
        expect(handler).toBeTypeOf('function');
        const json = vi.fn();
        const status = vi.fn(() => ({ json }));
        await handler?.({ header: vi.fn(() => '') }, { json, status }, vi.fn());
        expect(requestRuntimeAccessTokenRefreshForServer).toHaveBeenCalledWith('http://localhost:8080');
        expect(axios.get).toHaveBeenNthCalledWith(1, 'http://localhost:8080/api/runtime/ping', {
            headers: { 'X-Runtime-Token': 'stale-runtime-token-123456' },
        });
        expect(axios.get).toHaveBeenNthCalledWith(2, 'http://localhost:8080/api/runtime/ping', {
            headers: { 'X-Runtime-Token': 'fresh-runtime-token-123456' },
        });
        expect(json).toHaveBeenCalledWith({
            ok: true,
            runtimeBridge: 'healthy',
            schedulerReachable: true,
            tokenRefreshed: true,
            schedulerBaseUrl: 'http://localhost:8080',
        });
        expect(status).not.toHaveBeenCalled();
    });
    it('keeps explicit local scheduler config before server ping request header', async () => {
        process.env.AWS_RUNTIME_SCHEDULER_BASE_URL = 'http://localhost:8080';
        const { default: axios } = await import('axios');
        const { getRuntimeAccessToken } = await import('../services/runtime-binding.js');
        const { instanceRouter } = await import('./instance.js');
        vi.mocked(getRuntimeAccessToken).mockReturnValue('stale-runtime-token-123456');
        vi.mocked(axios.get).mockReset();
        vi.mocked(axios.get).mockResolvedValueOnce({ status: 200 });
        const handler = instanceRouter.stack.find((layer) => layer.route?.path === '/ping')?.route?.stack[1]?.handle;
        expect(handler).toBeTypeOf('function');
        const json = vi.fn();
        const status = vi.fn(() => ({ json }));
        const header = vi.fn((name) => (name === 'X-Scheduler-Base-Url' ? 'http://server.local:7380/api' : ''));
        await handler?.({ header }, { json, status }, vi.fn());
        expect(getRuntimeAccessToken).toHaveBeenCalledWith(undefined, 'http://localhost:8080');
        expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/api/runtime/ping', {
            headers: { 'X-Runtime-Token': 'stale-runtime-token-123456' },
        });
        expect(json).toHaveBeenCalledWith({
            ok: true,
            runtimeBridge: 'healthy',
            schedulerReachable: true,
            tokenRefreshed: false,
            schedulerBaseUrl: 'http://localhost:8080',
        });
        expect(status).not.toHaveBeenCalled();
    });
    it('uses scheduler base URL supplied by server ping request when bridge has no local scheduler config', async () => {
        delete process.env.AWS_RUNTIME_SCHEDULER_BASE_URL;
        process.env.AWS_TEST_HOME = `${process.cwd()}/.vitest-empty-bridge-home`;
        const { default: axios } = await import('axios');
        const { getRuntimeAccessToken } = await import('../services/runtime-binding.js');
        const { instanceRouter } = await import('./instance.js');
        vi.mocked(getRuntimeAccessToken).mockReturnValue('stale-runtime-token-123456');
        vi.mocked(axios.get).mockReset();
        vi.mocked(axios.get).mockResolvedValueOnce({ status: 200 });
        const handler = instanceRouter.stack.find((layer) => layer.route?.path === '/ping')?.route?.stack[1]?.handle;
        expect(handler).toBeTypeOf('function');
        const json = vi.fn();
        const status = vi.fn(() => ({ json }));
        const header = vi.fn((name) => (name === 'X-Scheduler-Base-Url' ? 'http://server.local:7380/api' : ''));
        await handler?.({ header }, { json, status }, vi.fn());
        expect(getRuntimeAccessToken).toHaveBeenCalledWith(undefined, 'http://server.local:7380');
        expect(axios.get).toHaveBeenCalledWith('http://server.local:7380/api/runtime/ping', {
            headers: { 'X-Runtime-Token': 'stale-runtime-token-123456' },
        });
        expect(json).toHaveBeenCalledWith({
            ok: true,
            runtimeBridge: 'healthy',
            schedulerReachable: true,
            tokenRefreshed: false,
            schedulerBaseUrl: 'http://server.local:7380',
        });
        expect(status).not.toHaveBeenCalled();
    });
    it('keeps single auto-register scheduler config before server ping request header', async () => {
        delete process.env.AWS_RUNTIME_SCHEDULER_BASE_URL;
        const fs = await import('node:fs');
        const path = await import('node:path');
        const testHome = path.join(process.cwd(), '.vitest-single-target-bridge-home');
        const configDir = path.join(testHome, '.aws-bridge');
        fs.rmSync(testHome, { recursive: true, force: true });
        fs.mkdirSync(configDir, { recursive: true });
        fs.writeFileSync(path.join(configDir, 'config.json'), JSON.stringify({
            autoRegisterTargets: [
                { serverUrl: 'http://bridge-config.local:7380/api' },
            ],
        }), 'utf-8');
        process.env.AWS_TEST_HOME = testHome;
        const { default: axios } = await import('axios');
        const { getRuntimeAccessToken } = await import('../services/runtime-binding.js');
        const { instanceRouter } = await import('./instance.js');
        vi.mocked(getRuntimeAccessToken).mockReturnValue('stale-runtime-token-123456');
        vi.mocked(axios.get).mockReset();
        vi.mocked(axios.get).mockResolvedValueOnce({ status: 200 });
        const handler = instanceRouter.stack.find((layer) => layer.route?.path === '/ping')?.route?.stack[1]?.handle;
        expect(handler).toBeTypeOf('function');
        const json = vi.fn();
        const status = vi.fn(() => ({ json }));
        const header = vi.fn((name) => (name === 'X-Scheduler-Base-Url' ? 'http://server.local:7380/api' : ''));
        await handler?.({ header }, { json, status }, vi.fn());
        expect(getRuntimeAccessToken).toHaveBeenCalledWith(undefined, 'http://bridge-config.local:7380');
        expect(axios.get).toHaveBeenCalledWith('http://bridge-config.local:7380/api/runtime/ping', {
            headers: { 'X-Runtime-Token': 'stale-runtime-token-123456' },
        });
        expect(json).toHaveBeenCalledWith({
            ok: true,
            runtimeBridge: 'healthy',
            schedulerReachable: true,
            tokenRefreshed: false,
            schedulerBaseUrl: 'http://bridge-config.local:7380',
        });
        expect(status).not.toHaveBeenCalled();
    });
    it('explains ambiguous auto-register scheduler targets without picking the first one', async () => {
        const { buildAmbiguousSchedulerBaseUrlResponse } = await import('./instance.js');
        const response = buildAmbiguousSchedulerBaseUrlResponse([
            'http://scheduler-a.local:7380',
            'http://scheduler-b.local:7380',
        ]);
        expect(response).toEqual({
            ok: false,
            error: 'ambiguous_scheduler_base_url',
            failureStage: 'scheduler_ping',
            runtimeBridge: 'healthy',
            schedulerBaseUrl: '',
            autoRegisterTargetUrls: [
                'http://scheduler-a.local:7380',
                'http://scheduler-b.local:7380',
            ],
            hint: expect.stringContaining('AWS_RUNTIME_SCHEDULER_BASE_URL'),
        });
    });
    it('starts bridge package update in background and returns operation id', async () => {
        const { instanceRouter } = await import('./instance.js');
        mockSpawnResult(0, 'updated');
        const handler = instanceRouter.stack.find((layer) => layer.route?.path === '/update-bridge')?.route?.stack[1]?.handle;
        expect(handler).toBeTypeOf('function');
        const { json, status } = createMockResponse();
        await handler?.({ body: { requestedByUserId: 'user-1' }, get: () => 'bridge.local:18081', protocol: 'http' }, { json, status }, vi.fn());
        expect(status).not.toHaveBeenCalled();
        expect(json).toHaveBeenCalledWith(expect.objectContaining({
            ok: true,
            message: 'Bridge 更新已在后台开始，完成后会通过消息中心通知结果。',
        }));
        await new Promise((resolve) => setImmediate(resolve));
        expect(spawnMock).toHaveBeenCalledWith(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['install', '-g', 'aws-runtime-bridge@latest'], { shell: false, windowsHide: true });
    });
    it('rejects background bridge update without requester user id', async () => {
        const { instanceRouter } = await import('./instance.js');
        const handler = instanceRouter.stack.find((layer) => layer.route?.path === '/update-bridge')?.route?.stack[1]?.handle;
        expect(handler).toBeTypeOf('function');
        const { json, status } = createMockResponse();
        await handler?.({}, { json, status }, vi.fn());
        expect(status).toHaveBeenCalledWith(400);
        expect(json).toHaveBeenCalledWith({ error: 'requestedByUserId is required' });
    });
    it('responds before triggering graceful bridge restart', async () => {
        vi.useFakeTimers();
        const { instanceRouter, setGracefulShutdownFn } = await import('./instance.js');
        const gracefulShutdown = vi.fn().mockResolvedValue(undefined);
        setGracefulShutdownFn(gracefulShutdown);
        const handler = instanceRouter.stack.find((layer) => layer.route?.path === '/restart-bridge')?.route?.stack[1]?.handle;
        expect(handler).toBeTypeOf('function');
        const { json, status } = createMockResponse();
        await handler?.({ body: { preserveSessions: true, requestedByUserId: 'user-1' }, get: () => 'bridge.local:18081', protocol: 'http' }, { json, status }, vi.fn());
        expect(status).not.toHaveBeenCalled();
        expect(json).toHaveBeenCalledWith(expect.objectContaining({
            ok: true,
            preserveSessions: true,
            message: 'Bridge 正在优雅退出；下次启动后会通过消息中心通知结果。若未由守护进程管理，请手动重新启动。',
        }));
        expect(gracefulShutdown).not.toHaveBeenCalled();
        await vi.advanceTimersByTimeAsync(250);
        expect(gracefulShutdown).toHaveBeenCalledWith('PANEL_RESTART', true);
    });
});
