/**
 * opencode-v2.test.mjs —— CLI 侧 OpenCode V1/V2 适配单测
 *
 * 覆盖:
 *   - parseBinVersion / getCliBinVersion 版本号解析
 *   - probeServeApi 版本感知探针(V1 /session JSON、V2 /api/info 401/200、
 *     Web 页面占用、无服务)
 *   - resolveServePassword(env 优先 → V2 服务注册文件 url 匹配,localhost 归一化)
 *   - checkPermissionConfig / fixPermissionConfig 版本感知权限检查与补写
 *     (opencode 2.x → permissions allow-all 数组;1.x / deveco → permission 字符串)
 *   - CliServeManager 连接信息传播(host/port/密码写入 process.env)
 *     与 ensureRunning 复用 V2 服务(env 密码认证)/ 无密码失败指引
 *   - precheckEnv 集成(serve 检查识别 V2 需密码并阻断)
 *
 * 全部基于 Mock HTTP 服务与临时目录(XDG_CONFIG_HOME / XDG_STATE_HOME 隔离),
 * 不依赖真实 opencode/deveco 安装,不拉起子进程。
 */

import assert from 'node:assert/strict';
import { createServer } from 'node:http';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const LIB = path.join(__dirname, '..', 'lib');

const {
    probeServeApi,
    resolveServePassword,
    CliServeManager,
} = await import(path.join(LIB, 'opencode.js'));

const {
    parseBinVersion,
    checkPermissionConfig,
    fixPermissionConfig,
    precheckEnv,
} = await import(path.join(LIB, 'doctor.js'));

// ─── 工具 ────────────────────────────────────────────────────────────────────

/** 临时覆盖 process.env,结束后恢复(值为 undefined 表示删除该变量) */
async function withEnv(overrides, fn) {
    const saved = {};
    for (const [k, v] of Object.entries(overrides)) {
        saved[k] = process.env[k];
        if (v === undefined) delete process.env[k];
        else process.env[k] = String(v);
    }
    try {
        return await fn();
    } finally {
        for (const [k, v] of Object.entries(saved)) {
            if (v === undefined) delete process.env[k];
            else process.env[k] = v;
        }
    }
}

/** 创建临时目录 */
function tmpDir(prefix) {
    return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}

/** 在随机端口启动 Mock HTTP 服务,返回 server(server.baseUrl 可用) */
function listenMock(handler) {
    return new Promise(resolve => {
        const server = createServer(handler);
        server.listen(0, '127.0.0.1', () => {
            server.baseUrl = `http://127.0.0.1:${server.address().port}`;
            resolve(server);
        });
    });
}

function closeMock(server) {
    return new Promise(resolve => server.close(resolve));
}

/** V1 系 Mock(/api/info 404 → /session 返回 JSON 数组) */
function v1Handler(req, res) {
    if (req.url === '/api/info') {
        res.writeHead(404, { 'content-type': 'application/json' });
        return res.end('{"error":"not found"}');
    }
    if (req.url === '/session') {
        res.writeHead(200, { 'content-type': 'application/json' });
        return res.end(JSON.stringify([{ id: 'ses_1', title: 'demo' }]));
    }
    res.writeHead(404); res.end();
}

/** V2 Mock(/api/info 需 Basic 认证,正确密码返回版本 JSON) */
function makeV2Handler(password) {
    const expected = 'Basic ' + Buffer.from(`opencode:${password}`).toString('base64');
    return (req, res) => {
        if (req.url === '/api/info') {
            if (req.headers.authorization === expected) {
                res.writeHead(200, { 'content-type': 'application/json' });
                return res.end(JSON.stringify({ version: '2.0.26', name: 'opencode' }));
            }
            res.writeHead(401, { 'content-type': 'application/json' });
            return res.end(JSON.stringify({ _tag: 'UnauthorizedError', message: 'Authentication required' }));
        }
        res.writeHead(404); res.end();
    };
}

/** Web 页面 Mock(未知路径一律 200 + HTML——V2/deveco 的 Web 兜底行为) */
function htmlHandler(req, res) {
    res.writeHead(200, { 'content-type': 'text/html' });
    res.end('<!doctype html><html><title>Web</title></html>');
}

