import { BadRequestException } from '@nestjs/common';
import { AuditEventsService } from './audit-events.service';
import { AuthenticatedUser } from '../auth/interfaces/jwt-payload.interface';

const makeModel = () => {
  const model = {
    findOne: jest.fn(),
    create: jest.fn(),
    bulkWrite: jest.fn(),
    find: jest.fn(),
    countDocuments: jest.fn(),
    aggregate: jest.fn(),
    deleteMany: jest.fn(),
  };

  return model;
};

const makeRollupModel = () => ({
  updateOne: jest.fn(),
  find: jest.fn(),
  countDocuments: jest.fn(),
  deleteMany: jest.fn(),
});

const makeAhaProgressModel = () => ({
  findOne: jest.fn().mockReturnValue({
    lean: jest.fn().mockReturnValue({
      exec: jest.fn().mockResolvedValue(null),
    }),
  }),
  find: jest.fn(),
  updateOne: jest.fn(),
  aggregate: jest.fn(),
  countDocuments: jest.fn(),
});

const makeAhaRuleModel = () => ({
  findOne: jest.fn().mockReturnValue({
    lean: jest.fn().mockReturnValue({
      exec: jest.fn().mockResolvedValue(null),
    }),
  }),
  find: jest.fn().mockReturnValue({
    sort: jest.fn().mockReturnValue({
      lean: jest.fn().mockReturnValue({
        exec: jest.fn().mockResolvedValue([]),
      }),
    }),
  }),
  updateMany: jest.fn(),
  create: jest.fn(),
});

const makeService = (
  model: ReturnType<typeof makeModel>,
  rollupModel = makeRollupModel(),
  ahaProgressModel = makeAhaProgressModel(),
  ahaRuleModel = makeAhaRuleModel(),
) =>
  new AuditEventsService(
    model as any,
    rollupModel as any,
    ahaProgressModel as any,
    ahaRuleModel as any,
  );

const makeEvent = (
  eventName: string,
  timestamp: string,
  overrides: Partial<Record<string, any>> = {},
) => ({
  event_id: overrides.event_id ?? `${eventName}-${timestamp}`,
  event_name: eventName,
  event_version: 1,
  timestamp,
  source: 'web' as const,
  actor_id: 'advisor-uuid',
  actor_role: 'advisor' as const,
  session_id: overrides.session_id ?? 'session-1',
  correlation_id: overrides.correlation_id ?? 'flow-1',
  metadata: overrides.metadata ?? {},
});

const activeRule = (overrides: Record<string, any> = {}) => ({
  version: overrides.version ?? 'v1',
  label: overrides.label ?? 'Tracking V1',
  description: overrides.description,
  is_active: true,
  key_events: overrides.key_events ?? [
    {
      key: 'first_import',
      label: 'Primeira importacao',
      event_names: ['import.openfinance.succeeded', 'import.csv.succeeded'],
      min_count: 1,
      required: true,
    },
  ],
  allowed_key_screens: overrides.allowed_key_screens ?? [
    'screen.client.loaded',
  ],
  min_distinct_key_screens: overrides.min_distinct_key_screens ?? 1,
  min_return_days: overrides.min_return_days ?? 1,
  created_by: overrides.created_by ?? 'ops@example.com',
});

const authUser: AuthenticatedUser = {
  id: 'jwt-subject',
  type: 'user',
  payload: {
    iss: 'users',
    iat: 1,
    exp: 9999999999,
    nbf: 1,
    sub: 'jwt-subject',
    jti: 'jwt-id',
  },
  expiresAt: new Date('2999-01-01T00:00:00.000Z'),
  issuedAt: new Date('2025-01-01T00:00:00.000Z'),
  tokenData: {
    uuid: 'advisor-uuid',
    name: 'Advisor Name',
    email: 'advisor@example.com',
    plan: 'pro',
  },
  rawToken: 'token',
};

