import { describe, it, expect, beforeEach, vi } from 'vitest';

import {
  _setRegisteredGroupForTests,
  _initTestDatabase,
  assignRoom,
  createTask,
  getAllTasks,
  recallMemories,
  getRegisteredAgentTypesForJid,
  getRegisteredGroup,
  getStoredRoomSettings,
  getTaskById,
} from './db.js';
import { forwardAuthorizedIpcMessage, processTaskIpc, IpcDeps } from './ipc.js';
import { DEFAULT_WATCH_CI_MAX_DURATION_MS } from './task-watch-status.js';
import { RegisteredGroup } from './types.js';

// Set up registered groups used across tests
const MAIN_GROUP: RegisteredGroup = {
  name: 'Main',
  folder: 'whatsapp_main',
  trigger: 'always',
  added_at: '2024-01-01T00:00:00.000Z',
  isMain: true,
};

const OTHER_GROUP: RegisteredGroup = {
  name: 'Other',
  folder: 'other-group',
  trigger: '@Andy',
  added_at: '2024-01-01T00:00:00.000Z',
};

const THIRD_GROUP: RegisteredGroup = {
  name: 'Third',
  folder: 'third-group',
  trigger: '@Andy',
  added_at: '2024-01-01T00:00:00.000Z',
};

let groups: Record<string, RegisteredGroup>;
let deps: IpcDeps;

beforeEach(() => {
  _initTestDatabase();

  groups = {
    'main@g.us': MAIN_GROUP,
    'other@g.us': OTHER_GROUP,
    'third@g.us': THIRD_GROUP,
  };

  // Populate DB as well
  _setRegisteredGroupForTests('main@g.us', MAIN_GROUP);
  _setRegisteredGroupForTests('other@g.us', OTHER_GROUP);
  _setRegisteredGroupForTests('third@g.us', THIRD_GROUP);

  deps = {
    sendMessage: async () => {},
    nudgeScheduler: vi.fn(),
    roomBindings: () => groups,
    assignRoom: (jid, room) => {
      const assigned = assignRoom(jid, room);
      if (!assigned) return;
      const { jid: _ignoredJid, ...group } = assigned;
      groups[jid] = group;
    },
    syncGroups: async () => {},
    getAvailableGroups: () => [],
    writeGroupsSnapshot: () => {},
  };
});

// --- schedule_task authorization ---

