/**
 * DesktopOpService 单元测试
 * 覆盖：runAndWait / enqueue / abortByFilter / 队列溢出 / 持久化 / Mutex 调度
 */

import { DesktopOpService } from '../../../../src/modules/desktop_op/service/desktop_op.js';
import type { DesktopTask } from '../../../../src/modules/desktop_op/runtime/types.js';

// ─── 辅助：deferred promise ───────────────────────────────────────────────────
function deferred<T>() {
  let resolve!: (v: T) => void;
  let reject!: (e: any) => void;
  const promise = new Promise<T>((res, rej) => {
    resolve = res;
    reject = rej;
  });
  return { promise, resolve, reject };
}

// ─── 辅助：构造 service 实例（注入全部 mock） ─────────────────────────────────
function makeService(over: Partial<{
  runtime: any;
  desktopMutex: any;
  adapterRegistry: any;
  logRepo: any;
  logger: any;
}> = {}) {
  const s = new DesktopOpService();
  (s as any).runtime = over.runtime ?? {
    runTask: jest.fn().mockResolvedValue({ ok: true, modelCalls: 1, steps: 2, durationMs: 5 }),
  };
  (s as any).desktopMutex = over.desktopMutex ?? {
    acquire: jest.fn().mockResolvedValue(() => {}),
  };
  (s as any).adapterRegistry = over.adapterRegistry ?? {
    get: jest.fn().mockReturnValue({ queueKey: (t: any) => t?.conversation ?? 'k' }),
  };
  (s as any).logRepo = over.logRepo ?? {
    save: jest.fn().mockResolvedValue({}),
  };
  (s as any).logger = over.logger ?? {
    warn: jest.fn(),
    info: jest.fn(),
  };
  return s;
}

// ─── 辅助：构造 DesktopTask ────────────────────────────────────────────────────
const makeTask = (over: Partial<DesktopTask> = {}): DesktopTask => ({
  id: 't' + Math.random().toString(36).slice(2),
  appId: 'weixin',
  target: { conversation: 'X', channelId: 5, roomName: 'X' },
  actionType: 'send-text',
  params: { text: 'hi' },
  enqueuedAt: Date.now(),
  ...over,
});