// ─── 版本号解析 ──────────────────────────────────────────────────────────────

test('parseBinVersion 兼容多种 --version 输出格式', () => {
    assert.equal(parseBinVersion('opencode v2.0.26'), '2.0.26');
    assert.equal(parseBinVersion('opencode 1.0.200'), '1.0.200');
    assert.equal(parseBinVersion('0.1.12'), '0.1.12');
    // 预发布后缀保留(主版本判定与展示均兼容)
    assert.equal(parseBinVersion('opencode v2.1.0-beta.3'), '2.1.0-beta.3');
    assert.equal(parseBinVersion('no version here'), null);
    assert.equal(parseBinVersion(''), null);
    assert.equal(parseBinVersion(null), null);
});

// ─── probeServeApi 版本感知探针 ──────────────────────────────────────────────

test('probeServeApi: V1 服务(/session 返回 JSON)→ running/v1', async () => {
    const server = await listenMock(v1Handler);
    try {
        const r = await probeServeApi(server.baseUrl, { timeoutMs: 1000 });
        assert.equal(r.state, 'running');
        assert.equal(r.apiVersion, 'v1');
        assert.equal(r.authRequired, false);
    } finally {
        await closeMock(server);
    }
});

test('probeServeApi: V2 服务无密码 → running/v2 且 authRequired', async () => {
    const server = await listenMock(makeV2Handler('pw123'));
    try {
        const r = await probeServeApi(server.baseUrl, { timeoutMs: 1000 });
        assert.equal(r.state, 'running');
        assert.equal(r.apiVersion, 'v2');
        assert.equal(r.authRequired, true);
        assert.match(r.detail, /需服务密码/);
    } finally {
        await closeMock(server);
    }
});

test('probeServeApi: V2 服务带正确密码 → running/v2 取到版本号', async () => {
    const server = await listenMock(makeV2Handler('pw123'));
    try {
        const r = await probeServeApi(server.baseUrl, { password: 'pw123', timeoutMs: 1000 });
        assert.equal(r.state, 'running');
        assert.equal(r.apiVersion, 'v2');
        assert.equal(r.authRequired, false);
        assert.equal(r.version, '2.0.26');
    } finally {
        await closeMock(server);
    }
});

test('probeServeApi: V2 服务密码错误 → authRequired(密码无效)', async () => {
    const server = await listenMock(makeV2Handler('pw123'));
    try {
        const r = await probeServeApi(server.baseUrl, { password: 'wrong', timeoutMs: 1000 });
        assert.equal(r.state, 'running');
        assert.equal(r.apiVersion, 'v2');
        assert.equal(r.authRequired, true);
        assert.match(r.detail, /服务密码无效/);
    } finally {
        await closeMock(server);
    }
});

test('probeServeApi: 端口被 Web 页面占用 → error', async () => {
    const server = await listenMock(htmlHandler);
    try {
        const r = await probeServeApi(server.baseUrl, { timeoutMs: 1000 });
        assert.equal(r.state, 'error');
        assert.equal(r.apiVersion, null);
    } finally {
        await closeMock(server);
    }
});

test('probeServeApi: 无服务 → down', async () => {
    // 先启动再关闭,拿到一个确定空闲的端口
    const server = await listenMock(() => {});
    const port = server.address().port;
    await closeMock(server);
    const r = await probeServeApi(`http://127.0.0.1:${port}`, { timeoutMs: 500 });
    assert.equal(r.state, 'down');
    assert.equal(r.apiVersion, null);
});

// ─── resolveServePassword ────────────────────────────────────────────────────

test('resolveServePassword: OPENCODE_PASSWORD 环境变量优先', async () => {
    await withEnv({ OPENCODE_PASSWORD: 'from-env' }, async () => {
        assert.equal(resolveServePassword('http://127.0.0.1:4096'), 'from-env');
    });
});