describe('AuditEventsService', () => {
  it('returns zero counts for an empty batch without writing', async () => {
    const model = makeModel();
    const service = makeService(model);

    await expect(service.createBatch([], authUser)).resolves.toEqual({
      inserted: 0,
      duplicates: 0,
    });

    expect(model.bulkWrite).not.toHaveBeenCalled();
  });

  it('lists persisted rollups with pagination', async () => {
    const model = makeModel();
    const rollupModel = makeRollupModel();
    rollupModel.find.mockReturnValue({
      sort: jest.fn().mockReturnValue({
        skip: jest.fn().mockReturnValue({
          limit: jest.fn().mockReturnValue({
            lean: jest.fn().mockReturnValue({
              exec: jest.fn().mockResolvedValue([{ rollup_key: 'rollup-1' }]),
            }),
          }),
        }),
      }),
    });
    rollupModel.countDocuments = jest.fn().mockReturnValue({
      exec: jest.fn().mockResolvedValue(1),
    });

    const result = await makeService(model, rollupModel).findRollups({
      actor_id: 'advisor-1',
      limit: 20,
      offset: 40,
    });

    expect(rollupModel.find).toHaveBeenCalledWith({ actor_id: 'advisor-1' });
    expect(result).toEqual({
      rollups: [{ rollup_key: 'rollup-1' }],
      total: 1,
      limit: 20,
      offset: 40,
    });
  });

  it('returns no rollup preview for error events', async () => {
    const model = makeModel();

    const result = await makeService(model).previewRollups({
      event_name: 'ui.crash',
    });

    expect(result).toEqual({
      raw_count: 0,
      group_count: 0,
      duplicate_count: 0,
      estimated_bytes: 0,
      groups: [],
    });
    expect(model.aggregate).not.toHaveBeenCalled();
  });

  it('bulk inserts sanitized events and derives actor from JWT user', async () => {
    const model = makeModel();
    model.bulkWrite.mockResolvedValue({ insertedCount: 1 });
    const ahaProgressModel = makeAhaProgressModel();

    const service = makeService(model, makeRollupModel(), ahaProgressModel);

    const result = await service.createBatch(
      [
        {
          event_id: 'event-1',
          event_name: 'api.request.failed',
          event_version: 1,
          timestamp: '2026-04-28T12:00:00.000Z',
          source: 'web',
          actor_id: 'spoofed',
          actor_role: 'client',
          session_id: 'session-1',
          correlation_id: 'flow-1',
          metadata: {
            status_code: 500,
            endpoint: '/clients',
            email: 'private@example.com',
          },
        },
      ],
      authUser,
    );

    expect(result).toEqual({ inserted: 1, duplicates: 0 });
    expect(model.findOne).not.toHaveBeenCalled();
    expect(model.create).not.toHaveBeenCalled();
    expect(model.bulkWrite).toHaveBeenCalledWith(
      [
        {
          insertOne: {
            document: expect.objectContaining({
              actor_id: 'advisor-uuid',
              actor_role: 'advisor',
              metadata: {
                status_code: 500,
                endpoint: '/clients',
              },
            }),
          },
        },
      ],
      { ordered: false },
    );
    expect(ahaProgressModel.findOne).not.toHaveBeenCalled();
  });

  it('accepts API failure events with an unknown actor without updating AHA progress', async () => {
    const model = makeModel();
    model.bulkWrite.mockResolvedValue({ insertedCount: 1 });
    const ahaProgressModel = makeAhaProgressModel();

    const service = makeService(model, makeRollupModel(), ahaProgressModel);

    await expect(
      service.createBatch([
        {
          event_id: 'event-unknown-api-failure',
          event_name: 'api.request.failed',
          event_version: 1,
          timestamp: '2026-04-28T12:00:00.000Z',
          source: 'api',
          actor_id: 'unknown',
          actor_role: 'unknown',
          session_id: 'request-1',
          correlation_id: 'correlation-1',
          metadata: {
            status_code: 500,
            endpoint: '/v1/public-route',
            exception_trace: '#0 /app/Controller.php(10): boom',
          },
        },
      ]),
    ).resolves.toEqual({ inserted: 1, duplicates: 0 });

    expect(model.bulkWrite).toHaveBeenCalledWith(
      [
        {
          insertOne: {
            document: expect.objectContaining({
              actor_id: 'unknown',
              actor_role: 'unknown',
              source: 'api',
              correlation_id: 'correlation-1',
              metadata: expect.objectContaining({
                exception_trace: '#0 /app/Controller.php(10): boom',
              }),
            }),
          },
        },
      ],
      { ordered: false },
    );
    expect(ahaProgressModel.findOne).not.toHaveBeenCalled();
  });

  it('rejects unknown actor outside api.request.failed from api source', async () => {
    const model = makeModel();
    const service = makeService(model);

    await expect(
      service.createBatch([
        {
          event_id: 'event-unknown-ui-crash',
          event_name: 'ui.crash',
          event_version: 1,
          timestamp: '2026-04-28T12:00:00.000Z',
          source: 'web',
          actor_id: 'unknown',
          actor_role: 'unknown',
          session_id: 'session-1',
          correlation_id: 'correlation-1',
          metadata: {
            message: 'Unexpected UI crash',
          },
        },
      ]),
    ).rejects.toThrow(
      'actor_role "unknown" is allowed only for api.request.failed events from api source',
    );

    expect(model.bulkWrite).not.toHaveBeenCalled();
  });

  it('persists advisor AHA progress by active rule version when milestones arrive', async () => {
    const model = makeModel();
    model.bulkWrite.mockResolvedValue({ insertedCount: 3 });
    const ahaProgressModel = makeAhaProgressModel();
    const ahaRuleModel = makeAhaRuleModel();
    ahaRuleModel.findOne.mockReturnValue({
      lean: jest.fn().mockReturnValue({
        exec: jest.fn().mockResolvedValue(
          activeRule({
            version: 'v2',
            allowed_key_screens: ['screen.client.loaded'],
          }),
        ),
      }),
    });
    ahaProgressModel.findOne
      .mockReturnValueOnce({
        lean: jest.fn().mockReturnValue({
          exec: jest.fn().mockResolvedValue(null),
        }),
      })
      .mockReturnValueOnce({
        lean: jest.fn().mockReturnValue({
          exec: jest.fn().mockResolvedValue({
            actor_id: 'advisor-uuid',
            actor_role: 'advisor',
            rule_version: 'v2',
            journey_started_at: new Date('2026-04-28T10:00:00.000Z'),
            key_events_seen: ['first_import'],
            key_events_counts: { first_import: 1 },
            key_events_completed: ['first_import'],
            key_events_completed_at: {
              first_import: new Date('2026-04-28T10:00:00.000Z'),
            },
            required_key_events_completed_at: new Date(
              '2026-04-28T10:00:00.000Z',
            ),
            last_event_at: new Date('2026-04-28T10:00:00.000Z'),
          }),
        }),
      })
      .mockReturnValueOnce({
        lean: jest.fn().mockReturnValue({
          exec: jest.fn().mockResolvedValue({
            actor_id: 'advisor-uuid',
            actor_role: 'advisor',
            rule_version: 'v2',
            journey_started_at: new Date('2026-04-28T10:00:00.000Z'),
            key_events_seen: ['first_import'],
            key_events_counts: { first_import: 1 },
            key_events_completed: ['first_import'],
            key_events_completed_at: {
              first_import: new Date('2026-04-28T10:00:00.000Z'),
            },
            required_key_events_completed_at: new Date(
              '2026-04-28T10:00:00.000Z',
            ),
            key_screens_seen: ['screen.client.loaded'],
            key_screens_seen_count: 1,
            key_screens_completed_at: new Date('2026-04-28T11:00:00.000Z'),
            last_event_at: new Date('2026-04-28T11:00:00.000Z'),
          }),
        }),
      });

    await makeService(
      model,
      makeRollupModel(),
      ahaProgressModel,
      ahaRuleModel,
    ).createBatch(
      [
        makeEvent('import.csv.succeeded', '2026-04-28T10:00:00.000Z'),
        makeEvent('screen.client.loaded', '2026-04-28T11:00:00.000Z'),
        makeEvent('user.returned_d1', '2026-04-29T12:00:00.000Z', {
          session_id: 'session-2',
          correlation_id: 'flow-2',
        }),
      ],
      authUser,
    );

    expect(ahaProgressModel.updateOne).toHaveBeenNthCalledWith(
      1,
      {
        actor_id: 'advisor-uuid',
        actor_role: 'advisor',
        rule_version: 'v2',
      },
      {
        $set: expect.objectContaining({
          journey_started_at: new Date('2026-04-28T10:00:00.000Z'),
          key_events_seen: ['first_import'],
          key_events_counts: { first_import: 1 },
          key_events_completed: ['first_import'],
          key_events_completed_at: {
            first_import: new Date('2026-04-28T10:00:00.000Z'),
          },
          required_key_events_completed_at: new Date(
            '2026-04-28T10:00:00.000Z',
          ),
          key_screens_seen: [],
          key_screens_seen_count: 0,
          last_event_at: new Date('2026-04-28T10:00:00.000Z'),
        }),
        $setOnInsert: {
          actor_id: 'advisor-uuid',
          actor_role: 'advisor',
          rule_version: 'v2',
        },
      },
      { upsert: true },
    );
    expect(ahaProgressModel.updateOne).toHaveBeenNthCalledWith(
      3,
      {
        actor_id: 'advisor-uuid',
        actor_role: 'advisor',
        rule_version: 'v2',
      },
      {
        $set: expect.objectContaining({
          return_completed_at: new Date('2026-04-29T12:00:00.000Z'),
          aha_reached_at: new Date('2026-04-29T12:00:00.000Z'),
          last_event_at: new Date('2026-04-29T12:00:00.000Z'),
        }),
        $setOnInsert: {
          actor_id: 'advisor-uuid',
          actor_role: 'advisor',
          rule_version: 'v2',
        },
      },
      { upsert: true },
    );
  });

  it('does not overwrite previously persisted milestone timestamps', async () => {
    const model = makeModel();
    model.bulkWrite.mockResolvedValue({ insertedCount: 1 });
    const ahaProgressModel = makeAhaProgressModel();
    const ahaRuleModel = makeAhaRuleModel();
    ahaRuleModel.findOne.mockReturnValue({
      lean: jest.fn().mockReturnValue({
        exec: jest.fn().mockResolvedValue(activeRule()),
      }),
    });
    ahaProgressModel.findOne.mockReturnValue({
      lean: jest.fn().mockReturnValue({
        exec: jest.fn().mockResolvedValue({
          actor_id: 'advisor-uuid',
          actor_role: 'advisor',
          rule_version: 'v1',
          journey_started_at: new Date('2026-04-28T10:00:00.000Z'),
          key_events_seen: ['first_import'],
          key_events_counts: { first_import: 1 },
          key_events_completed: ['first_import'],
          key_events_completed_at: {
            first_import: new Date('2026-04-28T10:00:00.000Z'),
          },
          required_key_events_completed_at: new Date(
            '2026-04-28T10:00:00.000Z',
          ),
          last_event_at: new Date('2026-04-28T10:00:00.000Z'),
        }),
      }),
    });

    await makeService(
      model,
      makeRollupModel(),
      ahaProgressModel,
      ahaRuleModel,
    ).createBatch(
      [
        makeEvent('import.csv.succeeded', '2026-04-29T15:00:00.000Z', {
          event_id: 'event-2',
          session_id: 'session-2',
          correlation_id: 'flow-2',
        }),
      ],
      authUser,
    );

    expect(ahaProgressModel.updateOne).toHaveBeenCalledWith(
      {
        actor_id: 'advisor-uuid',
        actor_role: 'advisor',
        rule_version: 'v1',
      },
      {
        $set: expect.objectContaining({
          key_events_counts: { first_import: 2 },
          required_key_events_completed_at: new Date(
            '2026-04-28T10:00:00.000Z',
          ),
          last_event_at: new Date('2026-04-29T15:00:00.000Z'),
        }),
        $setOnInsert: {
          actor_id: 'advisor-uuid',
          actor_role: 'advisor',
          rule_version: 'v1',
        },
      },
      { upsert: true },
    );
  });

  it('completes key screens only after the configured number of distinct screens', async () => {
    const model = makeModel();
    model.bulkWrite.mockResolvedValue({ insertedCount: 3 });
    const ahaProgressModel = makeAhaProgressModel();
    const ahaRuleModel = makeAhaRuleModel();

    ahaRuleModel.findOne.mockReturnValue({
      lean: jest.fn().mockReturnValue({
        exec: jest.fn().mockResolvedValue(
          activeRule({
            min_distinct_key_screens: 2,
            allowed_key_screens: [
              'screen.client.loaded',
              'screen.investments.loaded',
            ],
          }),
        ),
      }),
    });
    ahaProgressModel.findOne
      .mockReturnValueOnce({
        lean: jest.fn().mockReturnValue({
          exec: jest.fn().mockResolvedValue(null),
        }),
      })
      .mockReturnValueOnce({
        lean: jest.fn().mockReturnValue({
          exec: jest.fn().mockResolvedValue({
            actor_id: 'advisor-uuid',
            actor_role: 'advisor',
            rule_version: 'v1',
            journey_started_at: new Date('2026-04-30T12:00:00.000Z'),
            key_events_seen: [],
            key_events_counts: {},
            key_events_completed: [],
            key_events_completed_at: {},
            key_screens_seen: ['screen.client.loaded'],
            key_screens_seen_count: 1,
            last_event_at: new Date('2026-04-30T12:00:00.000Z'),
          }),
        }),
      })
      .mockReturnValueOnce({
        lean: jest.fn().mockReturnValue({
          exec: jest.fn().mockResolvedValue({
            actor_id: 'advisor-uuid',
            actor_role: 'advisor',
            rule_version: 'v1',
            journey_started_at: new Date('2026-04-30T12:00:00.000Z'),
            key_events_seen: [],
            key_events_counts: {},
            key_events_completed: [],
            key_events_completed_at: {},
            key_screens_seen: ['screen.client.loaded'],
            key_screens_seen_count: 1,
            last_event_at: new Date('2026-04-30T12:05:00.000Z'),
          }),
        }),
      });

    await makeService(
      model,
      makeRollupModel(),
      ahaProgressModel,
      ahaRuleModel,
    ).createBatch(
      [
        makeEvent('screen.client.loaded', '2026-04-30T12:00:00.000Z'),
        makeEvent('screen.client.loaded', '2026-04-30T12:05:00.000Z', {
          event_id: 'event-2',
        }),
        makeEvent('screen.investments.loaded', '2026-04-30T12:10:00.000Z', {
          event_id: 'event-3',
        }),
      ],
      authUser,
    );

    expect(ahaProgressModel.updateOne).toHaveBeenLastCalledWith(
      {
        actor_id: 'advisor-uuid',
        actor_role: 'advisor',
        rule_version: 'v1',
      },
      {
        $set: expect.objectContaining({
          key_screens_seen: [
            'screen.client.loaded',
            'screen.investments.loaded',
          ],
          key_screens_seen_count: 2,
          key_screens_completed_at: new Date('2026-04-30T12:10:00.000Z'),
        }),
        $setOnInsert: {
          actor_id: 'advisor-uuid',
          actor_role: 'advisor',
          rule_version: 'v1',
        },
      },
      { upsert: true },
    );
  });

  it('marks return only when the rule requires at least N days since journey start', async () => {
    const model = makeModel();
    model.bulkWrite.mockResolvedValue({ insertedCount: 3 });
    const ahaProgressModel = makeAhaProgressModel();
    const ahaRuleModel = makeAhaRuleModel();

    ahaRuleModel.findOne.mockReturnValue({
      lean: jest.fn().mockReturnValue({
        exec: jest.fn().mockResolvedValue(activeRule({ min_return_days: 3 })),
      }),
    });
    ahaProgressModel.findOne
      .mockReturnValueOnce({
        lean: jest.fn().mockReturnValue({
          exec: jest.fn().mockResolvedValue(null),
        }),
      })
      .mockReturnValueOnce({
        lean: jest.fn().mockReturnValue({
          exec: jest.fn().mockResolvedValue({
            actor_id: 'advisor-uuid',
            actor_role: 'advisor',
            rule_version: 'v1',
            journey_started_at: new Date('2026-04-01T09:00:00.000Z'),
            key_events_seen: ['first_import'],
            key_events_counts: { first_import: 1 },
            key_events_completed: ['first_import'],
            key_events_completed_at: {
              first_import: new Date('2026-04-01T09:00:00.000Z'),
            },
            required_key_events_completed_at: new Date(
              '2026-04-01T09:00:00.000Z',
            ),
            last_event_at: new Date('2026-04-01T09:00:00.000Z'),
          }),
        }),
      })
      .mockReturnValueOnce({
        lean: jest.fn().mockReturnValue({
          exec: jest.fn().mockResolvedValue({
            actor_id: 'advisor-uuid',
            actor_role: 'advisor',
            rule_version: 'v1',
            journey_started_at: new Date('2026-04-01T09:00:00.000Z'),
            key_events_seen: ['first_import'],
            key_events_counts: { first_import: 1 },
            key_events_completed: ['first_import'],
            key_events_completed_at: {
              first_import: new Date('2026-04-01T09:00:00.000Z'),
            },
            required_key_events_completed_at: new Date(
              '2026-04-01T09:00:00.000Z',
            ),
            last_event_at: new Date('2026-04-03T09:00:00.000Z'),
          }),
        }),
      });

    await makeService(
      model,
      makeRollupModel(),
      ahaProgressModel,
      ahaRuleModel,
    ).createBatch(
      [
        makeEvent('import.openfinance.succeeded', '2026-04-01T09:00:00.000Z'),
        makeEvent('user.returned_d1', '2026-04-03T09:00:00.000Z', {
          event_id: 'event-2',
        }),
        makeEvent('user.returned_d1', '2026-04-04T09:00:00.000Z', {
          event_id: 'event-3',
        }),
      ],
      authUser,
    );

    expect(ahaProgressModel.updateOne).toHaveBeenLastCalledWith(
      {
        actor_id: 'advisor-uuid',
        actor_role: 'advisor',
        rule_version: 'v1',
      },
      {
        $set: expect.objectContaining({
          return_completed_at: new Date('2026-04-04T09:00:00.000Z'),
        }),
        $setOnInsert: {
          actor_id: 'advisor-uuid',
          actor_role: 'advisor',
          rule_version: 'v1',
        },
      },
      { upsert: true },
    );
  });

  it('infers return from any later advisor event for cross-device sessions', async () => {
    const model = makeModel();
    model.bulkWrite.mockResolvedValue({ insertedCount: 1 });
    const ahaProgressModel = makeAhaProgressModel();
    const ahaRuleModel = makeAhaRuleModel();

    ahaRuleModel.findOne.mockReturnValue({
      lean: jest.fn().mockReturnValue({
        exec: jest.fn().mockResolvedValue(activeRule({ min_return_days: 1 })),
      }),
    });
    ahaProgressModel.findOne.mockReturnValue({
      lean: jest.fn().mockReturnValue({
        exec: jest.fn().mockResolvedValue({
          actor_id: 'advisor-uuid',
          actor_role: 'advisor',
          rule_version: 'v1',
          journey_started_at: new Date('2026-04-01T09:00:00.000Z'),
          key_events_seen: ['first_import'],
          key_events_counts: { first_import: 1 },
          key_events_completed: ['first_import'],
          key_events_completed_at: {
            first_import: new Date('2026-04-01T09:00:00.000Z'),
          },
          required_key_events_completed_at: new Date(
            '2026-04-01T09:00:00.000Z',
          ),
          key_screens_seen: ['screen.client.loaded'],
          key_screens_seen_count: 1,
          key_screens_completed_at: new Date('2026-04-01T09:05:00.000Z'),
          last_event_at: new Date('2026-04-01T09:05:00.000Z'),
        }),
      }),
    });

    await makeService(
      model,
      makeRollupModel(),
      ahaProgressModel,
      ahaRuleModel,
    ).createBatch(
      [
        makeEvent('session.started', '2026-04-02T09:30:00.000Z', {
          session_id: 'other-device-session',
        }),
      ],
      authUser,
    );

    expect(ahaProgressModel.updateOne).toHaveBeenCalledWith(
      {
        actor_id: 'advisor-uuid',
        actor_role: 'advisor',
        rule_version: 'v1',
      },
      {
        $set: expect.objectContaining({
          return_completed_at: new Date('2026-04-02T09:30:00.000Z'),
          aha_reached_at: new Date('2026-04-02T09:30:00.000Z'),
          last_event_at: new Date('2026-04-02T09:30:00.000Z'),
        }),
        $setOnInsert: {
          actor_id: 'advisor-uuid',
          actor_role: 'advisor',
          rule_version: 'v1',
        },
      },
      { upsert: true },
    );
  });

  it('does not persist advisor PII from JWT or event payload', async () => {
    const model = makeModel();
    model.bulkWrite.mockResolvedValue({ insertedCount: 1 });

    const service = makeService(model);
    const baseEvent = {
      event_id: 'event-1',
      event_name: 'auth.login',
      event_version: 1,
      timestamp: '2026-04-28T12:00:00.000Z',
      source: 'web' as const,
      actor_id: 'spoofed',
      actor_role: 'advisor' as const,
      session_id: 'session-1',
      correlation_id: 'flow-1',
      metadata: {},
    };

    await service.createBatch([baseEvent], authUser);
    expect(
      model.bulkWrite.mock.calls[0][0][0].insertOne.document,
    ).not.toHaveProperty('advisor');

    await service.createBatch(
      [
        {
          ...baseEvent,
          event_id: 'event-2',
          advisor: {
            name: 'Payload Advisor',
            email: 'payload@example.com',
            plan: 'payload-plan',
          },
        },
      ],
      {
        ...authUser,
        tokenData: {
          subject: '48',
        },
      },
    );

    expect(
      model.bulkWrite.mock.calls[1][0][0].insertOne.document,
    ).not.toHaveProperty('advisor');
  });

  it('keeps the web actor uuid when the JWT only has a numeric subject', async () => {
    const model = makeModel();
    model.bulkWrite.mockResolvedValue({ insertedCount: 1 });

    const service = makeService(model);

    await service.createBatch(
      [
        {
          event_id: 'event-1',
          event_name: 'auth.login',
          event_version: 1,
          timestamp: '2026-04-28T12:00:00.000Z',
          source: 'web',
          actor_id: 'advisor-uuid-from-web',
          actor_role: 'advisor',
          session_id: 'session-1',
          correlation_id: 'flow-1',
          metadata: {},
        },
      ],
      {
        ...authUser,
        id: '48',
        tokenData: {
          subject: '48',
        },
      },
    );

    expect(model.bulkWrite).toHaveBeenCalledWith(
      [
        {
          insertOne: {
            document: expect.objectContaining({
              actor_id: 'advisor-uuid-from-web',
              actor_role: 'advisor',
            }),
          },
        },
      ],
      { ordered: false },
    );
    expect(
      model.bulkWrite.mock.calls[0][0][0].insertOne.document,
    ).not.toHaveProperty('advisor');
  });

  it('counts duplicate key errors from unordered bulk inserts', async () => {
    const model = makeModel();
    const duplicateError = Object.assign(new Error('duplicate key'), {
      writeErrors: [{ code: 11000 }],
      result: { insertedCount: 1 },
    });
    model.bulkWrite.mockRejectedValue(duplicateError);

    const service = makeService(model);

    await expect(
      service.createBatch(
        [
          {
            event_id: 'event-1',
            event_name: 'auth.login',
            event_version: 1,
            timestamp: '2026-04-28T12:00:00.000Z',
            source: 'web',
            actor_id: 'advisor-uuid',
            actor_role: 'advisor',
            session_id: 'session-1',
            correlation_id: 'flow-1',
            metadata: {},
          },
          {
            event_id: 'event-2',
            event_name: 'auth.login',
            event_version: 1,
            timestamp: '2026-04-28T12:00:00.000Z',
            source: 'web',
            actor_id: 'advisor-uuid',
            actor_role: 'advisor',
            session_id: 'session-2',
            correlation_id: 'flow-2',
            metadata: {},
          },
        ],
        authUser,
      ),
    ).resolves.toEqual({ inserted: 1, duplicates: 1 });

    expect(model.findOne).not.toHaveBeenCalled();
    expect(model.create).not.toHaveBeenCalled();
  });

  it('rethrows non-duplicate bulk write errors', async () => {
    const model = makeModel();
    const writeError = Object.assign(new Error('validation failed'), {
      writeErrors: [{ code: 121 }],
      result: { insertedCount: 0 },
    });
    model.bulkWrite.mockRejectedValue(writeError);

    const service = makeService(model);

    await expect(
      service.createBatch(
        [
          {
            event_id: 'event-1',
            event_name: 'auth.login',
            event_version: 1,
            timestamp: '2026-04-28T12:00:00.000Z',
            source: 'web',
            actor_id: 'advisor-uuid',
            actor_role: 'advisor',
            session_id: 'session-1',
            correlation_id: 'flow-1',
            metadata: {},
          },
        ],
        authUser,
      ),
    ).rejects.toBe(writeError);
  });

  it('rethrows mixed duplicate and non-duplicate bulk write errors', async () => {
    const model = makeModel();
    const writeError = Object.assign(new Error('mixed write errors'), {
      writeErrors: [{ code: 11000 }, { code: 121 }],
      result: { insertedCount: 1 },
    });
    model.bulkWrite.mockRejectedValue(writeError);

    const service = makeService(model);

    await expect(
      service.createBatch(
        [
          {
            event_id: 'event-1',
            event_name: 'auth.login',
            event_version: 1,
            timestamp: '2026-04-28T12:00:00.000Z',
            source: 'web',
            actor_id: 'advisor-uuid',
            actor_role: 'advisor',
            session_id: 'session-1',
            correlation_id: 'flow-1',
            metadata: {},
          },
          {
            event_id: 'event-2',
            event_name: 'auth.login',
            event_version: 1,
            timestamp: '2026-04-28T12:00:00.000Z',
            source: 'web',
            actor_id: 'advisor-uuid',
            actor_role: 'advisor',
            session_id: 'session-2',
            correlation_id: 'flow-2',
            metadata: {},
          },
        ],
        authUser,
      ),
    ).rejects.toBe(writeError);
  });

  it('rejects non-error metadata over the max payload size after whitelist filtering', async () => {
    const model = makeModel();
    const service = makeService(model);

    await expect(
      service.createBatch(
        [
          {
            event_id: 'event-1',
            event_name: 'screen.client.loaded',
            event_version: 1,
            timestamp: '2026-04-28T12:00:00.000Z',
            source: 'web',
            actor_id: 'advisor-uuid',
            actor_role: 'advisor',
            session_id: 'session-1',
            correlation_id: 'flow-1',
            metadata: {
              route: 'x'.repeat(11_000),
            },
          },
        ],
        authUser,
      ),
    ).rejects.toBeInstanceOf(BadRequestException);
  });

  it('allows larger response bodies for error events', async () => {
    const model = makeModel();
    model.bulkWrite.mockResolvedValue({ insertedCount: 1 });
    const service = makeService(model);

    await expect(
      service.createBatch(
        [
          {
            event_id: 'event-1',
            event_name: 'api.request.failed',
            event_version: 1,
            timestamp: '2026-04-28T12:00:00.000Z',
            source: 'web',
            actor_id: 'advisor-uuid',
            actor_role: 'advisor',
            session_id: 'session-1',
            correlation_id: 'flow-1',
            metadata: {
              status_code: 500,
              response_body_preview: 'x'.repeat(11_000),
            },
          },
        ],
        authUser,
      ),
    ).resolves.toEqual({ inserted: 1, duplicates: 0 });
  });

  it('aggregates AHA metrics by advisor actor and rule version', async () => {
    const model = makeModel();
    const ahaProgressModel = makeAhaProgressModel();
    const ahaRuleModel = makeAhaRuleModel();
    ahaRuleModel.findOne.mockReturnValue({
      lean: jest.fn().mockReturnValue({
        exec: jest.fn().mockResolvedValue(activeRule({ version: 'v2' })),
      }),
    });
    ahaProgressModel.aggregate.mockResolvedValue([
      {
        _id: null,
        actors_started: 2,
        key_events_completed: 2,
        key_screens_completed: 1,
        return_completed: 1,
        aha_reached: 1,
      },
    ]);

    const service = makeService(
      model,
      makeRollupModel(),
      ahaProgressModel,
      ahaRuleModel,
    );

    await expect(service.getAhaMetrics({})).resolves.toMatchObject({
      rule_version: 'v2',
      period: {},
      totals: {
        actors_started: 2,
        key_events_completed: 2,
        key_screens_completed: 1,
        return_completed: 1,
        aha_reached: 1,
      },
    });
    expect(ahaProgressModel.aggregate).toHaveBeenCalledWith([
      expect.objectContaining({
        $match: {
          actor_role: 'advisor',
          rule_version: 'v2',
        },
      }),
      expect.any(Object),
      expect.any(Object),
    ]);
  });

  it('treats missing AHA milestone fields as incomplete in metrics aggregation', async () => {
    const ahaProgressModel = makeAhaProgressModel();
    const ahaRuleModel = makeAhaRuleModel();
    ahaRuleModel.findOne.mockReturnValue({
      lean: jest.fn().mockReturnValue({
        exec: jest.fn().mockResolvedValue(activeRule({ version: 'v1' })),
      }),
    });
    ahaProgressModel.aggregate.mockResolvedValue([
      {
        _id: null,
        actors_started: 1,
        key_events_completed: 0,
        key_screens_completed: 1,
        return_completed: 0,
        aha_reached: 0,
      },
    ]);

    await makeService(
      makeModel(),
      makeRollupModel(),
      ahaProgressModel,
      ahaRuleModel,
    ).getAhaMetrics({ actor_id: 'advisor-1' });

    const pipeline = ahaProgressModel.aggregate.mock.calls[0][0];
    expect(pipeline[1].$project.aha_reached.$cond[0]).toEqual({
      $ne: [{ $ifNull: ['$aha_reached_at', null] }, null],
    });
    expect(pipeline[1].$project.key_events_completed.$cond[0]).toEqual({
      $ne: [{ $ifNull: ['$required_key_events_completed_at', null] }, null],
    });
    expect(pipeline[1].$project.return_completed.$cond[0]).toEqual({
      $ne: [{ $ifNull: ['$return_completed_at', null] }, null],
    });
  });

  it('lists only AHA progress records that reached the moment', async () => {
    const ahaProgressModel = makeAhaProgressModel();
    ahaProgressModel.find.mockReturnValue({
      sort: jest.fn().mockReturnValue({
        skip: jest.fn().mockReturnValue({
          limit: jest.fn().mockReturnValue({
            lean: jest.fn().mockReturnValue({
              exec: jest.fn().mockResolvedValue([
                {
                  actor_id: 'advisor-1',
                  rule_version: 'v2',
                  aha_reached_at: new Date('2026-04-30T12:00:00.000Z'),
                },
              ]),
            }),
          }),
        }),
      }),
    });
    ahaProgressModel.countDocuments.mockReturnValue({
      exec: jest.fn().mockResolvedValue(1),
    });

    const service = makeService(
      makeModel(),
      makeRollupModel(),
      ahaProgressModel,
    );

    await expect(
      service.listAhaProgress({
        actor_role: 'advisor',
        rule_version: 'v2',
        aha_reached: true,
        limit: 20,
        offset: 40,
      } as any),
    ).resolves.toMatchObject({
      total: 1,
      limit: 20,
      offset: 40,
      items: [expect.objectContaining({ actor_id: 'advisor-1' })],
    });

    const reachedFilter = {
      actor_role: 'advisor',
      rule_version: 'v2',
      aha_reached_at: { $exists: true, $ne: null },
    };
    expect(ahaProgressModel.find).toHaveBeenCalledWith(reachedFilter);
    expect(ahaProgressModel.countDocuments).toHaveBeenCalledWith(reachedFilter);
  });

  it('filters timeline by client uuid and AHA metrics by actor id', async () => {
    const model = makeModel();
    const ahaProgressModel = makeAhaProgressModel();
    model.find.mockReturnValue({
      sort: jest.fn().mockReturnThis(),
      skip: jest.fn().mockReturnThis(),
      limit: jest.fn().mockReturnThis(),
      lean: jest.fn().mockReturnThis(),
      exec: jest.fn().mockResolvedValue([]),
    });
    model.countDocuments.mockReturnValue({
      exec: jest.fn().mockResolvedValue(0),
    });
    const ahaRuleModel = makeAhaRuleModel();
    ahaRuleModel.findOne.mockReturnValue({
      lean: jest.fn().mockReturnValue({
        exec: jest.fn().mockResolvedValue(activeRule()),
      }),
    });
    ahaProgressModel.aggregate.mockResolvedValue([
      {
        _id: null,
        actors_started: 0,
        key_events_completed: 0,
        key_screens_completed: 0,
        return_completed: 0,
        aha_reached: 0,
      },
    ]);

    const service = makeService(
      model,
      makeRollupModel(),
      ahaProgressModel,
      ahaRuleModel,
    );

    await service.findAll({
      actor_id: 'advisor-uuid',
      client_uuid: 'client-uuid',
    });
    await service.getAhaMetrics({
      actor_id: 'advisor-uuid',
    });

    expect(model.find).toHaveBeenCalledWith(
      expect.objectContaining({
        actor_id: 'advisor-uuid',
        'metadata.client_uuid': 'client-uuid',
      }),
    );
    expect(ahaProgressModel.aggregate).toHaveBeenCalledWith([
      expect.objectContaining({
        $match: expect.objectContaining({
          actor_role: 'advisor',
          actor_id: 'advisor-uuid',
          rule_version: 'v1',
        }),
      }),
      expect.any(Object),
      expect.any(Object),
    ]);
  });

  it('creates a new active rule version and deactivates the previous one', async () => {
    const ruleModel = makeAhaRuleModel();
    ruleModel.updateMany.mockResolvedValue({ modifiedCount: 1 });
    ruleModel.create.mockResolvedValue({});

    const service = makeService(
      makeModel(),
      makeRollupModel(),
      makeAhaProgressModel(),
      ruleModel,
    );

    await service.createAhaRuleVersion(
      {
        version: 'v2',
        label: 'D+3 com 2 telas',
        key_events: [
          {
            key: 'first_import',
            label: 'Primeira importacao',
            event_names: [
              'import.openfinance.succeeded',
              'import.csv.succeeded',
            ],
            min_count: 1,
            required: true,
          },
          {
            key: 'investment_review',
            label: 'Revisao de investimentos',
            event_names: ['investment.reviewed'],
            min_count: 2,
            required: false,
          },
        ],
        allowed_key_screens: [
          'screen.client.loaded',
          'screen.investments.loaded',
        ],
        min_distinct_key_screens: 2,
        min_return_days: 3,
      },
      authUser,
    );

    expect(ruleModel.updateMany).toHaveBeenCalledWith(
      { is_active: true },
      { $set: { is_active: false } },
    );
    expect(ruleModel.create).toHaveBeenCalledWith(
      expect.objectContaining({
        version: 'v2',
        key_events: expect.arrayContaining([
          expect.objectContaining({ key: 'first_import', required: true }),
        ]),
        is_active: true,
        created_by: 'advisor@example.com',
      }),
    );
  });

  it('rejects AHA rules with duplicate key event keys', async () => {
    const service = makeService(
      makeModel(),
      makeRollupModel(),
      makeAhaProgressModel(),
      makeAhaRuleModel(),
    );

    await expect(
      service.createAhaRuleVersion({
        version: 'v2',
        label: 'Duplicated key',
        key_events: [
          {
            key: 'first_import',
            label: 'Import A',
            event_names: ['import.openfinance.succeeded'],
            min_count: 1,
            required: true,
          },
          {
            key: 'first_import',
            label: 'Import B',
            event_names: ['import.csv.succeeded'],
            min_count: 1,
            required: false,
          },
        ],
        allowed_key_screens: ['screen.client.loaded'],
        min_distinct_key_screens: 1,
        min_return_days: 1,
      } as any),
    ).rejects.toThrow(BadRequestException);
  });

  it('rejects AHA rules without a required key event', async () => {
    const service = makeService(
      makeModel(),
      makeRollupModel(),
      makeAhaProgressModel(),
      makeAhaRuleModel(),
    );

    await expect(
      service.createAhaRuleVersion({
        version: 'v2',
        label: 'Optional only',
        key_events: [
          {
            key: 'first_import',
            label: 'Import',
            event_names: ['import.csv.succeeded'],
            min_count: 1,
            required: false,
          },
        ],
        allowed_key_screens: ['screen.client.loaded'],
        min_distinct_key_screens: 1,
        min_return_days: 1,
      } as any),
    ).rejects.toThrow(BadRequestException);
  });

  it('rejects AHA rules with invalid key event key format', async () => {
    const service = makeService(
      makeModel(),
      makeRollupModel(),
      makeAhaProgressModel(),
      makeAhaRuleModel(),
    );

    await expect(
      service.createAhaRuleVersion({
        version: 'v2',
        label: 'Invalid key',
        key_events: [
          {
            key: 'First-Import',
            label: 'Import',
            event_names: ['import.csv.succeeded'],
            min_count: 1,
            required: true,
          },
        ],
        allowed_key_screens: ['screen.client.loaded'],
        min_distinct_key_screens: 1,
        min_return_days: 1,
      } as any),
    ).rejects.toThrow(BadRequestException);
  });

  it('builds a snippet from the active rule', async () => {
    const ahaRuleModel = makeAhaRuleModel();
    ahaRuleModel.findOne.mockReturnValue({
      lean: jest.fn().mockReturnValue({
        exec: jest.fn().mockResolvedValue(
          activeRule({
            version: 'v3',
            allowed_key_screens: [
              'screen.client.loaded',
              'screen.investments.loaded',
            ],
            min_distinct_key_screens: 2,
            min_return_days: 3,
          }),
        ),
      }),
    });

    const service = makeService(
      makeModel(),
      makeRollupModel(),
      makeAhaProgressModel(),
      ahaRuleModel,
    );

    await expect(service.getAhaRuleSnippet()).resolves.toMatchObject({
      rule_version: 'v3',
      snippet: expect.stringContaining('versao: v3'),
    });
    await expect(service.getAhaRuleSnippet()).resolves.toMatchObject({
      snippet: expect.stringContaining('key="first_import"'),
    });
    await expect(service.getAhaRuleSnippet()).resolves.toMatchObject({
      snippet: expect.stringContaining('OBRIGATORIO'),
    });
    await expect(service.getAhaRuleSnippet()).resolves.toMatchObject({
      snippet: expect.stringContaining(
        'Antes de adicionar qualquer chamada de tracking',
      ),
    });
    await expect(service.getAhaRuleSnippet()).resolves.toMatchObject({
      snippet: expect.not.stringContaining('min_import_successes'),
    });
  });

  it('previews rollups grouped by day, event, actor and client', async () => {
    const model = makeModel();
    model.aggregate.mockResolvedValue([
      {
        rollup_key: '2026-04-28|screen.investments.loaded|advisor-1|client-1',
        day: '2026-04-28',
        event_name: 'screen.investments.loaded',
        actor_id: 'advisor-1',
        actor_role: 'advisor',
        source: 'web',
        client_uuid: 'client-1',
        count: 50,
        first_seen_at: new Date('2026-04-28T10:00:00.000Z'),
        last_seen_at: new Date('2026-04-28T18:00:00.000Z'),
        sample_session_ids: ['session-1'],
        sample_correlation_ids: ['flow-1'],
        metadata: { client_uuid: 'client-1' },
      },
    ]);

    const result = await makeService(model).previewRollups({
      event_name: 'screen.investments.loaded',
    });

    expect(result.raw_count).toBe(50);
    expect(result.group_count).toBe(1);
    expect(result.duplicate_count).toBe(49);
    const pipeline = model.aggregate.mock.calls[0][0];
    expect(model.aggregate).toHaveBeenCalledWith([
      expect.objectContaining({
        $match: expect.objectContaining({
          event_name: 'screen.investments.loaded',
        }),
      }),
      expect.any(Object),
      expect.any(Object),
      expect.any(Object),
      expect.any(Object),
      expect.any(Object),
    ]);
    const duplicateOnlyStage = pipeline.find(
      (stage: any) => stage?.$match?.count,
    );
    expect(duplicateOnlyStage).toEqual({
      $match: {
        count: { $gt: 1 },
      },
    });
    const projectStage = pipeline.find((stage: any) => stage?.$project);
    expect(projectStage.$project.rolled_up_until).toBe('$last_seen_at');
    expect(projectStage.$project.rollup_key.$concat).toEqual([
      '$_id.day',
      '|',
      '$_id.event_name',
      '|',
      '$_id.actor_id',
      '|',
      '$_id.actor_role',
      '|',
      '$_id.source',
      '|',
      { $ifNull: ['$_id.client_uuid', '__null__'] },
    ]);
  });

  it('upserts rollup groups without deleting raw events', async () => {
    const model = makeModel();
    const rollupModel = makeRollupModel();
    model.aggregate.mockResolvedValue([
      {
        rollup_key: '2026-04-28|screen.investments.loaded|advisor-1|client-1',
        day: '2026-04-28',
        event_name: 'screen.investments.loaded',
        actor_id: 'advisor-1',
        actor_role: 'advisor',
        source: 'web',
        client_uuid: 'client-1',
        count: 50,
        first_seen_at: new Date('2026-04-28T10:00:00.000Z'),
        last_seen_at: new Date('2026-04-28T18:00:00.000Z'),
        sample_session_ids: ['session-1'],
        sample_correlation_ids: ['flow-1'],
        metadata: { client_uuid: 'client-1' },
      },
    ]);
    rollupModel.updateOne.mockResolvedValue({});

    const result = await makeService(model, rollupModel).runRollups({
      actor_id: 'advisor-1',
    });

    expect(result).toEqual({ upserted_count: 1, raw_count: 50 });
    expect(rollupModel.updateOne).toHaveBeenCalledWith(
      {
        rollup_key: '2026-04-28|screen.investments.loaded|advisor-1|client-1',
      },
      {
        $set: expect.objectContaining({
          count: 50,
          actor_id: 'advisor-1',
        }),
      },
      { upsert: true },
    );
    expect(model.deleteMany).not.toHaveBeenCalled();
  });

  it('prunes raw events with the same filters used by the manual cleanup action', async () => {
    const model = makeModel();
    const rollupModel = makeRollupModel();
    rollupModel.find.mockReturnValue({
      lean: jest.fn().mockReturnThis(),
      exec: jest.fn().mockResolvedValue([
        {
          day: '2026-04-28',
          event_name: 'screen.investments.loaded',
          actor_id: 'advisor-1',
          actor_role: 'advisor',
          source: 'web',
          client_uuid: 'client-1',
          rolled_up_until: new Date('2026-04-28T18:00:00.000Z'),
        },
      ]),
    });
    model.deleteMany.mockReturnValue({
      exec: jest.fn().mockResolvedValue({ deletedCount: 49 }),
    });

    const result = await makeService(model, rollupModel).pruneRolledUpEvents({
      event_name: 'screen.investments.loaded',
      actor_id: 'advisor-1',
      startDate: new Date('2026-04-28T00:00:00.000Z'),
    });

    expect(result).toEqual({ deleted_count: 49, rollup_count: 1 });
    expect(rollupModel.find).toHaveBeenCalledWith(
      expect.objectContaining({
        count: { $gt: 1 },
        event_name: 'screen.investments.loaded',
        actor_id: 'advisor-1',
        day: expect.objectContaining({ $gte: '2026-04-27' }),
      }),
    );
    expect(model.deleteMany).toHaveBeenCalledWith(
      expect.objectContaining({
        event_name: 'screen.investments.loaded',
        actor_id: 'advisor-1',
        actor_role: 'advisor',
        source: 'web',
        'metadata.client_uuid': 'client-1',
        timestamp: expect.objectContaining({
          $gte: new Date('2026-04-28T03:00:00.000Z'),
        }),
      }),
    );
  });

  it('prunes raw events for an explicit empty client uuid', async () => {
    const model = makeModel();
    const rollupModel = makeRollupModel();
    rollupModel.find.mockReturnValue({
      lean: jest.fn().mockReturnThis(),
      exec: jest.fn().mockResolvedValue([
        {
          day: '2026-04-28',
          event_name: 'screen.investments.loaded',
          actor_id: 'advisor-1',
          actor_role: 'advisor',
          source: 'web',
          client_uuid: '',
          rolled_up_until: new Date('2026-04-28T18:00:00.000Z'),
        },
      ]),
    });
    model.deleteMany.mockReturnValue({
      exec: jest.fn().mockResolvedValue({ deletedCount: 1 }),
    });

    const result = await makeService(model, rollupModel).pruneRolledUpEvents({
      event_name: 'screen.investments.loaded',
      actor_id: 'advisor-1',
    });

    expect(result).toEqual({ deleted_count: 1, rollup_count: 1 });
    expect(model.deleteMany).toHaveBeenCalledWith(
      expect.objectContaining({
        event_name: 'screen.investments.loaded',
        actor_id: 'advisor-1',
        'metadata.client_uuid': '',
      }),
    );
  });

  it('does not prune raw events when no persisted rollup matches the filters', async () => {
    const model = makeModel();
    const rollupModel = makeRollupModel();
    rollupModel.find.mockReturnValue({
      lean: jest.fn().mockReturnThis(),
      exec: jest.fn().mockResolvedValue([]),
    });
    model.deleteMany.mockReturnValue({
      exec: jest.fn().mockResolvedValue({ deletedCount: 999 }),
    });

    const result = await makeService(model, rollupModel).pruneRolledUpEvents({
      event_name: 'screen.investments.loaded',
      actor_id: 'advisor-1',
      startDate: new Date('2026-04-28T00:00:00.000Z'),
    });

    expect(result).toEqual({ deleted_count: 0, rollup_count: 0 });
    expect(model.deleteMany).not.toHaveBeenCalled();
  });

  it('does not prune error events', async () => {
    const model = makeModel();
    const rollupModel = makeRollupModel();

    const result = await makeService(model, rollupModel).pruneRolledUpEvents({
      event_name: 'ui.crash',
    });

    expect(result).toEqual({ deleted_count: 0, rollup_count: 0 });
    expect(rollupModel.find).not.toHaveBeenCalled();
    expect(model.deleteMany).not.toHaveBeenCalled();
  });

  it('prunes only events up to the persisted rollup cutoff', async () => {
    const model = makeModel();
    const rollupModel = makeRollupModel();
    const rolledUpUntil = new Date('2026-04-28T18:00:00.000Z');
    rollupModel.find.mockReturnValue({
      lean: jest.fn().mockReturnThis(),
      exec: jest.fn().mockResolvedValue([
        {
          day: '2026-04-28',
          event_name: 'screen.investments.loaded',
          actor_id: 'advisor-1',
          actor_role: 'advisor',
          source: 'web',
          client_uuid: 'client-1',
          rolled_up_until: rolledUpUntil,
        },
      ]),
    });
    model.deleteMany.mockReturnValue({
      exec: jest.fn().mockResolvedValue({ deletedCount: 10 }),
    });

    await makeService(model, rollupModel).pruneRolledUpEvents({
      event_name: 'screen.investments.loaded',
      actor_id: 'advisor-1',
    });

    expect(model.deleteMany).toHaveBeenCalledWith(
      expect.objectContaining({
        timestamp: {
          $gte: new Date('2026-04-28T03:00:00.000Z'),
          $lte: rolledUpUntil,
        },
      }),
    );
  });

  it('deletes persisted rollups by filters', async () => {
    const model = makeModel();
    const rollupModel = makeRollupModel();
    rollupModel.deleteMany = jest.fn().mockReturnValue({
      exec: jest.fn().mockResolvedValue({ deletedCount: 3 }),
    });

    const result = await makeService(model, rollupModel).deleteRollups({
      actor_id: 'advisor-1',
      event_name: 'screen.investments.loaded',
    });

    expect(rollupModel.deleteMany).toHaveBeenCalledWith({
      actor_id: 'advisor-1',
      event_name: 'screen.investments.loaded',
    });
    expect(result).toEqual({ deleted_count: 3 });
  });

  it('deletes selected persisted rollups by rollup key', async () => {
    const model = makeModel();
    const rollupModel = makeRollupModel();
    rollupModel.deleteMany = jest.fn().mockReturnValue({
      exec: jest.fn().mockResolvedValue({ deletedCount: 2 }),
    });

    const result = await makeService(model, rollupModel).deleteSelectedRollups([
      'rollup-1',
      'rollup-2',
    ]);

    expect(rollupModel.deleteMany).toHaveBeenCalledWith({
      rollup_key: { $in: ['rollup-1', 'rollup-2'] },
    });
    expect(result).toEqual({ deleted_count: 2 });
  });

  it('deletes raw audit events by filters and refuses an empty filter', async () => {
    const model = makeModel();
    model.deleteMany = jest.fn().mockReturnValue({
      exec: jest.fn().mockResolvedValue({ deletedCount: 4 }),
    });
    const service = makeService(model);

    await expect(
      service.deleteAuditEvents({
        startDate: new Date('2026-04-27T03:00:00.000Z'),
        endDate: new Date('2026-04-28T02:59:59.999Z'),
        event_name: 'screen.investments.loaded',
        actor_id: 'advisor-1',
      }),
    ).resolves.toEqual({ deleted_count: 4 });

    expect(model.deleteMany).toHaveBeenCalledWith({
      actor_id: 'advisor-1',
      event_name: 'screen.investments.loaded',
      timestamp: {
        $gte: new Date('2026-04-27T03:00:00.000Z'),
        $lte: new Date('2026-04-28T02:59:59.999Z'),
      },
    });
    await expect(service.deleteAuditEvents()).rejects.toThrow(
      BadRequestException,
    );
  });

  it('deletes selected raw audit events by event id', async () => {
    const model = makeModel();
    model.deleteMany = jest.fn().mockReturnValue({
      exec: jest.fn().mockResolvedValue({ deletedCount: 2 }),
    });

    const result = await makeService(model).deleteSelectedAuditEvents([
      'event-1',
      'event-2',
    ]);

    expect(model.deleteMany).toHaveBeenCalledWith({
      event_id: { $in: ['event-1', 'event-2'] },
    });
    expect(result).toEqual({ deleted_count: 2 });
  });
});
