import { Test, TestingModule } from '@nestjs/testing';
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
import { ServiceCredentialGuard } from '../common/guards/service-credential/service-credential.guard';
import { RegisterScheduledJobBodyDto } from './dto/register-scheduled-job-body.dto';
import { RegisterScheduledJobDto } from './dto/register-scheduled-job.dto';
import { RunIfDueBodyDto } from './dto/run-if-due-body.dto';
import { RunIfDueDto } from './dto/run-if-due.dto';
import { ScheduledJobsController } from './scheduled-jobs.controller';
import { ScheduledJobsService } from './scheduled-jobs.service';

const mockJob: RegisterScheduledJobDto = {
  jobId: 'job-123',
  scheduleType: 'INTERVAL',
  intervalEvery: 5,
  intervalUnit: 'MINUTES',
};

const mockService = {
  register: jest
    .fn()
    .mockResolvedValue({ schedulerId: 'sched-1', degraded: false }),
  unregister: jest.fn().mockResolvedValue(undefined),
  runNow: jest.fn().mockResolvedValue({ id: 'job-run-1', degraded: false }),
  runIfDue: jest.fn().mockResolvedValue({ enqueued: true }),
  reconcile: jest.fn().mockResolvedValue(2),
  dryRun: jest
    .fn()
    .mockResolvedValue({ status: 'SUCCESS', logs: [], durationMs: 5 }),
};

describe('ScheduledJobsController', () => {
  let controller: ScheduledJobsController;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [ScheduledJobsController],
      providers: [
        {
          provide: ScheduledJobsService,
          useValue: mockService,
        },
      ],
    })
      .overrideGuard(ServiceCredentialGuard)
      .useValue({ canActivate: () => true })
      .compile();

    controller = module.get<ScheduledJobsController>(ScheduledJobsController);
    jest.clearAllMocks();
  });

  it('register delegates to service.register with body.job', async () => {
    const result = await controller.register({ job: mockJob });

    expect(mockService.register).toHaveBeenCalledTimes(1);
    expect(mockService.register).toHaveBeenCalledWith(mockJob);
    expect(result).toEqual({ schedulerId: 'sched-1', degraded: false });
  });

  it('unregister delegates to service.unregister with jobId', async () => {
    await controller.unregister({ jobId: 'job-123' });

    expect(mockService.unregister).toHaveBeenCalledTimes(1);
    expect(mockService.unregister).toHaveBeenCalledWith('job-123');
  });

  it('run-now delegates to service.runNow with jobId', async () => {
    const result = await controller.runNow({ jobId: 'job-abc' });

    expect(mockService.runNow).toHaveBeenCalledTimes(1);
    expect(mockService.runNow).toHaveBeenCalledWith('job-abc');
    expect(result).toEqual({ id: 'job-run-1', degraded: false });
  });

  it('run-if-due delegates to service.runIfDue with body.job (including lastRunAt)', async () => {
    const jobWithLastRun: RunIfDueDto = {
      ...mockJob,
      lastRunAt: '2024-01-01T00:00:00.000Z',
    };

    const result = await controller.runIfDue({ job: jobWithLastRun });

    expect(mockService.runIfDue).toHaveBeenCalledTimes(1);
    expect(mockService.runIfDue).toHaveBeenCalledWith(jobWithLastRun);
    expect(result).toEqual({ enqueued: true });
  });

  it('reconcile delegates to service.reconcile with body.jobs array', async () => {
    const jobs: RegisterScheduledJobDto[] = [
      mockJob,
      {
        jobId: 'job-456',
        scheduleType: 'CRON',
        cronExpression: '0 * * * *',
        timezone: 'America/Sao_Paulo',
      },
    ];

    const result = await controller.reconcile({ jobs });

    expect(mockService.reconcile).toHaveBeenCalledTimes(1);
    expect(mockService.reconcile).toHaveBeenCalledWith(jobs);
    expect(result).toBe(2);
  });

  it('run-if-due works without lastRunAt (first-run scenario)', async () => {
    mockService.runIfDue.mockResolvedValueOnce({ enqueued: true });
    const jobNoLastRun: RunIfDueDto = { ...mockJob };

    const result = await controller.runIfDue({ job: jobNoLastRun });

    expect(mockService.runIfDue).toHaveBeenCalledWith(jobNoLastRun);
    expect(result).toEqual({ enqueued: true });
  });

  it('dryRun encaminha o body ao service', async () => {
    const body = { code: 'return 1;', writeMode: 'safe' as const };
    const res = await controller.dryRun(body as never);
    expect(mockService.dryRun).toHaveBeenCalledWith(body);
    expect(res).toMatchObject({ status: 'SUCCESS' });
  });
});

// ---------------------------------------------------------------------------
// Envelope DTO validation (SJ-1): the @ValidateNested job must actually be
// validated. With an inline `{ job: ... }` type the ValidationPipe skipped it.
// ---------------------------------------------------------------------------
describe('RegisterScheduledJobBodyDto validation', () => {
  it('accepts a valid nested job', async () => {
    const dto = plainToInstance(RegisterScheduledJobBodyDto, { job: mockJob });
    const errors = await validate(dto);
    expect(errors).toHaveLength(0);
  });

  it('reports nested errors when the job is invalid', async () => {
    const dto = plainToInstance(RegisterScheduledJobBodyDto, {
      job: { jobId: '', scheduleType: 'NOPE' },
    });
    const errors = await validate(dto);
    expect(errors.length).toBeGreaterThan(0);
    expect(errors[0].property).toBe('job');
    expect(errors[0].children?.length).toBeGreaterThan(0);
  });
});

describe('RunIfDueBodyDto validation', () => {
  it('accepts a valid nested job', async () => {
    const dto = plainToInstance(RunIfDueBodyDto, { job: mockJob });
    const errors = await validate(dto);
    expect(errors).toHaveLength(0);
  });

  it('reports nested errors when the job is invalid', async () => {
    const dto = plainToInstance(RunIfDueBodyDto, {
      job: { jobId: '', scheduleType: 'WAT' },
    });
    const errors = await validate(dto);
    expect(errors.length).toBeGreaterThan(0);
    expect(errors[0].property).toBe('job');
    expect(errors[0].children?.length).toBeGreaterThan(0);
  });
});
