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

import type { UsageRow } from './dashboard-usage-rows.js';
import {
  buildWebUsageRowsForSnapshot,
  createCoalescingTrigger,
  editStatusMessageWithRetry,
  formatStatusHeader,
  getDashboardDuplicateCleanupIntervalMs,
  renderUsageTable,
  shouldPurgeDashboardChannelOnStart,
  summarizeWatcherTasks,
} from './unified-dashboard.js';

describe('summarizeWatcherTasks', () => {
  it('counts active and paused watcher tasks only', () => {
    const summary = summarizeWatcherTasks([
      {
        prompt:
          '[BACKGROUND CI WATCH]\n\nWatch target:\nGitHub Actions run 1\n\nCheck instructions:\na',
        status: 'active',
      },
      {
        prompt:
          '[BACKGROUND CI WATCH]\n\nWatch target:\nGitHub Actions run 2\n\nCheck instructions:\nb',
        status: 'paused',
      },
      {
        prompt:
          '[BACKGROUND CI WATCH]\n\nWatch target:\nGitHub Actions run 3\n\nCheck instructions:\nc',
        status: 'completed',
      },
      {
        prompt: 'normal scheduled task',
        status: 'active',
      },
    ]);

    expect(summary).toEqual({
      active: 1,
      paused: 1,
    });
  });
});

describe('formatStatusHeader', () => {
  it('shows active watcher count in the dashboard header', () => {
    expect(
      formatStatusHeader({
        totalActive: 3,
        totalRooms: 8,
        watchers: { active: 2, paused: 0 },
      }),
    ).toBe('**📊 에이전트 상태** — 활성 3 / 8 | 감시 2');
  });

  it('adds paused watcher count only when present', () => {
    expect(
      formatStatusHeader({
        totalActive: 3,
        totalRooms: 8,
        watchers: { active: 2, paused: 1 },
      }),
    ).toBe('**📊 에이전트 상태** — 활성 3 / 8 | 감시 2 | 일시정지 1');
  });
});

describe('shouldPurgeDashboardChannelOnStart', () => {
  it('purges on startup when explicitly requested, even with a stored message id', () => {
    expect(
      shouldPurgeDashboardChannelOnStart({
        purgeOnStart: true,
        storedMessageId: 'status-message-1',
      }),
    ).toBe(true);
  });

  it('does not purge when startup purge is disabled', () => {
    expect(
      shouldPurgeDashboardChannelOnStart({
        purgeOnStart: false,
        storedMessageId: null,
      }),
    ).toBe(false);
  });
});

describe('getDashboardDuplicateCleanupIntervalMs', () => {
  it('polls duplicate cleanup faster than the status update interval', () => {
    expect(getDashboardDuplicateCleanupIntervalMs(10_000)).toBe(2_000);
    expect(getDashboardDuplicateCleanupIntervalMs(1_000)).toBe(1_000);
  });
});

describe('renderUsageTable', () => {
  const claudeRow: UsageRow = {
    name: 'Claude pro',
    h5pct: 50,
    h5reset: '',
    d7pct: 30,
    d7reset: '',
  };
  const codexRow: UsageRow = {
    name: 'Codex',
    h5pct: 40,
    h5reset: '',
    d7pct: 25,
    d7reset: '',
  };

  it('renders Claude before separator before Codex', () => {
    const lines = renderUsageTable([claudeRow], [codexRow]);

    const claudeIdx = lines.findIndex((l) => l.includes('Claude'));
    const sepIdx = lines.findIndex((l) => /^─+$/.test(l));
    const codexIdx = lines.findIndex((l) => l.includes('Codex'));

    expect(claudeIdx).toBeGreaterThan(-1);
    expect(sepIdx).toBeGreaterThan(claudeIdx);
    expect(codexIdx).toBeGreaterThan(sepIdx);
  });

  it('omits separator when only Claude rows exist', () => {
    const lines = renderUsageTable([claudeRow], []);

    expect(lines.some((l) => /^─+$/.test(l))).toBe(false);
    expect(lines.some((l) => l.includes('Claude'))).toBe(true);
  });

  it('omits separator when only Codex rows exist', () => {
    const lines = renderUsageTable([], [codexRow]);

    expect(lines.some((l) => /^─+$/.test(l))).toBe(false);
    expect(lines.some((l) => l.includes('Codex'))).toBe(true);
  });

  it('returns fallback text when both groups are empty', () => {
    const lines = renderUsageTable([], []);
    expect(lines).toEqual(['_조회 불가_']);
  });
});