test('resolveServePassword: 注册文件 url 匹配时取其密码(localhost 归一化)', async () => {
    const stateRoot = tmpDir('oc-state-');
    fs.mkdirSync(path.join(stateRoot, 'opencode'), { recursive: true });
    fs.writeFileSync(path.join(stateRoot, 'opencode', 'service.json'), JSON.stringify({
        version: '2.0.26',
        url: 'http://127.0.0.1:49374',
        pid: 123,
        password: 'reg-pw',
    }));
    await withEnv({ OPENCODE_PASSWORD: undefined, XDG_STATE_HOME: stateRoot }, async () => {
        // localhost ↔ 127.0.0.1 归一化后匹配
        assert.equal(resolveServePassword('http://localhost:49374'), 'reg-pw');
        assert.equal(resolveServePassword('http://127.0.0.1:49374'), 'reg-pw');
        // 端口不同 → 不匹配
        assert.equal(resolveServePassword('http://127.0.0.1:4096'), null);
    });
});

test('resolveServePassword: 无 env 且无注册文件 → null', async () => {
    const stateRoot = tmpDir('oc-state-empty-');
    await withEnv({ OPENCODE_PASSWORD: undefined, XDG_STATE_HOME: stateRoot }, async () => {
        assert.equal(resolveServePassword('http://127.0.0.1:4096'), null);
    });
});

// ─── 权限检查 / 自动补写(版本感知) ────────────────────────────────────────

/** 在临时 XDG_CONFIG_HOME 下写入 AI CLI 配置 */
function writeCliConfig(configRoot, name, file, data) {
    const dir = path.join(configRoot, name);
    fs.mkdirSync(dir, { recursive: true });
    fs.writeFileSync(path.join(dir, file), JSON.stringify(data, null, 2));
}

test('权限检查: opencode 2.x 语义——缺 permissions 可补写,补写 allow-all 后通过', async () => {
    const configRoot = tmpDir('oc-cfg-');
    await withEnv({ XDG_CONFIG_HOME: configRoot }, async () => {
        // 初始:配置不存在 → 未通过 + fixable
        let r = checkPermissionConfig('opencode', { opencodeIsV2: true });
        assert.equal(r.passed, false);
        assert.equal(r.fixable, true);
        assert.match(r.fix, /permissions/);

        // 自动补写
        const fix = fixPermissionConfig({ opencodeIsV2: true });
        assert.equal(fix.ok, true);

        // 复查:通过,且写入了 allow-all 规则
        r = checkPermissionConfig('opencode', { opencodeIsV2: true });
        assert.equal(r.passed, true);
        const written = JSON.parse(fs.readFileSync(
            path.join(configRoot, 'opencode', 'opencode.json'), 'utf8'));
        assert.deepEqual(written.permissions, [
            { action: '*', resource: '*', effect: 'allow' },
        ]);
    });
});

test('权限检查: opencode 2.x 已有自定义规则(无 allow-all)→ 不覆盖仅提示', async () => {
    const configRoot = tmpDir('oc-cfg-');
    writeCliConfig(configRoot, 'opencode', 'opencode.json', {
        permissions: [{ action: 'shell', resource: 'git status *', effect: 'allow' }],
    });
    await withEnv({ XDG_CONFIG_HOME: configRoot }, async () => {
        const r = checkPermissionConfig('opencode', { opencodeIsV2: true });
        assert.equal(r.passed, false);
        assert.match(r.detail, /无 allow-all/);
        // fixable=true 仅因 deveco 配置文件缺失(doctor 可补建该文件);
        // 关键约束:opencode 的自定义规则不会被自动补写覆盖
        assert.equal(r.fixable, true);

        const fix = fixPermissionConfig({ opencodeIsV2: true });
        assert.equal(fix.ok, true);
        const data = JSON.parse(fs.readFileSync(
            path.join(configRoot, 'opencode', 'opencode.json'), 'utf8'));
        assert.equal(data.permissions.length, 1);
        assert.equal(data.permissions[0].action, 'shell');
        // deveco 缺失文件被补建(V1 语义),不影响 opencode
        assert.ok(fs.existsSync(path.join(configRoot, 'deveco', 'deveco.jsonc')));
    });
});

