/**
 * Reports P2 — integration tests for report_runs + report_schedules CRUD.
 *
 * Covers:
 *  - createReportRun rejects template id from another port
 *  - createReportRun rejects when config.kind ≠ outer kind
 *  - listReportRuns filters by kind / status / templateId
 *  - createReportSchedule computes nextRunAt deterministically per cadence
 *  - updateReportSchedule recomputes nextRunAt on cadence change but NOT
 *    on a no-op edit
 *  - deleteReportSchedule leaves linked report_runs with a NULL schedule_id
 *    (ON DELETE SET NULL contract)
 */

import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { and, eq } from 'drizzle-orm';

import { db } from '@/lib/db';
import { reportRuns, reportSchedules, reportTemplates } from '@/lib/db/schema/reports';
import { user } from '@/lib/db/schema/users';

let makePort: typeof import('../helpers/factories').makePort;
let makeAuditMeta: typeof import('../helpers/factories').makeAuditMeta;
let runsSvc: typeof import('@/lib/services/report-runs.service');
let schedulesSvc: typeof import('@/lib/services/report-schedules.service');
let TEST_USER_ID = '';

beforeAll(async () => {
  const factories = await import('../helpers/factories');
  makePort = factories.makePort;
  makeAuditMeta = factories.makeAuditMeta;
  runsSvc = await import('@/lib/services/report-runs.service');
  schedulesSvc = await import('@/lib/services/report-schedules.service');
  // Schedules + run-triggered-by-user FK against the real user table; pull
  // the first seeded row so test inserts don't trip 23503.
  const [u] = await db.select({ id: user.id }).from(user).limit(1);
  if (!u) throw new Error('No user available; run pnpm db:seed first');
  TEST_USER_ID = u.id;
});

function testMeta(portId: string) {
  return makeAuditMeta({ portId, userId: TEST_USER_ID });
}

async function makeTemplate(portId: string, kind = 'dashboard') {
  const [row] = await db
    .insert(reportTemplates)
    .values({
      portId,
      kind,
      name: `T-${crypto.randomUUID().slice(0, 8)}`,
      config: { kind },
      createdBy: TEST_USER_ID,
    })
    .returning();
  return row!;
}

describe('report-runs.service', () => {
  it('rejects a templateId from a different port', async () => {
    const portA = await makePort();
    const portB = await makePort();
    const tmplB = await makeTemplate(portB.id);
    await expect(
      runsSvc.createReportRun(
        {
          kind: 'dashboard',
          templateId: tmplB.id,
          config: { kind: 'dashboard' },
          outputFormat: 'pdf',
        },
        {
          portId: portA.id,
          triggeredBy: 'user',
          triggeredByUserId: TEST_USER_ID,
          meta: testMeta(portA.id),
        },
      ),
    ).rejects.toThrow(/report template/i);
  });

  it('rejects when config.kind does not match outer kind', async () => {
    const port = await makePort();
    await expect(
      runsSvc.createReportRun(
        { kind: 'clients', config: { kind: 'dashboard' }, outputFormat: 'pdf' },
        {
          portId: port.id,
          triggeredBy: 'user',
          triggeredByUserId: TEST_USER_ID,
          meta: testMeta(port.id),
        },
      ),
    ).rejects.toThrow(/config\.kind must equal/);
  });

  it('filters listReportRuns by kind + status', async () => {
    const port = await makePort();
    await runsSvc.createReportRun(
      { kind: 'dashboard', config: { kind: 'dashboard' }, outputFormat: 'pdf' },
      {
        portId: port.id,
        triggeredBy: 'user',
        triggeredByUserId: TEST_USER_ID,
        meta: testMeta(port.id),
      },
    );
    await runsSvc.createReportRun(
      { kind: 'clients', config: { kind: 'clients' }, outputFormat: 'csv' },
      {
        portId: port.id,
        triggeredBy: 'user',
        triggeredByUserId: TEST_USER_ID,
        meta: testMeta(port.id),
      },
    );

    const dashboardOnly = await runsSvc.listReportRuns(port.id, {
      kind: 'dashboard',
      page: 1,
      pageSize: 20,
    });
    expect(dashboardOnly.data.every((r) => r.kind === 'dashboard')).toBe(true);
    expect(dashboardOnly.total).toBe(1);

    const allPending = await runsSvc.listReportRuns(port.id, {
      status: 'pending',
      page: 1,
      pageSize: 20,
    });
    expect(allPending.total).toBe(2);
  });
});

