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

import { TeamTaskStallNotifier } from '../../../../../src/main/services/team/stallMonitor/TeamTaskStallNotifier';

import type { TaskStallAlert } from '../../../../../src/main/services/team/stallMonitor/TeamTaskStallTypes';

function createAlert(overrides: Partial<TaskStallAlert> = {}): TaskStallAlert {
  return {
    teamName: 'demo',
    taskId: 'task-a',
    displayId: 'abcd1234',
    subject: 'Task A',
    branch: 'work',
    signal: 'turn_ended_after_touch',
    progressSignal: 'weak_start_only',
    reason: 'Potential work stall after weak start-only task comment.',
    epochKey: 'task-a:work:turn_ended_after_touch:stamp:file:msg:tool',
    owner: 'alice',
    ownerProviderId: 'opencode',
    taskRef: {
      taskId: 'task-a',
      displayId: 'abcd1234',
      teamName: 'demo',
    },
    ...overrides,
  };
}

describe('TeamTaskStallNotifier', () => {
  it('sends OpenCode owner nudges with deterministic message ids', async () => {
    const teamDataService = {
      sendSystemNotificationToLead: vi.fn(async () => undefined),
    };
    const teamProvisioningService = {
      relayOpenCodeMemberInboxMessages: vi.fn(async () => ({
        relayed: 1,
        attempted: 1,
        delivered: 1,
        failed: 0,
        lastDelivery: { delivered: true, accepted: true },
      })),
    };
    const inboxReader = {
      getMessagesFor: vi.fn(async () => []),
    };
    const inboxWriter = {
      sendMessage: vi.fn(async () => ({ deliveredToInbox: true, messageId: 'msg' })),
    };
    const notifier = new TeamTaskStallNotifier(
      teamDataService as never,
      teamProvisioningService as never,
      inboxReader as never,
      inboxWriter as never
    );
    const alert = createAlert();
    const messageId = `task-stall:demo:task-a:${alert.epochKey}`;

    await expect(notifier.notifyOpenCodeOwners('demo', [alert])).resolves.toEqual([alert]);

    expect(inboxWriter.sendMessage).toHaveBeenCalledWith(
      'demo',
      expect.objectContaining({
        member: 'alice',
        from: 'system',
        to: 'alice',
        messageId,
        summary: 'Potential stalled task',
        taskRefs: [alert.taskRef],
        actionMode: 'do',
        source: 'system_notification',
        messageKind: 'task_stall_remediation',
      })
    );
    expect(teamProvisioningService.relayOpenCodeMemberInboxMessages).toHaveBeenCalledWith(
      'demo',
      'alice',
      {
        onlyMessageId: messageId,
        source: 'watchdog',
        deliveryMetadata: {
          replyRecipient: 'user',
          actionMode: 'do',
          taskRefs: [alert.taskRef],
        },
      }
    );
    expect(teamDataService.sendSystemNotificationToLead).not.toHaveBeenCalled();
  });

  it('skips non-OpenCode owners', async () => {
    const notifier = new TeamTaskStallNotifier(
      { sendSystemNotificationToLead: vi.fn(async () => undefined) } as never,
      {
        relayOpenCodeMemberInboxMessages: vi.fn(async () => ({
          lastDelivery: { delivered: true },
        })),
      } as never,
      { getMessagesFor: vi.fn(async () => []) } as never,
      { sendMessage: vi.fn(async () => ({ deliveredToInbox: true, messageId: 'msg' })) } as never
    );

    await expect(
      notifier.notifyOpenCodeOwners('demo', [
        createAlert({ ownerProviderId: 'codex', owner: 'alice' }),
      ])
    ).resolves.toEqual([]);
  });

  it('skips review alerts because task owner is not necessarily the reviewer', async () => {
    const relay = vi.fn(async () => ({ lastDelivery: { delivered: true } }));
    const notifier = new TeamTaskStallNotifier(
      { sendSystemNotificationToLead: vi.fn(async () => undefined) } as never,
      { relayOpenCodeMemberInboxMessages: relay } as never,
      { getMessagesFor: vi.fn(async () => []) } as never,
      { sendMessage: vi.fn(async () => ({ deliveredToInbox: true, messageId: 'msg' })) } as never
    );

    await expect(
      notifier.notifyOpenCodeOwners('demo', [
        createAlert({ branch: 'review', ownerProviderId: 'opencode', owner: 'alice' }),
      ])
    ).resolves.toEqual([]);
    expect(relay).not.toHaveBeenCalled();
  });

  it('returns no remediated alert when OpenCode delivery is rejected', async () => {
    const notifier = new TeamTaskStallNotifier(
      { sendSystemNotificationToLead: vi.fn(async () => undefined) } as never,
      {
        relayOpenCodeMemberInboxMessages: vi.fn(async () => ({
          relayed: 0,
          attempted: 1,
          delivered: 0,
          failed: 1,
          lastDelivery: {
            delivered: false,
            reason: 'opencode_runtime_not_active',
          },
        })),
      } as never,
      { getMessagesFor: vi.fn(async () => []) } as never,
      { sendMessage: vi.fn(async () => ({ deliveredToInbox: true, messageId: 'msg' })) } as never
    );

    await expect(notifier.notifyOpenCodeOwners('demo', [createAlert()])).resolves.toEqual([]);
  });

  it('does not mark queued-behind delivery as remediated even when active ledger exists', async () => {
    const notifier = new TeamTaskStallNotifier(
      { sendSystemNotificationToLead: vi.fn(async () => undefined) } as never,
      {
        relayOpenCodeMemberInboxMessages: vi.fn(async () => ({
          relayed: 0,
          attempted: 1,
          delivered: 0,
          failed: 0,
          lastDelivery: {
            delivered: true,
            accepted: false,
            responsePending: true,
            ledgerRecordId: 'active-ledger-record',
            queuedBehindMessageId: 'msg-active',
            reason: 'opencode_delivery_response_pending',
          },
        })),
      } as never,
      { getMessagesFor: vi.fn(async () => []) } as never,
      { sendMessage: vi.fn(async () => ({ deliveredToInbox: true, messageId: 'msg' })) } as never
    );

    await expect(notifier.notifyOpenCodeOwners('demo', [createAlert()])).resolves.toEqual([]);
  });

  it('marks accepted response-pending delivery as remediated and leaves follow-up to the delivery ledger', async () => {
    const relay = vi.fn(async () => ({
      relayed: 1,
      attempted: 1,
      delivered: 1,
      failed: 0,
      lastDelivery: {
        delivered: true,
        accepted: true,
        responsePending: true,
        ledgerRecordId: 'active-ledger-record',
        reason: 'opencode_delivery_response_pending',
      },
    }));
    const notifier = new TeamTaskStallNotifier(
      { sendSystemNotificationToLead: vi.fn(async () => undefined) } as never,
      { relayOpenCodeMemberInboxMessages: relay } as never,
      { getMessagesFor: vi.fn(async () => []) } as never,
      { sendMessage: vi.fn(async () => ({ deliveredToInbox: true, messageId: 'msg' })) } as never
    );

    const alert = createAlert();
    await expect(notifier.notifyOpenCodeOwners('demo', [alert])).resolves.toEqual([alert]);
    expect(relay).toHaveBeenCalledTimes(1);
  });

  it('does not deliver runtime nudge when inbox write fails', async () => {
    const relay = vi.fn(async () => ({ lastDelivery: { delivered: true } }));
    const notifier = new TeamTaskStallNotifier(
      { sendSystemNotificationToLead: vi.fn(async () => undefined) } as never,
      { relayOpenCodeMemberInboxMessages: relay } as never,
      { getMessagesFor: vi.fn(async () => []) } as never,
      { sendMessage: vi.fn(async () => { throw new Error('disk full'); }) } as never
    );

    await expect(notifier.notifyOpenCodeOwners('demo', [createAlert()])).resolves.toEqual([]);
    expect(relay).not.toHaveBeenCalled();
    expect(vi.mocked(console.warn).mock.calls[0]?.join(' ')).toContain(
      'OpenCode task stall remediation inbox write failed'
    );
    vi.mocked(console.warn).mockClear();
  });

  it('does not write or relay when existing inbox read fails', async () => {
    const relay = vi.fn(async () => ({ lastDelivery: { delivered: true } }));
    const inboxWrite = vi.fn(async () => ({ deliveredToInbox: true, messageId: 'msg' }));
    const notifier = new TeamTaskStallNotifier(
      { sendSystemNotificationToLead: vi.fn(async () => undefined) } as never,
      { relayOpenCodeMemberInboxMessages: relay } as never,
      { getMessagesFor: vi.fn(async () => { throw new Error('read failed'); }) } as never,
      { sendMessage: inboxWrite } as never
    );

    await expect(notifier.notifyOpenCodeOwners('demo', [createAlert()])).resolves.toEqual([]);
    expect(inboxWrite).not.toHaveBeenCalled();
    expect(relay).not.toHaveBeenCalled();
    expect(vi.mocked(console.warn).mock.calls[0]?.join(' ')).toContain(
      'OpenCode task stall remediation inbox write failed'
    );
    vi.mocked(console.warn).mockClear();
  });
});