test('权限检查: opencode 1.x 语义——permission 字符串(回归)', async () => {
    const configRoot = tmpDir('oc-cfg-');
    writeCliConfig(configRoot, 'opencode', 'opencode.json', { permission: 'allow' });
    await withEnv({ XDG_CONFIG_HOME: configRoot }, async () => {
        // 显式按 V1 语义判定 → 通过
        let r = checkPermissionConfig('opencode', { opencodeIsV2: false });
        assert.equal(r.passed, true);

        // 同一配置按 V2 语义判定(2.x 忽略 V1 字段)→ 未通过且可补写
        r = checkPermissionConfig('opencode', { opencodeIsV2: true });
        assert.equal(r.passed, false);
        assert.equal(r.fixable, true);

        // 补写 permissions 后两种语义均通过(V1 字段保留不动)
        fixPermissionConfig({ opencodeIsV2: true });
        const data = JSON.parse(fs.readFileSync(
            path.join(configRoot, 'opencode', 'opencode.json'), 'utf8'));
        assert.equal(data.permission, 'allow');
        assert.deepEqual(data.permissions, [{ action: '*', resource: '*', effect: 'allow' }]);
    });
});

test('权限检查: deveco 配置恒为 V1 语义(opencode 2.x 不影响 deveco 文件)', async () => {
    const configRoot = tmpDir('oc-cfg-');
    writeCliConfig(configRoot, 'deveco', 'deveco.jsonc', { permission: 'allow' });
    await withEnv({ XDG_CONFIG_HOME: configRoot }, async () => {
        const r = checkPermissionConfig('deveco-code', { opencodeIsV2: true });
        assert.equal(r.passed, true);
    });
});

// ─── CliServeManager ─────────────────────────────────────────────────────────

test('CliServeManager: 连接信息传播(拉起场景密码覆盖 / 复用场景补缺)', async () => {
    await withEnv({
        OPENCODE_HOST: undefined, OPENCODE_PORT: undefined,
        OPENCODE_PASSWORD: undefined, DEVECO_HOST: undefined, DEVECO_PORT: undefined,
    }, async () => {
        const mgr = new CliServeManager({ projectRoot: tmpDir('oc-proj-'), cliType: 'opencode' });

        // 复用场景(spawnedByUs=false):用户已显式配置密码 → 不覆盖
        process.env.OPENCODE_PASSWORD = 'user-pw';
        mgr.password = 'captured-pw';
        mgr._propagateConnectionEnv();
        assert.equal(process.env.OPENCODE_HOST, 'localhost');
        assert.equal(process.env.OPENCODE_PORT, '4096');
        assert.equal(process.env.OPENCODE_PASSWORD, 'user-pw');

        // 拉起场景(spawnedByUs=true):捕获的密码为准
        mgr.spawnedByUs = true;
        mgr._propagateConnectionEnv();
        assert.equal(process.env.OPENCODE_PASSWORD, 'captured-pw');
    });
});

test('CliServeManager: ensureRunning 复用运行中的 V2 服务(env 密码认证 + env 传播)', async () => {
    const server = await listenMock(makeV2Handler('pw123'));
    const stateRoot = tmpDir('oc-state-'); // 空注册目录,隔离本机真实注册文件
    try {
        await withEnv({
            OPENCODE_HOST: '127.0.0.1',
            OPENCODE_PORT: String(server.address().port),
            OPENCODE_PASSWORD: 'pw123',
            XDG_STATE_HOME: stateRoot,
        }, async () => {
            const mgr = new CliServeManager({ projectRoot: tmpDir('oc-proj-'), cliType: 'opencode' });
            const r = await mgr.ensureRunning();
            assert.equal(r.ok, true);
            assert.equal(r.spawned, false);
            assert.equal(mgr.apiVersion, 'v2');
            assert.equal(mgr.password, 'pw123');
            assert.match(r.message, /V2 API v2\.0\.26/);
            // 连接信息已传播(确定性连接到该地址)
            assert.equal(process.env.OPENCODE_PORT, String(server.address().port));
            assert.equal(process.env.OPENCODE_PASSWORD, 'pw123');
        });
    } finally {
        await closeMock(server);
    }
});