describe('buildWebUsageRowsForSnapshot', () => {
  it('keeps real Claude and Kimi rows ahead of Codex rows for web snapshots', () => {
    const rows = buildWebUsageRowsForSnapshot({
      serviceAgentType: 'claude-code',
      claudeAccounts: [
        {
          index: 0,
          masked: 'claude-1',
          isActive: true,
          isRateLimited: false,
          usage: {
            five_hour: {
              utilization: 0.2,
              resets_at: '2026-04-26T12:00:00.000Z',
            },
            seven_day: {
              utilization: 0.4,
              resets_at: '2026-04-27T12:00:00.000Z',
            },
          },
        },
        {
          index: 1,
          masked: 'claude-2',
          isActive: false,
          isRateLimited: false,
          usage: {
            five_hour: {
              utilization: 0.3,
              resets_at: '2026-04-26T12:00:00.000Z',
            },
            seven_day: {
              utilization: 0.5,
              resets_at: '2026-04-27T12:00:00.000Z',
            },
          },
        },
      ],
      kimiUsage: {
        fiveHour: { pct: 18, resetTime: '2026-04-26T12:00:00.000Z' },
        weekly: { pct: 29, resetTime: '2026-04-27T12:00:00.000Z' },
      },
      codexRows: [
        {
          name: 'Codex1',
          h5pct: 25,
          h5reset: '1h',
          d7pct: 35,
          d7reset: '2d',
        },
      ],
    });

    const names = rows.map((row) => row.name);
    expect(names[0]).toMatch(/^Claude1/);
    expect(names[1]).toMatch(/^Claude2/);
    expect(names[2]).toMatch(/^Kimi/);
    expect(names[3]).toBe('Codex1');
  });

  it('does not invent Claude or Kimi rows for codex-only services', () => {
    const rows = buildWebUsageRowsForSnapshot({
      serviceAgentType: 'codex',
      claudeAccounts: [
        {
          index: 0,
          masked: 'claude-1',
          isActive: true,
          isRateLimited: false,
          usage: null,
        },
      ],
      kimiUsage: {
        fiveHour: { pct: 18, resetTime: '2026-04-26T12:00:00.000Z' },
        weekly: { pct: 29, resetTime: '2026-04-27T12:00:00.000Z' },
      },
      codexRows: [
        {
          name: 'Codex1',
          h5pct: 25,
          h5reset: '1h',
          d7pct: 35,
          d7reset: '2d',
        },
      ],
    });

    expect(rows.map((row) => row.name)).toEqual(['Codex1']);
  });
});

describe('editStatusMessageWithRetry', () => {
  it('succeeds on the first attempt without sleeping', async () => {
    let calls = 0;
    let slept = 0;
    const ok = await editStatusMessageWithRetry({
      editOnce: async () => {
        calls++;
      },
      maxRetries: 2,
      retryDelayMs: 15_000,
      sleep: async () => {
        slept++;
      },
    });
    expect(ok).toBe(true);
    expect(calls).toBe(1);
    expect(slept).toBe(0);
  });

  it('retries after a failure and succeeds on the second attempt', async () => {
    let calls = 0;
    const delays: number[] = [];
    const ok = await editStatusMessageWithRetry({
      editOnce: async () => {
        calls++;
        if (calls === 1) throw new Error('503');
      },
      maxRetries: 2,
      retryDelayMs: 15_000,
      sleep: async (ms) => {
        delays.push(ms);
      },
    });
    expect(ok).toBe(true);
    expect(calls).toBe(2);
    expect(delays).toEqual([15_000]); // one 15s wait between the two attempts
  });

  it('gives up after maxRetries (3 total attempts) and reports each failure', async () => {
    let calls = 0;
    const attempts: Array<{ attempt: number; willRetry: boolean }> = [];
    let slept = 0;
    const ok = await editStatusMessageWithRetry({
      editOnce: async () => {
        calls++;
        throw new Error('503');
      },
      maxRetries: 2,
      retryDelayMs: 15_000,
      sleep: async () => {
        slept++;
      },
      onAttemptFailed: (attempt, willRetry) =>
        attempts.push({ attempt, willRetry }),
    });
    expect(ok).toBe(false);
    expect(calls).toBe(3); // initial + 2 retries
    expect(slept).toBe(2); // only between attempts, not after the last
    expect(attempts).toEqual([
      { attempt: 1, willRetry: true },
      { attempt: 2, willRetry: true },
      { attempt: 3, willRetry: false },
    ]);
  });

  it('uses 15s spacing via the default sleep only when a retry is needed', async () => {
    // Sanity: with maxRetries 0, a single failure returns false and never sleeps.
    let slept = 0;
    const ok = await editStatusMessageWithRetry({
      editOnce: async () => {
        throw new Error('nope');
      },
      maxRetries: 0,
      retryDelayMs: 15_000,
      sleep: async () => {
        slept++;
      },
    });
    expect(ok).toBe(false);
    expect(slept).toBe(0);
  });
});

describe('createCoalescingTrigger', () => {
  it('coalesces a burst into one run and re-arms after firing', () => {
    let runs = 0;
    let fire: (() => void) | null = null;
    const timer = {
      set: (cb: () => void) => {
        fire = cb;
        return 0 as unknown as ReturnType<typeof setTimeout>;
      },
    };
    const trigger = createCoalescingTrigger(() => runs++, 1500, timer);

    trigger();
    trigger();
    trigger();
    expect(runs).toBe(0); // nothing runs until the debounce window elapses

    fire!();
    expect(runs).toBe(1); // three calls collapsed into a single run

    // A new request after the run schedules and fires again.
    trigger();
    fire!();
    expect(runs).toBe(2);
  });
});