describe('schedule_task authorization', () => {
  it('main group can schedule for another group', async () => {
    await processTaskIpc(
      {
        type: 'schedule_task',
        prompt: 'do something',
        schedule_type: 'once',
        schedule_value: '2025-06-01T00:00:00',
        targetJid: 'other@g.us',
      },
      'whatsapp_main',
      true,
      deps,
    );

    // Verify task was created in DB for the other group
    const allTasks = getAllTasks();
    expect(allTasks.length).toBe(1);
    expect(allTasks[0].group_folder).toBe('other-group');
  });

  it('non-main group can schedule for itself', async () => {
    await processTaskIpc(
      {
        type: 'schedule_task',
        prompt: 'self task',
        schedule_type: 'once',
        schedule_value: '2025-06-01T00:00:00',
        targetJid: 'other@g.us',
      },
      'other-group',
      false,
      deps,
    );

    const allTasks = getAllTasks();
    expect(allTasks.length).toBe(1);
    expect(allTasks[0].group_folder).toBe('other-group');
  });

  it('stores the target group agent type on scheduled tasks', async () => {
    groups['other@g.us'] = {
      ...OTHER_GROUP,
      agentType: 'codex',
    };
    _setRegisteredGroupForTests('other@g.us', groups['other@g.us']);

    await processTaskIpc(
      {
        type: 'schedule_task',
        prompt: 'codex owned task',
        schedule_type: 'once',
        schedule_value: '2025-06-01T00:00:00',
        targetJid: 'other@g.us',
      },
      'whatsapp_main',
      true,
      deps,
    );

    const allTasks = getAllTasks();
    expect(allTasks).toHaveLength(1);
    expect(allTasks[0].agent_type).toBe('codex');
  });

  it('stores the scheduling caller agent type when provided', async () => {
    groups['other@g.us'] = {
      ...OTHER_GROUP,
      agentType: 'claude-code',
    };
    _setRegisteredGroupForTests('other@g.us', groups['other@g.us']);

    await processTaskIpc(
      {
        type: 'schedule_task',
        prompt: 'caller owned task',
        schedule_type: 'once',
        schedule_value: '2025-06-01T00:00:00',
        targetJid: 'other@g.us',
        agent_type: 'codex',
        room_role: 'owner',
      },
      'whatsapp_main',
      true,
      deps,
    );

    const allTasks = getAllTasks();
    expect(allTasks).toHaveLength(1);
    expect(allTasks[0].agent_type).toBe('codex');
    expect(allTasks[0].room_role).toBe('owner');
  });

  it('non-main group cannot schedule for another group', async () => {
    await processTaskIpc(
      {
        type: 'schedule_task',
        prompt: 'unauthorized',
        schedule_type: 'once',
        schedule_value: '2025-06-01T00:00:00',
        targetJid: 'main@g.us',
      },
      'other-group',
      false,
      deps,
    );

    const allTasks = getAllTasks();
    expect(allTasks.length).toBe(0);
  });

  it('rejects schedule_task for unregistered target JID', async () => {
    await processTaskIpc(
      {
        type: 'schedule_task',
        prompt: 'no target',
        schedule_type: 'once',
        schedule_value: '2025-06-01T00:00:00',
        targetJid: 'unknown@g.us',
      },
      'whatsapp_main',
      true,
      deps,
    );

    const allTasks = getAllTasks();
    expect(allTasks.length).toBe(0);
  });

  it('accepts a target folder name and resolves it to the registered JID', async () => {
    await processTaskIpc(
      {
        type: 'schedule_task',
        prompt: 'folder-based target',
        schedule_type: 'once',
        schedule_value: '2025-06-01T00:00:00',
        targetJid: 'other-group',
      },
      'other-group',
      false,
      deps,
    );

    const allTasks = getAllTasks();
    expect(allTasks).toHaveLength(1);
    expect(allTasks[0].group_folder).toBe('other-group');
    expect(allTasks[0].chat_jid).toBe('other@g.us');
  });
});

describe('persist_memory authorization', () => {
  it('allows a room runtime to persist memory for its own room scope', async () => {
    await processTaskIpc(
      {
        type: 'persist_memory',
        scopeKind: 'room',
        scopeKey: 'room:other-group',
        content: 'compact summary',
        keywords: ['room:other-group'],
        source_kind: 'compact',
        source_ref: 'compact:test',
      },
      'other-group',
      false,
      deps,
    );

    const recalled = recallMemories({
      scopeKind: 'room',
      scopeKey: 'room:other-group',
      limit: 10,
    });
    expect(recalled).toHaveLength(1);
    expect(recalled[0].content).toBe('compact summary');
  });

  it('blocks persist_memory when scopeKey does not match the source group', async () => {
    await processTaskIpc(
      {
        type: 'persist_memory',
        scopeKind: 'room',
        scopeKey: 'room:third-group',
        content: 'should be denied',
        keywords: ['room:third-group'],
        source_kind: 'compact',
      },
      'other-group',
      false,
      deps,
    );

    const recalled = recallMemories({
      scopeKind: 'room',
      scopeKey: 'room:third-group',
      limit: 10,
    });
    expect(recalled).toHaveLength(0);
  });
});

// --- pause_task authorization ---

describe('pause_task authorization', () => {
  beforeEach(() => {
    createTask({
      id: 'task-main',
      group_folder: 'whatsapp_main',
      chat_jid: 'main@g.us',
      prompt: 'main task',
      schedule_type: 'once',
      schedule_value: '2025-06-01T00:00:00',
      context_mode: 'isolated',
      next_run: '2025-06-01T00:00:00.000Z',
      status: 'active',
      created_at: '2024-01-01T00:00:00.000Z',
    });
    createTask({
      id: 'task-other',
      group_folder: 'other-group',
      chat_jid: 'other@g.us',
      prompt: 'other task',
      schedule_type: 'once',
      schedule_value: '2025-06-01T00:00:00',
      context_mode: 'isolated',
      next_run: '2025-06-01T00:00:00.000Z',
      status: 'active',
      created_at: '2024-01-01T00:00:00.000Z',
    });
  });

  it('main group can pause any task', async () => {
    await processTaskIpc(
      { type: 'pause_task', taskId: 'task-other' },
      'whatsapp_main',
      true,
      deps,
    );
    expect(getTaskById('task-other')!.status).toBe('paused');
  });

  it('non-main group can pause its own task', async () => {
    await processTaskIpc(
      { type: 'pause_task', taskId: 'task-other' },
      'other-group',
      false,
      deps,
    );
    expect(getTaskById('task-other')!.status).toBe('paused');
  });

  it('non-main group cannot pause another groups task', async () => {
    await processTaskIpc(
      { type: 'pause_task', taskId: 'task-main' },
      'other-group',
      false,
      deps,
    );
    expect(getTaskById('task-main')!.status).toBe('active');
  });
});