test('CliServeManager: ensureRunning 遇 V2 需密码且无法获取 → 失败并给指引(不拉起)', async () => {
    const server = await listenMock(makeV2Handler('pw123'));
    const stateRoot = tmpDir('oc-state-');
    try {
        await withEnv({
            OPENCODE_HOST: '127.0.0.1',
            OPENCODE_PORT: String(server.address().port),
            OPENCODE_PASSWORD: undefined,
            XDG_STATE_HOME: stateRoot,
        }, async () => {
            const mgr = new CliServeManager({ projectRoot: tmpDir('oc-proj-'), cliType: 'opencode' });
            const r = await mgr.ensureRunning();
            assert.equal(r.ok, false);
            assert.equal(r.spawned, false);
            assert.match(r.message, /需要密码/);
            assert.ok(r.hint, '应给出处理指引');
            assert.match(r.hint, /OPENCODE_PASSWORD/);
        });
    } finally {
        await closeMock(server);
    }
});

test('CliServeManager: checkHealth 对无服务端口返回 down(不拉起)', async () => {
    const server = await listenMock(() => {});
    const port = server.address().port;
    await closeMock(server);
    const mgr = new CliServeManager({
        projectRoot: tmpDir('oc-proj-'), cliType: 'opencode', host: '127.0.0.1', port,
    });
    const r = await mgr.checkHealth();
    assert.equal(r.ok, false);
    assert.equal(r.reason, 'down');
});

// ─── precheckEnv 集成(doctor 检查项装配) ──────────────────────────────────

test('precheckEnv: CLI_TYPE=opencode 时 serve 检查识别 V2 需密码并阻断', async () => {
    const server = await listenMock(makeV2Handler('pw123'));
    const configRoot = tmpDir('oc-cfg-');
    const stateRoot = tmpDir('oc-state-');
    const skillDir = tmpDir('oc-skill-');
    const dataDir = tmpDir('oc-data-');
    try {
        await withEnv({
            CLI_TYPE: 'opencode',
            OPENCODE_HOST: '127.0.0.1',
            OPENCODE_PORT: String(server.address().port),
            OPENCODE_PASSWORD: undefined,
            XDG_CONFIG_HOME: configRoot,
            XDG_STATE_HOME: stateRoot,
            GIT_TOKEN: 'dummy-token',
        }, async () => {
            const { ok, failures } = await precheckEnv({
                projectRoot: tmpDir('oc-proj-'),
                skillDir,
                dataDir,
            });
            assert.equal(ok, false);
            const serveFailure = failures.find(f => f.name === 'opencode serve 服务');
            assert.ok(serveFailure, 'serve 检查失败应出现在 failures 中');
            assert.match(serveFailure.detail, /V2 API/);
            assert.match(serveFailure.fix, /OPENCODE_PASSWORD/);
        });
    } finally {
        await closeMock(server);
    }
});

test('precheckEnv: V1 serve 运行中(/session JSON)→ serve 检查通过', async () => {
    const server = await listenMock(v1Handler);
    const configRoot = tmpDir('oc-cfg-');
    const stateRoot = tmpDir('oc-state-');
    const skillDir = tmpDir('oc-skill-');
    const dataDir = tmpDir('oc-data-');
    try {
        await withEnv({
            CLI_TYPE: 'opencode',
            OPENCODE_HOST: '127.0.0.1',
            OPENCODE_PORT: String(server.address().port),
            OPENCODE_PASSWORD: undefined,
            XDG_CONFIG_HOME: configRoot,
            XDG_STATE_HOME: stateRoot,
            GIT_TOKEN: 'dummy-token',
        }, async () => {
            const { failures } = await precheckEnv({
                projectRoot: tmpDir('oc-proj-'),
                skillDir,
                dataDir,
            });
            // serve 检查不阻断(运行中的 V1 服务;其余失败项与本测试无关——
            // 如本机未装 opencode 会导致 CLI 检查失败,这里只断言 serve 不在其中)
            const serveFailure = failures.find(f => f.name === 'opencode serve 服务');
            assert.equal(serveFailure, undefined);
        });
    } finally {
        await closeMock(server);
    }
});