import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { CronScheduler } from './cron.js';
import type { CronJobConfig } from '../config/schema.js';
import type { InboundMessage } from '../channels/types.js';

function makeCronJob(overrides?: Partial<CronJobConfig>): CronJobConfig {
  return {
    name: 'test-job',
    schedule: '0 9 * * *',
    message: 'Hello from cron',
    output: { channel: 'telegram', peer: '123' },
    enabled: true,
    ...overrides,
  };
}

describe('CronScheduler', () => {
  let scheduler: CronScheduler;
  let mockChannelRegistry: { get: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    mockChannelRegistry = {
      get: vi.fn(),
    };
  });

  afterEach(async () => {
    if (scheduler) {
      await scheduler.disconnect();
    }
  });

  it('implements ChannelAdapter interface', () => {
    scheduler = new CronScheduler([], mockChannelRegistry as any);
    expect(scheduler.name).toBe('cron');
    expect(scheduler.status).toBe('disconnected');
  });

  it('status changes to connected after connect()', async () => {
    scheduler = new CronScheduler([], mockChannelRegistry as any);
    await scheduler.connect();
    expect(scheduler.status).toBe('connected');
  });

  it('status changes to disconnected after disconnect()', async () => {
    scheduler = new CronScheduler([], mockChannelRegistry as any);
    await scheduler.connect();
    await scheduler.disconnect();
    expect(scheduler.status).toBe('disconnected');
  });

  it('skips disabled jobs', async () => {
    const jobs = [makeCronJob({ enabled: false })];
    scheduler = new CronScheduler(jobs, mockChannelRegistry as any);

    const messages: InboundMessage[] = [];
    scheduler.onMessage((msg: InboundMessage) => messages.push(msg));

    await scheduler.connect();
    // Disabled job should not fire
    expect(messages).toHaveLength(0);
  });

  it('fires a message when triggerJob is called', async () => {
    const jobs = [makeCronJob()];
    scheduler = new CronScheduler(jobs, mockChannelRegistry as any);

    const messages: InboundMessage[] = [];
    scheduler.onMessage((msg: InboundMessage) => messages.push(msg));
    await scheduler.connect();

    // Manually trigger (simulates cron firing)
    scheduler.triggerJob('test-job');

    expect(messages).toHaveLength(1);
    expect(messages[0].channel).toBe('cron');
    expect(messages[0].senderId).toBe('test-job');
    expect(messages[0].text).toBe('Hello from cron');
  });

  it('forwards response to output channel on send()', async () => {
    const mockOutputAdapter = {
      send: vi.fn().mockResolvedValue(undefined),
    };
    mockChannelRegistry.get.mockReturnValue(mockOutputAdapter);

    const jobs = [makeCronJob()];
    scheduler = new CronScheduler(jobs, mockChannelRegistry as any);
    await scheduler.connect();

    await scheduler.send('test-job', { text: 'Agent response' });

    expect(mockChannelRegistry.get).toHaveBeenCalledWith('telegram');
    expect(mockOutputAdapter.send).toHaveBeenCalledWith('123', { text: 'Agent response' });
  });

  it('logs warning when output channel not found', async () => {
    mockChannelRegistry.get.mockReturnValue(undefined);
    const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

    const jobs = [makeCronJob()];
    scheduler = new CronScheduler(jobs, mockChannelRegistry as any);
    await scheduler.connect();

    await scheduler.send('test-job', { text: 'Agent response' });

    expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Output channel'));
    warnSpy.mockRestore();
  });

  it('logs warning when job name not found in send()', async () => {
    const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

    const jobs = [makeCronJob()];
    scheduler = new CronScheduler(jobs, mockChannelRegistry as any);
    await scheduler.connect();

    await scheduler.send('nonexistent-job', { text: 'response' });

    expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('No cron job'));
    warnSpy.mockRestore();
  });

  it('triggerJob includes model_tier in metadata when configured', () => {
    const jobs = [makeCronJob({ model_tier: 'fast' })];
    scheduler = new CronScheduler(jobs, mockChannelRegistry as any);

    const messages: InboundMessage[] = [];
    scheduler.onMessage((msg: InboundMessage) => messages.push(msg));

    scheduler.triggerJob('test-job');

    expect(messages).toHaveLength(1);
    expect(messages[0].metadata?.modelTier).toBe('fast');
  });

  it('triggerJob metadata.modelTier is undefined when not configured', () => {
    const jobs = [makeCronJob()];
    scheduler = new CronScheduler(jobs, mockChannelRegistry as any);

    const messages: InboundMessage[] = [];
    scheduler.onMessage((msg: InboundMessage) => messages.push(msg));

    scheduler.triggerJob('test-job');

    expect(messages).toHaveLength(1);
    expect(messages[0].metadata?.modelTier).toBeUndefined();
  });

  it('lists registered job names', () => {
    const jobs = [
      makeCronJob({ name: 'job-a' }),
      makeCronJob({ name: 'job-b', enabled: false }),
    ];
    scheduler = new CronScheduler(jobs, mockChannelRegistry as any);

    const names = scheduler.getJobNames();
    expect(names).toEqual(['job-a', 'job-b']);
  });
});