// ─────────────────────────────────────────────────────────────────────────────
describe('DesktopOpService', () => {
  // 测试 1：runAndWait 正常完成
  it('1. runAndWait 成功返回 { ok, taskId, modelCalls, steps, durationMs }，日志写 success', async () => {
    const logRepo = { save: jest.fn().mockResolvedValue({}) };
    const s = makeService({ logRepo });
    const task = makeTask();

    const result = await s.runAndWait(task);

    expect(result).toMatchObject({ ok: true, taskId: task.id, modelCalls: 1, steps: 2, durationMs: 5 });

    // 等待 fire-and-forget 的 persistLog 落库
    await new Promise(r => setImmediate(r));
    expect(logRepo.save).toHaveBeenCalledWith(
      expect.objectContaining({ taskId: task.id, status: 'success' })
    );
  });

  // 测试 2：runAndWait 运行时抛错 → reject + 日志写错误 token
  it('2. runtime.runTask 抛 verify-failed → reject，日志写 status=verify-failed', async () => {
    const logRepo = { save: jest.fn().mockResolvedValue({}) };
    const runtime = { runTask: jest.fn().mockRejectedValue(new Error('verify-failed')) };
    const s = makeService({ runtime, logRepo });
    const task = makeTask();

    await expect(s.runAndWait(task)).rejects.toThrow('verify-failed');

    await new Promise(r => setImmediate(r));
    expect(logRepo.save).toHaveBeenCalledWith(
      expect.objectContaining({ taskId: task.id, status: 'verify-failed' })
    );
  });

  // 测试 3：runAndWait 超时（真实计时器，50ms 超时）
  it('3. runtime 永远不 resolve → 50ms 后 reject task-timeout', async () => {
    jest.useRealTimers();
    const logRepo = { save: jest.fn().mockResolvedValue({}) };
    // runtime 返回一个永远不 resolve 的 Promise，同时检测 abort 信号
    const d = deferred<any>();
    const runtime = {
      runTask: jest.fn().mockImplementation((_task: any, signal: AbortSignal) => {
        return new Promise((_res, rej) => {
          signal.addEventListener('abort', () => rej(new Error('aborted')));
        });
      }),
    };
    const s = makeService({ runtime, logRepo });
    const task = makeTask();

    await expect(s.runAndWait(task, 50)).rejects.toThrow('task-timeout');

    // 等待 persistLog 落库
    await new Promise(r => setTimeout(r, 20));
    const calls = logRepo.save.mock.calls.map((c: any[]) => c[0]);
    // 应有 task-timeout 或 aborted 状态的日志
    const hasTimeoutLog = calls.some(
      (c: any) => c.taskId === task.id && (c.status === 'task-timeout' || c.status === 'aborted')
    );
    expect(hasTimeoutLog).toBe(true);

    d.resolve(undefined); // 清理 deferred（可选）
  });

  // 测试 4：enqueue fire-and-forget，返回 { taskId, queuePosition:1 }，最终 runtime 被调用
  it('4. enqueue 返回 { taskId, queuePosition:1 }，runtime.runTask 最终被调用', async () => {
    const runtime = { runTask: jest.fn().mockResolvedValue({ ok: true, modelCalls: 0, steps: 0, durationMs: 0 }) };
    const s = makeService({ runtime });
    const task = makeTask();

    const r = s.enqueue(task);
    expect(r).toMatchObject({ taskId: task.id, queuePosition: 1 });

    // 等待 worker 执行
    await new Promise(r2 => setTimeout(r2, 20));
    expect(runtime.runTask).toHaveBeenCalledWith(expect.objectContaining({ id: task.id }), expect.any(AbortSignal));
  });

  // 测试 5：同 target 两个任务串行执行
  it('5. 同 target 两个任务串行：第二个 runtime.runTask 在第一个完成后才被调用', async () => {
    jest.useRealTimers();
    const d1 = deferred<any>();
    const d2 = deferred<any>();
    const defs = [d1, d2];
    let callIdx = 0;
    const runtime = {
      runTask: jest.fn().mockImplementation(() => defs[callIdx++].promise),
    };
    const s = makeService({ runtime });
    const task1 = makeTask({ target: { conversation: 'same', channelId: 1, roomName: 'same' } });
    const task2 = makeTask({ target: { conversation: 'same', channelId: 1, roomName: 'same' } });

    s.enqueue(task1);
    s.enqueue(task2);

    // 给 worker 循环调度机会
    await new Promise(r => setImmediate(r));

    // 此时只有 task1 的 runTask 被调用
    expect(runtime.runTask).toHaveBeenCalledTimes(1);
    expect(runtime.runTask.mock.calls[0][0].id).toBe(task1.id);

    // 完成 task1
    d1.resolve({ ok: true, modelCalls: 0, steps: 0, durationMs: 0 });
    await new Promise(r => setTimeout(r, 20));

    // 现在 task2 的 runTask 被调用
    expect(runtime.runTask).toHaveBeenCalledTimes(2);
    expect(runtime.runTask.mock.calls[1][0].id).toBe(task2.id);

    d2.resolve({ ok: true, modelCalls: 0, steps: 0, durationMs: 0 });
    await new Promise(r => setTimeout(r, 10));
  });

  // 测试 6：不同 target 可以并发（Mutex 保证同一时刻只有一个持锁）
  it('6. 不同 target 各自 worker，Mutex 保证同一时刻只有一个任务持锁', async () => {
    jest.useRealTimers();
    let concurrentCount = 0;
    let maxConcurrent = 0;
    const mutex = {
      acquire: jest.fn().mockImplementation(async () => {
        concurrentCount++;
        maxConcurrent = Math.max(maxConcurrent, concurrentCount);
        return () => { concurrentCount--; };
      }),
    };
    const d1 = deferred<any>();
    const d2 = deferred<any>();
    const dMap = new Map<string, ReturnType<typeof deferred>>();
    const runtime = {
      runTask: jest.fn().mockImplementation((task: DesktopTask) => {
        const d = deferred<any>();
        dMap.set(task.id, d);
        return d.promise;
      }),
    };
    const s = makeService({ runtime, desktopMutex: mutex });
    const taskA = makeTask({ id: 'tA', target: { conversation: 'conv-A', channelId: 1, roomName: 'A' } });
    const taskB = makeTask({ id: 'tB', target: { conversation: 'conv-B', channelId: 2, roomName: 'B' } });

    s.enqueue(taskA);
    s.enqueue(taskB);

    // 等待两个 worker 启动并等待 mutex
    await new Promise(r => setTimeout(r, 30));

    // 真实 Mutex：最多 1 个并发持锁
    // 但我们用 mock mutex，mock 不串行——这里只验证 acquire 被调用了 2 次
    expect(mutex.acquire).toHaveBeenCalledTimes(2);

    // 完成两个任务
    for (const d of dMap.values()) {
      d.resolve({ ok: true, modelCalls: 0, steps: 0, durationMs: 0 });
    }
    await new Promise(r => setTimeout(r, 20));
  });

  // 测试 7：队列溢出 — 第 21 个 pending 任务挤出第 1 个 pending 任务
  it('7. 入队 21 个任务，最早的 pending 任务被溢出，日志写 queue-overflow', async () => {
    jest.useRealTimers();
    const logRepo = { save: jest.fn().mockResolvedValue({}) };
    // runtime 永不 resolve，保证 worker 持续阻塞（task[0] 占住 worker，其余堆在队列）
    const blockDeferred = deferred<any>();
    const runtime = { runTask: jest.fn().mockReturnValue(blockDeferred.promise) };
    const s = makeService({ runtime, logRepo });

    const sameTarget = { conversation: 'X', channelId: 5, roomName: 'X' };
    // task[0] 入队并启动 worker（worker 永远阻塞在 task[0]）
    const task0 = makeTask({ id: 'tq0', target: sameTarget });
    s.enqueue(task0);

    // 等待 worker 开始消费 task[0]（task[0] 被 shift 出队列）
    await new Promise(r => setTimeout(r, 10));

    // 现在往队列里放 task[1..20]（共 20 个），队列恰好满
    const pendingTasks: DesktopTask[] = [];
    for (let i = 1; i <= 20; i++) {
      const t = makeTask({ id: `tq${i}`, target: sameTarget });
      pendingTasks.push(t);
      s.enqueue(t);
    }

    // 用 runAndWait 订阅 task[1]（此时 task[1] 在队列第一位）
    // 注意：task[1] 已经被入队了，需要用已有的 waiters 机制
    // 改为：单独订阅一个新任务 tqWait，它是队列里第一个（通过 enqueue 之后 runAndWait 无法追溯）
    // 所以改策略：先 enqueue 19 个填满队列（到 19），然后 runAndWait 第 20 个（queuePosition=20=MAX），
    // 再 enqueue 第 21 个 → 挤出第 1 个 pending（tq1），tqWait 还未溢出
    // 最简方案：直接检查日志里有 queue-overflow 即可，不检查 promise reject
    const extraTask = makeTask({ id: 'tq21', target: sameTarget });
    s.enqueue(extraTask);

    // 等待溢出处理
    await new Promise(r => setTimeout(r, 30));

    // 日志包含 queue-overflow
    const logCalls = logRepo.save.mock.calls.map((c: any[]) => c[0]);
    const overflowLog = logCalls.find((c: any) => c.status === 'queue-overflow');
    expect(overflowLog).toBeDefined();
    expect(overflowLog.taskId).toBe('tq1'); // tq1 是最早的 pending 任务被溢出

    blockDeferred.resolve(undefined);
  });

  // 测试 8：abortByFilter 清除 pending 队列 + 中断正在运行的任务
  it('8. abortByFilter 清除 pending 队列并中断 running 任务', async () => {
    jest.useRealTimers();
    const logRepo = { save: jest.fn().mockResolvedValue({}) };
    // 第一个任务永远阻塞，监听 abort 信号
    const d1 = deferred<any>();
    let abortSignalRef: AbortSignal | null = null;
    const runtime = {
      runTask: jest.fn().mockImplementation((_task: any, signal: AbortSignal) => {
        abortSignalRef = signal;
        return new Promise((_res, rej) => {
          signal.addEventListener('abort', () => rej(new Error('aborted')));
        });
      }),
    };
    const s = makeService({ runtime, logRepo });

    const task1 = makeTask({ id: 'tab1', target: { conversation: 'X', channelId: 5, roomName: 'X' } });
    const task2 = makeTask({ id: 'tab2', target: { conversation: 'X', channelId: 5, roomName: 'X' } });
    const task3 = makeTask({ id: 'tab3', target: { conversation: 'X', channelId: 5, roomName: 'X' } });

    // p1 会因为 abortByFilter 触发 abort 信号，worker 的 catch 会 rejectWaiter，需要 catch 避免 unhandled rejection
    const p1 = s.runAndWait(task1, 10000).catch(() => {/* 预期被 abort */});
    s.enqueue(task2);
    s.enqueue(task3);

    // 等待 task1 开始执行
    await new Promise(r => setTimeout(r, 30));
    expect(runtime.runTask).toHaveBeenCalledTimes(1);

    // 触发 cascading abort
    s.abortByFilter(t => t.target?.channelId === 5, 'channel-deleted');

    // 验证 task1 的 abort 信号已触发
    expect(abortSignalRef?.aborted).toBe(true);

    // 等待 worker 处理完 + 日志写入
    await new Promise(r => setTimeout(r, 50));
    await p1;

    const logCalls = logRepo.save.mock.calls.map((c: any[]) => c[0]);

    // task2/task3 应有 aborted 日志
    const abortedLogs = logCalls.filter((c: any) => c.status === 'aborted');
    expect(abortedLogs.length).toBeGreaterThanOrEqual(2);
    const reasons = abortedLogs.map((c: any) => c.abortedReason);
    expect(reasons.every((r: string) => r === 'channel-deleted')).toBe(true);

    d1.resolve(undefined);
  });

  // 测试 9：持久化字段验证 — success 时日志含完整字段
  it('9. success 时 logRepo.save 包含 taskId / appId / targetJson / channelId / roomName / status / modelCalls / steps / durationMs', async () => {
    const logRepo = { save: jest.fn().mockResolvedValue({}) };
    const s = makeService({ logRepo });
    const task = makeTask({
      id: 'tpersist',
      appId: 'weixin',
      target: { conversation: 'X', channelId: 5, roomName: 'room1' },
    });

    await s.runAndWait(task);
    await new Promise(r => setImmediate(r));

    expect(logRepo.save).toHaveBeenCalledWith(
      expect.objectContaining({
        taskId: 'tpersist',
        appId: 'weixin',
        channelId: 5,
        roomName: 'room1',
        status: 'success',
        modelCalls: 1,
        steps: 2,
        durationMs: 5,
      })
    );
  });

  // 测试 10：持久化含 error 消息
  it('10. runtime 抛 window-not-found → save 包含 status=window-not-found 和 error 字段', async () => {
    const logRepo = { save: jest.fn().mockResolvedValue({}) };
    const runtime = { runTask: jest.fn().mockRejectedValue(new Error('window-not-found')) };
    const s = makeService({ runtime, logRepo });
    const task = makeTask();

    await expect(s.runAndWait(task)).rejects.toThrow('window-not-found');
    await new Promise(r => setImmediate(r));

    expect(logRepo.save).toHaveBeenCalledWith(
      expect.objectContaining({
        status: 'window-not-found',
        error: 'window-not-found',
      })
    );
  });

  // 测试 11：errToStatus 前缀匹配 — 带后缀的错误消息正确截取 token
  it('11. 错误消息 "precondition-failed: not in chat" → log status=precondition-failed', async () => {
    const logRepo = { save: jest.fn().mockResolvedValue({}) };
    const runtime = {
      runTask: jest.fn().mockRejectedValue(new Error('precondition-failed: not in chat')),
    };
    const s = makeService({ runtime, logRepo });
    const task = makeTask();

    await expect(s.runAndWait(task)).rejects.toThrow();
    await new Promise(r => setImmediate(r));

    expect(logRepo.save).toHaveBeenCalledWith(
      expect.objectContaining({ status: 'precondition-failed' })
    );
  });

  // 测试 12：Mutex acquire 调用一次，release 一次（含异常路径）
  it('12. mutex.acquire 调用一次，release 一次，异常时也 release', async () => {
    const releaseFn = jest.fn();
    const mutex = {
      acquire: jest.fn().mockResolvedValue(releaseFn),
    };
    const runtime = { runTask: jest.fn().mockRejectedValue(new Error('model-failed')) };
    const s = makeService({ runtime, desktopMutex: mutex });
    const task = makeTask();

    await expect(s.runAndWait(task)).rejects.toThrow('model-failed');

    // acquire 调用 1 次
    expect(mutex.acquire).toHaveBeenCalledTimes(1);
    expect(mutex.acquire).toHaveBeenCalledWith(task.id, task.appId);

    // release 也应被调用 1 次（finally 块）
    expect(releaseFn).toHaveBeenCalledTimes(1);
  });
});