// --- resume_task authorization ---

describe('resume_task authorization', () => {
  beforeEach(() => {
    createTask({
      id: 'task-paused',
      group_folder: 'other-group',
      chat_jid: 'other@g.us',
      prompt: 'paused task',
      schedule_type: 'once',
      schedule_value: '2025-06-01T00:00:00',
      context_mode: 'isolated',
      next_run: '2025-06-01T00:00:00.000Z',
      status: 'paused',
      created_at: '2024-01-01T00:00:00.000Z',
    });
  });

  it('main group can resume any task', async () => {
    await processTaskIpc(
      { type: 'resume_task', taskId: 'task-paused' },
      'whatsapp_main',
      true,
      deps,
    );
    expect(getTaskById('task-paused')!.status).toBe('active');
  });

  it('non-main group can resume its own task', async () => {
    await processTaskIpc(
      { type: 'resume_task', taskId: 'task-paused' },
      'other-group',
      false,
      deps,
    );
    expect(getTaskById('task-paused')!.status).toBe('active');
  });

  it('non-main group cannot resume another groups task', async () => {
    await processTaskIpc(
      { type: 'resume_task', taskId: 'task-paused' },
      'third-group',
      false,
      deps,
    );
    expect(getTaskById('task-paused')!.status).toBe('paused');
  });
});

// --- update_task behavior ---

describe('update_task behavior', () => {
  it('updates once tasks even when next_run is not recomputed', async () => {
    createTask({
      id: 'task-once-update',
      group_folder: 'other-group',
      chat_jid: 'other@g.us',
      prompt: 'original once task',
      schedule_type: 'once',
      schedule_value: '2025-06-01T00:00:00',
      context_mode: 'isolated',
      next_run: '2025-06-01T00:00:00.000Z',
      status: 'active',
      created_at: '2024-01-01T00:00:00.000Z',
    });

    await processTaskIpc(
      {
        type: 'update_task',
        taskId: 'task-once-update',
        prompt: 'updated once task',
        schedule_value: '2025-07-01T00:00:00',
      },
      'other-group',
      false,
      deps,
    );

    const task = getTaskById('task-once-update')!;
    expect(task.prompt).toBe('updated once task');
    expect(task.schedule_value).toBe('2025-07-01T00:00:00');
    expect(task.next_run).toBe('2025-06-01T00:00:00.000Z');
  });
});

// --- cancel_task authorization ---

describe('cancel_task authorization', () => {
  it('main group can cancel any task', async () => {
    createTask({
      id: 'task-to-cancel',
      group_folder: 'other-group',
      chat_jid: 'other@g.us',
      prompt: 'cancel me',
      schedule_type: 'once',
      schedule_value: '2025-06-01T00:00:00',
      context_mode: 'isolated',
      next_run: null,
      status: 'active',
      created_at: '2024-01-01T00:00:00.000Z',
    });

    await processTaskIpc(
      { type: 'cancel_task', taskId: 'task-to-cancel' },
      'whatsapp_main',
      true,
      deps,
    );
    expect(getTaskById('task-to-cancel')).toBeUndefined();
  });

  it('non-main group can cancel its own task', async () => {
    createTask({
      id: 'task-own',
      group_folder: 'other-group',
      chat_jid: 'other@g.us',
      prompt: 'my task',
      schedule_type: 'once',
      schedule_value: '2025-06-01T00:00:00',
      context_mode: 'isolated',
      next_run: null,
      status: 'active',
      created_at: '2024-01-01T00:00:00.000Z',
    });

    await processTaskIpc(
      { type: 'cancel_task', taskId: 'task-own' },
      'other-group',
      false,
      deps,
    );
    expect(getTaskById('task-own')).toBeUndefined();
  });

  it('non-main group cannot cancel another groups task', async () => {
    createTask({
      id: 'task-foreign',
      group_folder: 'whatsapp_main',
      chat_jid: 'main@g.us',
      prompt: 'not yours',
      schedule_type: 'once',
      schedule_value: '2025-06-01T00:00:00',
      context_mode: 'isolated',
      next_run: null,
      status: 'active',
      created_at: '2024-01-01T00:00:00.000Z',
    });

    await processTaskIpc(
      { type: 'cancel_task', taskId: 'task-foreign' },
      'other-group',
      false,
      deps,
    );
    expect(getTaskById('task-foreign')).toBeDefined();
  });
});