describe('report-schedules.service', () => {
  it('nextRunFor weekly_monday_9 always lands on a Monday strictly after now', () => {
    const cases = [
      new Date('2026-05-25T10:00:00Z'), // Monday after target hour
      new Date('2026-05-27T08:00:00Z'), // Wednesday before
      new Date('2026-05-31T23:59:00Z'), // Sunday late
    ];
    for (const now of cases) {
      const out = schedulesSvc.nextRunFor('weekly_monday_9', now);
      expect(out.getUTCDay()).toBe(1);
      expect(out.getUTCHours()).toBe(9);
      expect(out.getTime()).toBeGreaterThan(now.getTime());
    }
  });

  it('nextRunFor monthly_first_9 lands on the 1st of the next month after target hour', () => {
    const now = new Date('2026-05-25T10:00:00Z');
    const out = schedulesSvc.nextRunFor('monthly_first_9', now);
    expect(out.getUTCDate()).toBe(1);
    expect(out.getUTCMonth()).toBe(5); // June
    expect(out.getUTCHours()).toBe(9);
  });

  it('nextRunFor quarterly_first_9 picks the next quarter start', () => {
    const now = new Date('2026-05-25T10:00:00Z');
    const out = schedulesSvc.nextRunFor('quarterly_first_9', now);
    expect(out.getUTCDate()).toBe(1);
    expect(out.getUTCMonth()).toBe(6); // July
    expect(out.getUTCHours()).toBe(9);
  });

  it('createReportSchedule computes nextRunAt + persists', async () => {
    const port = await makePort();
    const tmpl = await makeTemplate(port.id);
    const row = await schedulesSvc.createReportSchedule(
      {
        templateId: tmpl.id,
        cadence: 'weekly_monday_9',
        recipients: [{ email: 'ops@example.com' }],
        outputFormat: 'pdf',
        enabled: true,
      },
      { portId: port.id, meta: testMeta(port.id) },
    );
    expect(row.nextRunAt.getUTCDay()).toBe(1);
    expect(row.recipients).toHaveLength(1);
    expect(row.enabled).toBe(true);
  });

  it('updateReportSchedule recomputes nextRunAt only on cadence change OR re-enable', async () => {
    const port = await makePort();
    const tmpl = await makeTemplate(port.id);
    const row = await schedulesSvc.createReportSchedule(
      {
        templateId: tmpl.id,
        cadence: 'weekly_monday_9',
        recipients: [{ email: 'ops@example.com' }],
        outputFormat: 'pdf',
        enabled: true,
      },
      { portId: port.id, meta: testMeta(port.id) },
    );
    const originalNext = row.nextRunAt.getTime();

    // No-op edit (recipient change) — nextRunAt stays.
    const after = await schedulesSvc.updateReportSchedule(
      row.id,
      { recipients: [{ email: 'sales@example.com' }] },
      { portId: port.id, meta: testMeta(port.id) },
    );
    expect(after.nextRunAt.getTime()).toBe(originalNext);

    // Cadence change — nextRunAt advances to next monthly first.
    const afterCadence = await schedulesSvc.updateReportSchedule(
      row.id,
      { cadence: 'monthly_first_9' },
      { portId: port.id, meta: testMeta(port.id) },
    );
    expect(afterCadence.nextRunAt.getUTCDate()).toBe(1);
  });

  it('deleteReportSchedule sets schedule_id NULL on linked runs (no orphans)', async () => {
    const port = await makePort();
    const tmpl = await makeTemplate(port.id);
    const sched = await schedulesSvc.createReportSchedule(
      {
        templateId: tmpl.id,
        cadence: 'weekly_monday_9',
        recipients: [{ email: 'ops@example.com' }],
        outputFormat: 'pdf',
        enabled: true,
      },
      { portId: port.id, meta: testMeta(port.id) },
    );

    const run = await runsSvc.createReportRun(
      {
        kind: 'dashboard',
        templateId: tmpl.id,
        config: { kind: 'dashboard' },
        outputFormat: 'pdf',
      },
      {
        portId: port.id,
        triggeredBy: 'schedule',
        scheduleId: sched.id,
        meta: testMeta(port.id),
      },
    );

    await schedulesSvc.deleteReportSchedule(sched.id, port.id, testMeta(port.id));

    const after = await db.query.reportRuns.findFirst({
      where: and(eq(reportRuns.id, run.id), eq(reportRuns.portId, port.id)),
    });
    expect(after?.scheduleId).toBeNull();
  });
});

afterAll(async () => {
  // No global cleanup; per-test ports keep rows isolated.
  void reportRuns;
  void reportSchedules;
});
