import { EngagementEventType } from './engagement-event-type.enum';
import { EngagementService } from './engagement.service';

describe('EngagementService', () => {
  const userId = '507f1f77bcf86cd799439011';
  const postId = '507f191e810c19729de860ea';

  const setup = () => {
    const eventModel = {
      create: jest.fn().mockResolvedValue({}),
      aggregate: jest.fn(),
    };
    const preferenceModel = {
      updateOne: jest.fn().mockResolvedValue({ acknowledged: true }),
      deleteOne: jest.fn().mockResolvedValue({ acknowledged: true }),
      find: jest.fn(),
    };
    const postsService = {
      findById: jest.fn().mockResolvedValue({ id: postId }),
    };
    const feedVersionService = {
      bumpUserVersion: jest.fn().mockResolvedValue(2),
    };
    const service = new EngagementService(
      eventModel as any,
      preferenceModel as any,
      postsService as any,
      feedVersionService as any,
    );
    return { service, eventModel, preferenceModel, postsService, feedVersionService };
  };

  it('records a normalized engagement event after checking post visibility', async () => {
    const { service, eventModel, postsService } = setup();

    await expect(
      service.track(userId, {
        postId,
        type: EngagementEventType.WATCH,
        watchTimeMs: 9000,
        progressPercent: 72,
        sessionId: '  session-1  ',
      }),
    ).resolves.toEqual({ accepted: true, postId, type: EngagementEventType.WATCH });

    expect(postsService.findById).toHaveBeenCalledWith(postId, userId);
    expect(eventModel.create).toHaveBeenCalledWith(
      expect.objectContaining({
        userId: expect.objectContaining({}),
        postId: expect.objectContaining({}),
        type: EngagementEventType.WATCH,
        watchTimeMs: 9000,
        progressPercent: 72,
        sessionId: 'session-1',
        occurredAt: expect.any(Date),
      }),
    );
  });

  it('uses safe defaults for optional event measurements', async () => {
    const { service, eventModel } = setup();

    await service.track(userId, { postId, type: EngagementEventType.IMPRESSION });

    expect(eventModel.create).toHaveBeenCalledWith(
      expect.objectContaining({ watchTimeMs: 0, progressPercent: 0, sessionId: '' }),
    );
  });

  it('routes not-interested events through the preference workflow', async () => {
    const { service, eventModel, preferenceModel, feedVersionService } = setup();

    await expect(
      service.track(userId, { postId, type: EngagementEventType.NOT_INTERESTED }),
    ).resolves.toEqual({ postId, notInterested: true });

    expect(preferenceModel.updateOne).toHaveBeenCalledWith(
      expect.objectContaining({ userId: expect.anything(), postId: expect.anything() }),
      { $set: { notInterested: true } },
      { upsert: true },
    );
    expect(eventModel.create).toHaveBeenCalledWith(
      expect.objectContaining({ type: EngagementEventType.NOT_INTERESTED }),
    );
    expect(feedVersionService.bumpUserVersion).toHaveBeenCalledWith(userId);
  });

  it('tracks a batch and reports every accepted item', async () => {
    const { service } = setup();
    const events = [
      { postId, type: EngagementEventType.OPEN },
      { postId, type: EngagementEventType.COMPLETE },
    ];

    await expect(service.trackBatch(userId, events)).resolves.toMatchObject({
      accepted: 2,
      items: [
        { accepted: true, type: EngagementEventType.OPEN },
        { accepted: true, type: EngagementEventType.COMPLETE },
      ],
    });
  });

  it('removes a not-interested preference without requiring post visibility', async () => {
    const { service, preferenceModel, postsService, eventModel, feedVersionService } = setup();

    await expect(service.setNotInterested(userId, postId, false)).resolves.toEqual({
      postId,
      notInterested: false,
    });

    expect(postsService.findById).not.toHaveBeenCalled();
    expect(preferenceModel.deleteOne).toHaveBeenCalledWith(
      expect.objectContaining({ userId: expect.anything(), postId: expect.anything() }),
    );
    expect(eventModel.create).not.toHaveBeenCalled();
    expect(feedVersionService.bumpUserVersion).toHaveBeenCalledWith(userId);
  });

  it('returns stored not-interested post ids as strings', async () => {
    const { service, preferenceModel } = setup();
    const exec = jest.fn().mockResolvedValue([{ postId }]);
    const lean = jest.fn(() => ({ exec }));
    const select = jest.fn(() => ({ lean }));
    preferenceModel.find.mockReturnValue({ select });

    await expect(service.getNotInterestedPostIds(userId)).resolves.toEqual([postId]);
    expect(preferenceModel.find).toHaveBeenCalledWith(
      expect.objectContaining({ userId: expect.anything(), notInterested: true }),
    );
    expect(select).toHaveBeenCalledWith({ postId: 1 });
  });

  it('returns ranking aggregates keyed by post id', async () => {
    const { service, eventModel } = setup();
    const row = { _id: { toString: () => postId }, watchMs: 5000, completions: 1, rewatches: 2, profileOpens: 1 };
    const exec = jest.fn().mockResolvedValue([row]);
    eventModel.aggregate.mockReturnValue({ exec });

    const result = await service.getRankingSignals(userId);

    expect(result).toEqual(new Map([[postId, row]]));
    expect(eventModel.aggregate).toHaveBeenCalledWith(
      expect.arrayContaining([
        expect.objectContaining({ $match: expect.objectContaining({ userId: expect.anything() }) }),
        { $sort: { watchMs: -1 } },
        { $limit: 500 },
      ]),
    );
  });

  it('builds a 30-day engagement summary grouped by type', async () => {
    const { service, eventModel } = setup();
    eventModel.aggregate.mockReturnValue({
      exec: jest.fn().mockResolvedValue([
        { _id: EngagementEventType.WATCH, count: 3, watchTimeMs: 12000 },
        { _id: EngagementEventType.SHARE, count: 1, watchTimeMs: 0 },
      ]),
    });

    await expect(service.getMySummary(userId)).resolves.toEqual({
      periodDays: 30,
      byType: {
        watch: { count: 3, watchTimeMs: 12000 },
        share: { count: 1, watchTimeMs: 0 },
      },
    });
  });
});
