import { afterEach, describe, expect, it, vi } from 'vitest';
import type { BackupConfig } from '../config/schema.js';
import { BackupScheduler } from './scheduler.js';

function makeBackupConfig(overrides: Partial<BackupConfig> = {}): BackupConfig {
  return {
    enabled: true,
    schedule: undefined,
    interval: '1h',
    run_on_start: false,
    local_dir: '/tmp',
    include_vectors: true,
    minio: {
      enabled: false,
      endpoint: undefined,
      access_key: undefined,
      secret_key: undefined,
      bucket: undefined,
      prefix: 'flynn',
      secure: true,
    },
    failure_threshold: 2,
    notify_recovery: true,
    notify: undefined,
    ...overrides,
  };
}

describe('BackupScheduler', () => {
  afterEach(() => {
    vi.useRealTimers();
    vi.restoreAllMocks();
  });

  it('runs on interval and avoids overlapping executions', async () => {
    vi.useFakeTimers();
    let runCount = 0;
    let release: (() => void) | undefined;
    const runSnapshot = vi.fn(async () => {
      runCount += 1;
      await new Promise<void>((resolve) => {
        release = resolve;
      });
      return { archivePath: '/tmp/a.tar.gz', fileName: 'a.tar.gz', uploaded: false };
    });

    const scheduler = new BackupScheduler({
      dataDir: '/data',
      backupConfig: makeBackupConfig({ interval: '1h' }),
      runSnapshot,
    });

    scheduler.start();
    await vi.advanceTimersByTimeAsync(3_600_000);
    await vi.advanceTimersByTimeAsync(10_800_000);
    expect(runCount).toBe(1);

    release?.();
    await vi.runOnlyPendingTimersAsync();
    await vi.advanceTimersByTimeAsync(3_600_000);
    expect(runCount).toBeGreaterThanOrEqual(2);
    scheduler.stop();
  });

  it('notifies after failure threshold and sends recovery notification', async () => {
    vi.useFakeTimers();
    const send = vi.fn<(peerId: string, message: { text: string }) => Promise<void>>(async () => {});
    const channelLookup = {
      get: vi.fn(() => ({ send })),
    };

    let attempts = 0;
    const runSnapshot = vi.fn(async () => {
      attempts += 1;
      if (attempts <= 2) {
        throw new Error('minio upload failed');
      }
      return { archivePath: '/tmp/ok.tar.gz', fileName: 'ok.tar.gz', uploaded: false };
    });

    const scheduler = new BackupScheduler({
      dataDir: '/data',
      backupConfig: makeBackupConfig({
        interval: '1h',
        failure_threshold: 2,
        notify: { channel: 'telegram', peer: '123' },
        notify_recovery: true,
      }),
      runSnapshot,
      channelLookup,
    });

    scheduler.start();
    await vi.advanceTimersByTimeAsync(3_600_000);
    await vi.advanceTimersByTimeAsync(3_600_000);
    expect(send).toHaveBeenCalledTimes(1);
    const failureCall = send.mock.calls.at(0);
    expect(failureCall?.[1].text).toContain('Backup FAILING');

    await vi.advanceTimersByTimeAsync(3_600_000);
    expect(send).toHaveBeenCalledTimes(2);
    const recoveryCall = send.mock.calls.at(1);
    expect(recoveryCall?.[1].text).toContain('Backup RECOVERED');
    scheduler.stop();
  });

  it('runs immediately when run_on_start is enabled', async () => {
    const runSnapshot = vi.fn(async () => {
      return { archivePath: '/tmp/start.tar.gz', fileName: 'start.tar.gz', uploaded: false };
    });

    const scheduler = new BackupScheduler({
      dataDir: '/data',
      backupConfig: makeBackupConfig({ run_on_start: true, interval: '1h' }),
      runSnapshot,
    });

    scheduler.start();
    await Promise.resolve();
    expect(runSnapshot).toHaveBeenCalledTimes(1);
    scheduler.stop();
  });
});