// --- assign_room authorization ---

describe('assign_room authorization', () => {
  it('non-main group cannot assign a room', async () => {
    await processTaskIpc(
      {
        type: 'assign_room',
        jid: 'new@g.us',
        name: 'New Group',
        folder: 'new-group',
      },
      'other-group',
      false,
      deps,
    );

    // roomBindings should not have changed
    expect(groups['new@g.us']).toBeUndefined();
  });

  it('main group cannot assign with unsafe folder path', async () => {
    await processTaskIpc(
      {
        type: 'assign_room',
        jid: 'new@g.us',
        name: 'New Group',
        folder: '../../outside',
      },
      'whatsapp_main',
      true,
      deps,
    );

    expect(groups['new@g.us']).toBeUndefined();
  });
});

// --- refresh_groups authorization ---

describe('refresh_groups authorization', () => {
  it('non-main group cannot trigger refresh', async () => {
    // This should be silently blocked (no crash, no effect)
    await processTaskIpc(
      { type: 'refresh_groups' },
      'other-group',
      false,
      deps,
    );
    // If we got here without error, the auth gate worked
  });
});

// --- IPC message authorization ---
// Tests the authorization pattern from startIpcWatcher (ipc.ts).
// The logic: isMain || (targetGroup && targetGroup.folder === sourceGroup)

describe('IPC message authorization', () => {
  // Replicate the exact check from the IPC watcher
  function isMessageAuthorized(
    sourceGroup: string,
    isMain: boolean,
    targetChatJid: string,
    roomBindings: Record<string, RegisteredGroup>,
  ): boolean {
    const targetGroup = roomBindings[targetChatJid];
    return isMain || (!!targetGroup && targetGroup.folder === sourceGroup);
  }

  it('main group can send to any group', () => {
    expect(
      isMessageAuthorized('whatsapp_main', true, 'other@g.us', groups),
    ).toBe(true);
    expect(
      isMessageAuthorized('whatsapp_main', true, 'third@g.us', groups),
    ).toBe(true);
  });

  it('non-main group can send to its own chat', () => {
    expect(
      isMessageAuthorized('other-group', false, 'other@g.us', groups),
    ).toBe(true);
  });

  it('non-main group cannot send to another groups chat', () => {
    expect(isMessageAuthorized('other-group', false, 'main@g.us', groups)).toBe(
      false,
    );
    expect(
      isMessageAuthorized('other-group', false, 'third@g.us', groups),
    ).toBe(false);
  });

  it('non-main group cannot send to unregistered JID', () => {
    expect(
      isMessageAuthorized('other-group', false, 'unknown@g.us', groups),
    ).toBe(false);
  });

  it('main group can send to unregistered JID', () => {
    // Main is always authorized regardless of target
    expect(
      isMessageAuthorized('whatsapp_main', true, 'unknown@g.us', groups),
    ).toBe(true);
  });

  it('forwards senderRole through authorized IPC messages', async () => {
    const sendMessage = vi.fn(async () => {});

    const result = await forwardAuthorizedIpcMessage(
      {
        type: 'message',
        chatJid: 'other@g.us',
        text: 'review text',
        senderRole: 'reviewer',
        runId: 'run-reviewer-ipc',
      },
      'other-group',
      false,
      groups,
      sendMessage,
    );

    expect(sendMessage).toHaveBeenCalledWith(
      'other@g.us',
      'review text',
      'reviewer',
      'run-reviewer-ipc',
      undefined,
    );
    expect(result).toEqual(
      expect.objectContaining({
        outcome: 'sent',
        senderRole: 'reviewer',
      }),
    );
  });

  it('forwards structured attachments through authorized IPC messages', async () => {
    const sendMessage = vi.fn(async () => {});
    const attachments = [
      {
        path: '/tmp/ejclaw-room-mobile-list-390.png',
        name: 'room.png',
        mime: 'image/png',
      },
    ];

    const result = await forwardAuthorizedIpcMessage(
      {
        type: 'message',
        chatJid: 'other@g.us',
        text: '스크린샷입니다.',
        attachments,
      },
      'other-group',
      false,
      groups,
      sendMessage,
    );

    expect(sendMessage).toHaveBeenCalledWith(
      'other@g.us',
      '스크린샷입니다.',
      undefined,
      undefined,
      attachments,
    );
    expect(result.outcome).toBe('sent');
  });

  it('normalizes raw EJClaw envelopes in authorized IPC messages', async () => {
    const sendMessage = vi.fn(async () => {});

    const result = await forwardAuthorizedIpcMessage(
      {
        type: 'message',
        chatJid: 'other@g.us',
        text: JSON.stringify({
          ejclaw: {
            visibility: 'public',
            text: '첨부를 렌더링했습니다.',
            verdict: 'done',
            attachments: [
              {
                path: '/tmp/bar-chart-label-fit-playwright.png',
                name: 'bar-chart-label-fit-playwright.png',
                mime: 'image/png',
              },
            ],
          },
        }),
      },
      'other-group',
      false,
      groups,
      sendMessage,
    );

    expect(sendMessage).toHaveBeenCalledWith(
      'other@g.us',
      '첨부를 렌더링했습니다.',
      undefined,
      undefined,
      [
        {
          path: '/tmp/bar-chart-label-fit-playwright.png',
          name: 'bar-chart-label-fit-playwright.png',
          mime: 'image/png',
        },
      ],
    );
    expect(result.outcome).toBe('sent');
  });

  it('injects authorized inbound messages for the source group without routing them as outbound sends', async () => {
    const sendMessage = vi.fn(async () => {});
    const injectInboundMessage = vi.fn(async () => {});

    const result = await (forwardAuthorizedIpcMessage as any)(
      {
        type: 'inject_inbound_message',
        chatJid: 'other@g.us',
        text: 'act like a human input',
        senderRole: 'owner',
        sender: '1480575043007418420',
        senderName: 'algo',
        treatAsHuman: true,
        sourceKind: 'trusted_external_bot',
      },
      'other-group',
      false,
      groups,
      sendMessage,
      injectInboundMessage,
    );

    expect(sendMessage).not.toHaveBeenCalled();
    expect(injectInboundMessage).toHaveBeenCalledWith(
      expect.objectContaining({
        chatJid: 'other@g.us',
        text: 'act like a human input',
        sender: '1480575043007418420',
        senderName: 'algo',
        treatAsHuman: true,
        sourceKind: 'trusted_external_bot',
      }),
    );
    expect(result).toEqual(
      expect.objectContaining({
        outcome: 'sent',
        chatJid: 'other@g.us',
      }),
    );
  });

  it('does not inject unauthorized inbound messages for another group', async () => {
    const sendMessage = vi.fn(async () => {});
    const injectInboundMessage = vi.fn(async () => {});

    const result = await (forwardAuthorizedIpcMessage as any)(
      {
        type: 'inject_inbound_message',
        chatJid: 'third@g.us',
        text: 'should be blocked',
        sender: '1480575043007418420',
        senderName: 'algo',
        treatAsHuman: true,
        sourceKind: 'trusted_external_bot',
      },
      'other-group',
      false,
      groups,
      sendMessage,
      injectInboundMessage,
    );

    expect(result.outcome).toBe('blocked');
    expect(sendMessage).not.toHaveBeenCalled();
    expect(injectInboundMessage).not.toHaveBeenCalled();
  });

  it('does not forward unauthorized IPC messages even when senderRole exists', async () => {
    const sendMessage = vi.fn(async () => {});

    const result = await forwardAuthorizedIpcMessage(
      {
        type: 'message',
        chatJid: 'third@g.us',
        text: 'review text',
        senderRole: 'reviewer',
        runId: 'run-reviewer-ipc',
      },
      'other-group',
      false,
      groups,
      sendMessage,
    );

    expect(result.outcome).toBe('blocked');
    expect(result.senderRole).toBe('reviewer');
    expect(sendMessage).not.toHaveBeenCalled();
  });
});

// --- schedule_task with cron and interval types ---

describe('schedule_task schedule types', () => {
  it('creates task with cron schedule and computes next_run', async () => {
    await processTaskIpc(
      {
        type: 'schedule_task',
        prompt: 'cron task',
        schedule_type: 'cron',
        schedule_value: '0 9 * * *', // every day at 9am
        targetJid: 'other@g.us',
      },
      'whatsapp_main',
      true,
      deps,
    );

    const tasks = getAllTasks();
    expect(tasks).toHaveLength(1);
    expect(tasks[0].schedule_type).toBe('cron');
    expect(tasks[0].next_run).toBeTruthy();
    // next_run should be a valid ISO date in the future
    expect(new Date(tasks[0].next_run!).getTime()).toBeGreaterThan(
      Date.now() - 60000,
    );
  });

  it('rejects invalid cron expression', async () => {
    await processTaskIpc(
      {
        type: 'schedule_task',
        prompt: 'bad cron',
        schedule_type: 'cron',
        schedule_value: 'not a cron',
        targetJid: 'other@g.us',
      },
      'whatsapp_main',
      true,
      deps,
    );

    expect(getAllTasks()).toHaveLength(0);
  });

  it('creates task with interval schedule', async () => {
    const before = Date.now();

    await processTaskIpc(
      {
        type: 'schedule_task',
        prompt: 'interval task',
        schedule_type: 'interval',
        schedule_value: '3600000', // 1 hour
        targetJid: 'other@g.us',
      },
      'whatsapp_main',
      true,
      deps,
    );

    const tasks = getAllTasks();
    expect(tasks).toHaveLength(1);
    expect(tasks[0].schedule_type).toBe('interval');
    // next_run should be ~1 hour from now
    const nextRun = new Date(tasks[0].next_run!).getTime();
    expect(nextRun).toBeGreaterThanOrEqual(before + 3600000 - 1000);
    expect(nextRun).toBeLessThanOrEqual(Date.now() + 3600000 + 1000);
  });

  it('starts watch_ci interval tasks immediately and nudges the scheduler', async () => {
    const before = Date.now();

    await processTaskIpc(
      {
        type: 'schedule_task',
        prompt: `
[BACKGROUND CI WATCH]

Watch target:
GitHub Actions run 123456

Check instructions:
Check the run.
        `.trim(),
        schedule_type: 'interval',
        schedule_value: '60000',
        targetJid: 'other@g.us',
      },
      'whatsapp_main',
      true,
      deps,
    );

    const tasks = getAllTasks();
    expect(tasks).toHaveLength(1);
    expect(tasks[0].schedule_type).toBe('interval');
    expect(tasks[0].max_duration_ms).toBe(DEFAULT_WATCH_CI_MAX_DURATION_MS);
    const nextRun = new Date(tasks[0].next_run!).getTime();
    expect(nextRun).toBeGreaterThanOrEqual(before - 1000);
    expect(nextRun).toBeLessThanOrEqual(Date.now() + 1000);
    expect(deps.nudgeScheduler).toHaveBeenCalledTimes(1);
  });

  it('persists structured GitHub watch metadata for host-driven watchers', async () => {
    await processTaskIpc(
      {
        type: 'schedule_task',
        prompt: `
[BACKGROUND CI WATCH]

Watch target:
GitHub Actions run 654321

Check instructions:
Managed by host-driven watcher.
        `.trim(),
        schedule_type: 'interval',
        schedule_value: '15000',
        ci_provider: 'github',
        ci_metadata: JSON.stringify({
          repo: 'owner/repo',
          run_id: 654321,
        }),
        targetJid: 'other@g.us',
      },
      'whatsapp_main',
      true,
      deps,
    );

    const tasks = getAllTasks();
    expect(tasks).toHaveLength(1);
    expect(tasks[0].ci_provider).toBe('github');
    expect(tasks[0].ci_metadata).toContain('owner/repo');
    expect(tasks[0].max_duration_ms).toBe(DEFAULT_WATCH_CI_MAX_DURATION_MS);
  });

  it('does not assign a max duration to regular scheduled tasks', async () => {
    await processTaskIpc(
      {
        type: 'schedule_task',
        prompt: 'regular interval task',
        schedule_type: 'interval',
        schedule_value: '60000',
        targetJid: 'other@g.us',
      },
      'whatsapp_main',
      true,
      deps,
    );

    const tasks = getAllTasks();
    expect(tasks).toHaveLength(1);
    expect(tasks[0].max_duration_ms).toBeNull();
  });

  it('rejects invalid interval (non-numeric)', async () => {
    await processTaskIpc(
      {
        type: 'schedule_task',
        prompt: 'bad interval',
        schedule_type: 'interval',
        schedule_value: 'abc',
        targetJid: 'other@g.us',
      },
      'whatsapp_main',
      true,
      deps,
    );

    expect(getAllTasks()).toHaveLength(0);
  });

  it('rejects invalid interval (zero)', async () => {
    await processTaskIpc(
      {
        type: 'schedule_task',
        prompt: 'zero interval',
        schedule_type: 'interval',
        schedule_value: '0',
        targetJid: 'other@g.us',
      },
      'whatsapp_main',
      true,
      deps,
    );

    expect(getAllTasks()).toHaveLength(0);
  });

  it('rejects invalid once timestamp', async () => {
    await processTaskIpc(
      {
        type: 'schedule_task',
        prompt: 'bad once',
        schedule_type: 'once',
        schedule_value: 'not-a-date',
        targetJid: 'other@g.us',
      },
      'whatsapp_main',
      true,
      deps,
    );

    expect(getAllTasks()).toHaveLength(0);
  });
});

// --- context_mode defaulting ---

describe('schedule_task context_mode', () => {
  it('accepts context_mode=group', async () => {
    await processTaskIpc(
      {
        type: 'schedule_task',
        prompt: 'group context',
        schedule_type: 'once',
        schedule_value: '2025-06-01T00:00:00',
        context_mode: 'group',
        targetJid: 'other@g.us',
      },
      'whatsapp_main',
      true,
      deps,
    );

    const tasks = getAllTasks();
    expect(tasks[0].context_mode).toBe('group');
  });

  it('accepts context_mode=isolated', async () => {
    await processTaskIpc(
      {
        type: 'schedule_task',
        prompt: 'isolated context',
        schedule_type: 'once',
        schedule_value: '2025-06-01T00:00:00',
        context_mode: 'isolated',
        targetJid: 'other@g.us',
      },
      'whatsapp_main',
      true,
      deps,
    );

    const tasks = getAllTasks();
    expect(tasks[0].context_mode).toBe('isolated');
  });

  it('defaults invalid context_mode to isolated', async () => {
    await processTaskIpc(
      {
        type: 'schedule_task',
        prompt: 'bad context',
        schedule_type: 'once',
        schedule_value: '2025-06-01T00:00:00',
        context_mode: 'bogus' as any,
        targetJid: 'other@g.us',
      },
      'whatsapp_main',
      true,
      deps,
    );

    const tasks = getAllTasks();
    expect(tasks[0].context_mode).toBe('isolated');
  });

  it('defaults missing context_mode to isolated', async () => {
    await processTaskIpc(
      {
        type: 'schedule_task',
        prompt: 'no context mode',
        schedule_type: 'once',
        schedule_value: '2025-06-01T00:00:00',
        targetJid: 'other@g.us',
      },
      'whatsapp_main',
      true,
      deps,
    );

    const tasks = getAllTasks();
    expect(tasks[0].context_mode).toBe('isolated');
  });
});

// --- assign_room success path ---

describe('assign_room success', () => {
  it('main group can assign a new room', async () => {
    await processTaskIpc(
      {
        type: 'assign_room',
        jid: 'new@g.us',
        name: 'New Group',
        folder: 'new-group',
      },
      'whatsapp_main',
      true,
      deps,
    );

    const group = getRegisteredGroup('new@g.us');
    expect(group).toBeDefined();
    expect(group!.name).toBe('New Group');
    expect(group!.folder).toBe('new-group');
  });

  it('assign_room persists trigger metadata through IPC', async () => {
    await processTaskIpc(
      {
        type: 'assign_room',
        jid: 'triggered@g.us',
        name: 'Triggered Group',
        folder: 'triggered-group',
        trigger: '@Repro',
        requiresTrigger: true,
      },
      'whatsapp_main',
      true,
      deps,
    );

    expect(getStoredRoomSettings('triggered@g.us')).toMatchObject({
      trigger: '@Repro',
      requiresTrigger: true,
    });
    expect(getRegisteredGroup('triggered@g.us')).toMatchObject({
      trigger: '@Repro',
      requiresTrigger: true,
    });
  });

  it('assign_room auto-fills missing folder for single rooms without exposing trigger metadata', async () => {
    await processTaskIpc(
      {
        type: 'assign_room',
        jid: 'partial@g.us',
        name: 'Partial',
      },
      'whatsapp_main',
      true,
      deps,
    );

    const group = getRegisteredGroup('partial@g.us');
    expect(group).toBeDefined();
    expect(group!.folder).toMatch(/^grp_whatsapp_/);
    expect(getStoredRoomSettings('partial@g.us')?.roomMode).toBe('single');
  });

  it('assign_room preserves an existing tribunal room mode when room_mode is omitted', async () => {
    assignRoom('legacy-tribunal@g.us', {
      name: 'Legacy Tribunal',
      roomMode: 'tribunal',
      ownerAgentType: 'codex',
      folder: 'legacy-tribunal',
    });

    await processTaskIpc(
      {
        type: 'assign_room',
        jid: 'legacy-tribunal@g.us',
        name: 'Legacy Tribunal Renamed',
        folder: 'legacy-tribunal',
      },
      'whatsapp_main',
      true,
      deps,
    );

    expect(getStoredRoomSettings('legacy-tribunal@g.us')).toMatchObject({
      chatJid: 'legacy-tribunal@g.us',
      roomMode: 'tribunal',
      modeSource: 'explicit',
      name: 'Legacy Tribunal Renamed',
    });
    expect(
      getRegisteredAgentTypesForJid('legacy-tribunal@g.us').sort(),
    ).toEqual(['claude-code', 'codex']);
  });

  it('main group can assign a tribunal room and materialize capability rows', async () => {
    await processTaskIpc(
      {
        type: 'assign_room',
        jid: 'tribunal@g.us',
        name: 'Tribunal Room',
        room_mode: 'tribunal',
        owner_agent_type: 'codex',
      },
      'whatsapp_main',
      true,
      deps,
    );

    const stored = getStoredRoomSettings('tribunal@g.us');
    expect(stored).toMatchObject({
      chatJid: 'tribunal@g.us',
      roomMode: 'tribunal',
      modeSource: 'explicit',
      name: 'Tribunal Room',
      ownerAgentType: 'codex',
    });
    expect(stored?.folder).toMatch(/^grp_whatsapp_/);
    expect(getRegisteredAgentTypesForJid('tribunal@g.us').sort()).toEqual([
      'claude-code',
      'codex',
    ]);
  });

  it('main group can assign per-room reviewer and arbiter agent types', async () => {
    await processTaskIpc(
      {
        type: 'assign_room',
        jid: 'tribunal-role-overrides@g.us',
        name: 'Tribunal Role Overrides',
        room_mode: 'tribunal',
        owner_agent_type: 'claude-code',
        reviewer_agent_type: 'codex',
        arbiter_agent_type: 'claude-code',
      },
      'whatsapp_main',
      true,
      deps,
    );

    expect(getStoredRoomSettings('tribunal-role-overrides@g.us')).toMatchObject(
      {
        chatJid: 'tribunal-role-overrides@g.us',
        roomMode: 'tribunal',
        modeSource: 'explicit',
        name: 'Tribunal Role Overrides',
        ownerAgentType: 'claude-code',
      },
    );
    expect(
      getRegisteredAgentTypesForJid('tribunal-role-overrides@g.us').sort(),
    ).toEqual(['claude-code', 'codex']);
    expect(
      getRegisteredGroup('tribunal-role-overrides@g.us', 'codex'),
    ).toMatchObject({
      agentType: 'codex',
    });
  });

  it('assign_room rejects request with missing fields', async () => {
    await processTaskIpc(
      {
        type: 'assign_room',
        jid: 'broken@g.us',
      },
      'whatsapp_main',
      true,
      deps,
    );

    expect(getRegisteredGroup('broken@g.us')).toBeUndefined();
  });
});
