import { Types } from 'mongoose';
import { ModerationStatus } from '../../common/enums/moderation-status.enum';
import { PostType } from '../../common/enums/post-type.enum';
import { PostVisibility } from '../../common/enums/post-visibility.enum';
import { SortOrder } from '../../common/enums/sort-order.enum';
import { PostShareChannel, PostShareTarget } from './dto/share-post.dto';
import { PostSchema } from './schemas/post.schema';
import { PostsService } from './posts.service';

const createService = (options: { shareBaseUrl?: string; publicWebUrl?: string } = {}) => {
  const configService = {
    get: jest.fn((key: string) => {
      if (key === 'shareBaseUrl') {
        return options.shareBaseUrl;
      }
      if (key === 'publicWebUrl') {
        return options.publicWebUrl;
      }
      return undefined;
    }),
  };
  const postsRepository = {
    findMany: jest.fn().mockResolvedValue([]),
    count: jest.fn().mockResolvedValue(0),
    findById: jest.fn(),
    updateById: jest.fn(),
    create: jest.fn(),
    incrementShareCount: jest.fn().mockResolvedValue(undefined),
    incrementViewCount: jest.fn().mockResolvedValue(undefined),
    incrementPlayCount: jest.fn().mockResolvedValue(undefined),
    deleteById: jest.fn().mockResolvedValue(undefined),
    findManyAdmin: jest.fn().mockResolvedValue([]),
    countAdmin: jest.fn().mockResolvedValue(0),
    updateModerationStatus: jest.fn(),
    findManyByIds: jest.fn().mockResolvedValue([]),
  };
  const connection = {
    collection: jest.fn(() => collectionMock),
  };
  const collectionMock = {
    countDocuments: jest.fn().mockResolvedValue(0),
    findOne: jest.fn().mockResolvedValue(null),
    findOneAndUpdate: jest.fn().mockResolvedValue(null),
    insertOne: jest.fn().mockResolvedValue({ insertedId: new Types.ObjectId() }),
    find: jest.fn(() => ({
      sort: jest.fn().mockReturnThis(),
      limit: jest.fn().mockReturnThis(),
      project: jest.fn().mockReturnThis(),
      toArray: jest.fn().mockResolvedValue([]),
    })),
  };
  const usersRepository = {
    findById: jest.fn().mockResolvedValue({
      id: new Types.ObjectId().toString(),
      isDisabled: false,
      toObject: () => ({ _id: new Types.ObjectId(), username: 'viewer', name: 'Viewer' }),
    }),
    findByUsernames: jest.fn().mockResolvedValue([]),
    findMany: jest.fn().mockResolvedValue([]),
    findPrivateUserIds: jest.fn().mockResolvedValue([]),
    incrementPostsCount: jest.fn().mockResolvedValue(undefined),
  };
  const notificationsService = {
    createShareNotification: jest.fn().mockResolvedValue(undefined),
    createMentionNotification: jest.fn().mockResolvedValue(undefined),
  };
  const appCacheService = {
    setIfAbsent: jest.fn().mockResolvedValue(true),
  };
  const storageService = {
    saveFile: jest.fn().mockResolvedValue('https://cdn.example.com/media/file.bin'),
    saveFiles: jest.fn().mockResolvedValue([]),
    deleteFile: jest.fn().mockResolvedValue(undefined),
    deleteContainingDirectory: jest.fn().mockResolvedValue(undefined),
  };
  const imageProcessingService = {
    processForResponsiveDelivery: jest.fn().mockResolvedValue({ variants: [] }),
  };
  const videoProcessingService = {
    optimizeForPlayback: jest.fn().mockImplementation(async (file) => ({
      file,
      hlsFiles: [],
      thumbnail: null,
    })),
  };
  const mediaProbeService = {
    extractDurationSecondsFromBuffer: jest.fn().mockResolvedValue(12),
  };
  const feedVersionService = {
    bumpGlobalVersion: jest.fn().mockResolvedValue(undefined),
  };
  const auditService = {
    logSuperAdminAction: jest.fn().mockResolvedValue(undefined),
  };
  const chatService = {
    createConversation: jest.fn().mockResolvedValue({ id: '507f191e810c19729de860f1' }),
    sendMessage: jest.fn().mockResolvedValue({ id: '507f191e810c19729de860f2' }),
  };
  const followsRepository = {
    findMany: jest.fn().mockResolvedValue([]),
    findFollowingIds: jest.fn().mockResolvedValue([]),
  };
  const blocksRepository = {
    findAnyBetween: jest.fn().mockResolvedValue(null),
    findBlockingOrBlockedIds: jest.fn().mockResolvedValue([]),
  };
  const contentModerationService = {
    analyze: jest.fn().mockReturnValue({ flagged: false, score: 0, reasons: [] }),
  };

  const service = new PostsService(
    connection as any,
    configService as any,
    postsRepository as any,
    usersRepository as any,
    storageService as any,
    imageProcessingService as any,
    videoProcessingService as any,
    mediaProbeService as any,
    feedVersionService as any,
    notificationsService as any,
    auditService as any,
    chatService as any,
    followsRepository as any,
    blocksRepository as any,
    appCacheService as any,
    contentModerationService as any,
  );

  return {
    service,
    postsRepository,
    connection,
    collectionMock,
    usersRepository,
    notificationsService,
    appCacheService,
    storageService,
    imageProcessingService,
    videoProcessingService,
    mediaProbeService,
    feedVersionService,
    auditService,
    chatService,
    followsRepository,
    blocksRepository,
    contentModerationService,
  };
};

describe('PostsService archived profile posts', () => {
  it('returns only archived posts for the owner when visibility=archived', async () => {
    const userId = new Types.ObjectId().toString();
    const { service, postsRepository } = createService();
    postsRepository.findMany.mockResolvedValue([{ id: 'post-1', isArchived: true }]);
    postsRepository.count.mockResolvedValue(1);

    const result = await service.findUserPosts(
      userId,
      { visibility: 'archived', page: 1, limit: 20 },
      userId,
    );

    expect(result.items).toHaveLength(1);
    expect(postsRepository.findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        authorId: new Types.ObjectId(userId),
        isArchived: true,
      }),
      0,
      20,
      expect.objectContaining({ pinnedToProfile: -1 }),
    );
    expect(postsRepository.count).toHaveBeenCalledWith(
      expect.objectContaining({
        authorId: new Types.ObjectId(userId),
        isArchived: true,
      }),
    );
  });

  it('does not expose archived posts to another viewer', async () => {
    const userId = new Types.ObjectId().toString();
    const viewerId = new Types.ObjectId().toString();
    const { service, postsRepository } = createService();

    const result = await service.findUserPosts(
      userId,
      { visibility: 'archived', page: 1, limit: 20 },
      viewerId,
    );

    expect(result.items).toEqual([]);
    expect(result.total).toBe(0);
    expect(postsRepository.findMany).not.toHaveBeenCalled();
  });

  it('keeps normal public profile filtering unchanged', async () => {
    const userId = new Types.ObjectId().toString();
    const viewerId = new Types.ObjectId().toString();
    const { service, postsRepository } = createService();

    await service.findUserPosts(
      userId,
      { visibility: PostVisibility.PUBLIC, page: 1, limit: 20 },
      viewerId,
    );

    expect(postsRepository.findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        authorId: new Types.ObjectId(userId),
        isArchived: { $ne: true },
        visibility: PostVisibility.PUBLIC,
      }),
      0,
      20,
      expect.any(Object),
    );
  });

  it('does not allow a non-follower to request private profile posts', async () => {
    const userId = new Types.ObjectId().toString();
    const viewerId = new Types.ObjectId().toString();
    const { service, postsRepository } = createService();

    const result = await service.findUserPosts(
      userId,
      { visibility: PostVisibility.PRIVATE, page: 1, limit: 20 },
      viewerId,
    );

    expect(result.items).toEqual([]);
    expect(postsRepository.findMany).not.toHaveBeenCalled();
  });

  it('hides a private post when fetched directly by another user', async () => {
    const authorId = new Types.ObjectId().toString();
    const viewerId = new Types.ObjectId().toString();
    const postId = new Types.ObjectId().toString();
    const { service, postsRepository } = createService();
    postsRepository.findById.mockResolvedValue({
      id: postId,
      authorId: new Types.ObjectId(authorId),
      visibility: PostVisibility.PRIVATE,
      isDeleted: false,
      isArchived: false,
      moderationStatus: 'active',
    });

    await expect(service.findById(postId, viewerId)).rejects.toThrow('Post not found');
  });

  it('maps archived postType=reel to video posts', async () => {
    const userId = new Types.ObjectId().toString();
    const { service, postsRepository } = createService();

    await service.findUserPosts(
      userId,
      { visibility: 'archived', postType: 'reel', page: 1, limit: 20 },
      userId,
    );

    expect(postsRepository.findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        isArchived: true,
        postType: PostType.VIDEO,
      }),
      0,
      20,
      expect.any(Object),
    );
  });

  it('returns original posts and shared_post items in one profile timeline', async () => {
    const userId = new Types.ObjectId().toString();
    const viewerId = new Types.ObjectId().toString();
    const authorId = new Types.ObjectId().toString();
    const ownPostId = new Types.ObjectId().toString();
    const originalPostId = new Types.ObjectId().toString();
    const shareId = new Types.ObjectId();
    const { service, postsRepository, usersRepository, collectionMock } = createService();
    const ownPost = {
      id: ownPostId,
      _id: new Types.ObjectId(ownPostId),
      authorId: new Types.ObjectId(userId),
      content: 'Own post',
      visibility: PostVisibility.PUBLIC,
      postType: PostType.TEXT,
      shareCount: 0,
      isArchived: false,
      moderationStatus: 'active',
      createdAt: new Date('2026-07-03T10:00:00.000Z'),
    };
    const originalPost = {
      id: originalPostId,
      _id: new Types.ObjectId(originalPostId),
      authorId: {
        _id: new Types.ObjectId(authorId),
        id: authorId,
        name: 'Original Author',
        username: 'original',
        avatar: '',
      },
      content: 'Shared original',
      visibility: PostVisibility.PUBLIC,
      postType: PostType.TEXT,
      shareCount: 2,
      hashtags: [],
      isArchived: false,
      isDeleted: false,
      moderationStatus: 'active',
      createdAt: new Date('2026-07-03T09:00:00.000Z'),
      toObject: () => ({
        id: originalPostId,
        _id: new Types.ObjectId(originalPostId),
        authorId: {
          _id: new Types.ObjectId(authorId),
          id: authorId,
          name: 'Original Author',
          username: 'original',
          avatar: '',
        },
        content: 'Shared original',
        visibility: PostVisibility.PUBLIC,
        postType: PostType.TEXT,
        shareCount: 2,
        createdAt: new Date('2026-07-03T09:00:00.000Z'),
      }),
    };
    const share = {
      _id: shareId,
      userId: new Types.ObjectId(userId),
      postId: new Types.ObjectId(originalPostId),
      originalAuthorId: new Types.ObjectId(authorId),
      target: 'profile',
      isDeleted: false,
      deletedAt: null,
      createdAt: new Date('2026-07-03T12:00:00.000Z'),
    };
    const shareFindChain = {
      sort: jest.fn().mockReturnThis(),
      limit: jest.fn().mockReturnThis(),
      project: jest.fn().mockReturnThis(),
      toArray: jest.fn().mockResolvedValue([share]),
    };
    const viewerShareFindChain = {
      sort: jest.fn().mockReturnThis(),
      limit: jest.fn().mockReturnThis(),
      project: jest.fn().mockReturnThis(),
      toArray: jest.fn().mockResolvedValue([{ postId: new Types.ObjectId(originalPostId) }]),
    };
    collectionMock.countDocuments.mockImplementation((filter: Record<string, unknown>) =>
      Promise.resolve(filter.target === 'profile' ? 1 : 0),
    );
    collectionMock.find.mockReturnValueOnce(shareFindChain).mockReturnValueOnce(viewerShareFindChain);
    postsRepository.count.mockResolvedValue(1);
    postsRepository.findMany.mockResolvedValue([ownPost]);
    postsRepository.findManyByIds.mockResolvedValue([originalPost]);
    usersRepository.findById.mockImplementation((id: string) =>
      Promise.resolve({
        id,
        isDisabled: false,
        toObject: () => ({
          _id: new Types.ObjectId(id),
          id,
          name: id === userId ? 'Profile User' : 'Original Author',
          username: id === userId ? 'profile' : 'original',
          avatar: '',
        }),
      }),
    );

    const result = await service.findUserPosts(userId, { page: 1, limit: 20 }, viewerId);

    expect(result.total).toBe(2);
    expect(result.items[0]).toEqual(
      expect.objectContaining({
        id: shareId.toString(),
        type: 'shared_post',
        sharedAt: '2026-07-03T12:00:00.000Z',
        sharedBy: expect.objectContaining({ id: userId, username: 'profile' }),
        sharedFrom: expect.objectContaining({ id: authorId, username: 'original' }),
        originalPost: expect.objectContaining({
          id: originalPostId,
          type: 'post',
          isSharedByMe: true,
        }),
      }),
    );
    expect(result.items[1]).toEqual(expect.objectContaining({ id: ownPostId, type: 'post' }));
  });
});

describe('PostsService content placement', () => {
  it('creates posts with contentTop and contentBottom and extracts text metadata from both', async () => {
    const userId = new Types.ObjectId().toString();
    const mentionedUserId = new Types.ObjectId().toString();
    const { service, postsRepository, usersRepository } = createService();
    postsRepository.create.mockResolvedValue({ id: 'post-1' });
    postsRepository.findById.mockResolvedValue({ id: 'post-1' });
    usersRepository.findByUsernames.mockResolvedValue([
      { id: mentionedUserId, username: 'artist', isDisabled: false },
    ]);

    await service.create(userId, {
      contentTop: 'Top #oud @artist',
      contentBottom: 'Bottom #maqam',
    });

    expect(postsRepository.create).toHaveBeenCalledWith(
      userId,
      expect.objectContaining({
        content: '',
        contentTop: 'Top #oud @artist',
        contentBottom: 'Bottom #maqam',
        hashtags: ['oud', 'maqam'],
        mentionUsernames: ['artist'],
        mentionedUserIds: expect.arrayContaining([expect.any(Types.ObjectId)]),
      }),
    );
  });

  it('updates contentBottom while preserving legacy content fallback', async () => {
    const userId = new Types.ObjectId().toString();
    const postId = new Types.ObjectId().toString();
    const { service, postsRepository } = createService();
    postsRepository.findById.mockResolvedValue({
      id: postId,
      authorId: new Types.ObjectId(userId),
      content: 'legacy #old',
      contentTop: '',
      contentBottom: '',
      imageUrls: [],
      videoUrl: '',
      audioUrl: '',
      taggedUserIds: [],
      collaboratorIds: [],
      mentionUsernames: [],
      mentionedUserIds: [],
    });
    postsRepository.updateById.mockResolvedValue({ id: postId, content: 'legacy #old', contentBottom: 'new #fresh' });

    await service.update(userId, postId, { contentBottom: 'new #fresh' });

    expect(postsRepository.updateById).toHaveBeenCalledWith(
      postId,
      expect.objectContaining({
        content: 'legacy #old',
        contentBottom: 'new #fresh',
        hashtags: ['fresh', 'old'],
      }),
    );
  });
});

describe('PostsService post sharing', () => {
  const buildPost = (authorId = new Types.ObjectId().toString()) => ({
    id: new Types.ObjectId().toString(),
    authorId: new Types.ObjectId(authorId),
    content: 'Shared oud session',
    visibility: PostVisibility.PUBLIC,
    shareCount: 4,
    isArchived: false,
    moderationStatus: 'active',
  });

  it('returns a share sheet with a public post URL and suggestedFriends array', async () => {
    const viewerId = new Types.ObjectId().toString();
    const authorId = new Types.ObjectId().toString();
    const post = buildPost(authorId);
    const { service, postsRepository, usersRepository } = createService();
    postsRepository.findById.mockResolvedValue(post);
    usersRepository.findById.mockImplementation((id: string) =>
      Promise.resolve({
        id,
        isDisabled: false,
        toObject: () => ({
          _id: new Types.ObjectId(id),
          username: id === viewerId ? 'viewer' : 'author',
          name: id === viewerId ? 'Viewer' : 'Author',
          stageName: '',
          avatar: '',
        }),
      }),
    );

    const result = await service.getShareSheet(viewerId, post.id);

    expect(result).toEqual(
      expect.objectContaining({
        postId: post.id,
        shareUrl: `https://oudelaa.com/posts/${post.id}`,
        shareCount: 4,
        suggestedFriends: [],
      }),
    );
    expect(result.viewer).toEqual(expect.objectContaining({ id: viewerId, username: 'viewer' }));
  });

  it('creates a profile share record without a body', async () => {
    const viewerId = new Types.ObjectId().toString();
    const post = buildPost();
    const { service, postsRepository, collectionMock } = createService();
    postsRepository.findById.mockResolvedValue(post);

    const result = await service.registerShare(viewerId, post.id);

    expect(postsRepository.incrementShareCount).toHaveBeenCalledWith(post.id, 1);
    expect(collectionMock.insertOne).toHaveBeenCalledWith(
      expect.objectContaining({
        userId: new Types.ObjectId(viewerId),
        postId: new Types.ObjectId(post.id),
        target: 'profile',
        isDeleted: false,
        deletedAt: null,
      }),
    );
    expect(result).toEqual(
      expect.objectContaining({
        message: 'Post shared successfully',
        postId: post.id,
        shareCount: 5,
        isSharedByMe: true,
        shareUrl: `https://oudelaa.com/posts/${post.id}`,
      }),
    );
    expect(result.shareId).toBeTruthy();
  });

  it('returns shared_post from profile posts after POST /share creates a profile share', async () => {
    const viewerId = new Types.ObjectId().toString();
    const authorId = new Types.ObjectId().toString();
    const post = buildPost(authorId);
    const shareId = new Types.ObjectId();
    const { service, postsRepository, collectionMock, usersRepository } = createService();
    let insertedShare: any;
    postsRepository.findById.mockResolvedValue(post);
    collectionMock.insertOne.mockImplementation(async (payload: any) => {
      insertedShare = { ...payload, _id: shareId };
      return { insertedId: shareId };
    });

    const shareResult = await service.registerShare(viewerId, post.id);

    expect(shareResult.shareId).toBe(shareId.toString());
    expect(insertedShare).toEqual(
      expect.objectContaining({
        userId: new Types.ObjectId(viewerId),
        postId: new Types.ObjectId(post.id),
        target: 'profile',
        isDeleted: false,
        deletedAt: null,
      }),
    );

    const originalPost = {
      ...post,
      _id: new Types.ObjectId(post.id),
      id: post.id,
      authorId: {
        _id: new Types.ObjectId(authorId),
        id: authorId,
        name: 'Original Author',
        username: 'original',
        avatar: '',
      },
      postType: PostType.TEXT,
      hashtags: [],
      isDeleted: false,
      createdAt: new Date('2026-07-03T09:00:00.000Z'),
      toObject: () => ({
        ...post,
        _id: new Types.ObjectId(post.id),
        id: post.id,
        authorId: {
          _id: new Types.ObjectId(authorId),
          id: authorId,
          name: 'Original Author',
          username: 'original',
          avatar: '',
        },
        postType: PostType.TEXT,
        createdAt: new Date('2026-07-03T09:00:00.000Z'),
      }),
    };
    const profileShareFindChain = {
      sort: jest.fn().mockReturnThis(),
      limit: jest.fn().mockReturnThis(),
      project: jest.fn().mockReturnThis(),
      toArray: jest.fn().mockResolvedValue([insertedShare]),
    };
    const viewerShareFindChain = {
      sort: jest.fn().mockReturnThis(),
      limit: jest.fn().mockReturnThis(),
      project: jest.fn().mockReturnThis(),
      toArray: jest.fn().mockResolvedValue([{ postId: new Types.ObjectId(post.id) }]),
    };

    collectionMock.countDocuments.mockImplementation((filter: Record<string, unknown>) =>
      Promise.resolve(filter.target === 'profile' ? 1 : 0),
    );
    collectionMock.find.mockReturnValueOnce(profileShareFindChain).mockReturnValueOnce(viewerShareFindChain);
    postsRepository.count.mockResolvedValue(0);
    postsRepository.findMany.mockResolvedValue([]);
    postsRepository.findManyByIds.mockResolvedValue([originalPost]);
    usersRepository.findById.mockImplementation((id: string) =>
      Promise.resolve({
        id,
        isDisabled: false,
        toObject: () => ({
          _id: new Types.ObjectId(id),
          id,
          name: id === viewerId ? 'Profile User' : 'Original Author',
          username: id === viewerId ? 'profile' : 'original',
          avatar: '',
        }),
      }),
    );

    const profileResult = await service.findUserPosts(viewerId, { page: 1, limit: 20 }, viewerId);

    expect(profileResult.total).toBe(1);
    expect(profileResult.items).toEqual([
      expect.objectContaining({
        id: shareId.toString(),
        type: 'shared_post',
        originalPost: expect.objectContaining({
          id: post.id,
          type: 'post',
          isSharedByMe: true,
        }),
      }),
    ]);
  });

  it('does not duplicate a profile share for the same user and post', async () => {
    const viewerId = new Types.ObjectId().toString();
    const post = buildPost();
    const shareId = new Types.ObjectId();
    const { service, postsRepository, collectionMock } = createService();
    postsRepository.findById.mockResolvedValue(post);
    collectionMock.findOne.mockResolvedValue({
      _id: shareId,
      userId: new Types.ObjectId(viewerId),
      postId: new Types.ObjectId(post.id),
      target: 'profile',
      isDeleted: false,
      deletedAt: null,
      createdAt: new Date(),
    });

    const result = await service.registerShare(viewerId, post.id);

    expect(collectionMock.insertOne).not.toHaveBeenCalled();
    expect(postsRepository.incrementShareCount).not.toHaveBeenCalled();
    expect(result.shareId).toBe(shareId.toString());
    expect(result.shareCount).toBe(4);
    expect(result.isSharedByMe).toBe(true);
  });

  it('records copy_link shares and increments shareCount', async () => {
    const viewerId = new Types.ObjectId().toString();
    const post = buildPost();
    const { service, postsRepository, collectionMock } = createService();
    postsRepository.findById.mockResolvedValue(post);

    const result = await service.registerShare(viewerId, post.id, {
      target: 'copy_link' as any,
      channel: 'copy_link' as any,
    });

    expect(collectionMock.insertOne).toHaveBeenCalledWith(
      expect.objectContaining({
        target: 'copy_link',
        channel: 'copy_link',
        shareUrl: `https://oudelaa.com/posts/${post.id}`,
      }),
    );
    expect(result.shareCount).toBe(5);
    expect(result.isSharedByMe).toBe(false);
  });

  it('soft deletes a profile share and decrements shareCount', async () => {
    const viewerId = new Types.ObjectId().toString();
    const post = buildPost();
    const { service, postsRepository, collectionMock } = createService();
    postsRepository.findById.mockResolvedValue(post);
    collectionMock.findOneAndUpdate.mockResolvedValue({
      _id: new Types.ObjectId(),
      userId: new Types.ObjectId(viewerId),
      postId: new Types.ObjectId(post.id),
      target: 'profile',
      isDeleted: true,
      deletedAt: new Date(),
    });

    const result = await service.unsharePost(viewerId, post.id);

    expect(collectionMock.findOneAndUpdate).toHaveBeenCalledWith(
      expect.objectContaining({
        userId: new Types.ObjectId(viewerId),
        postId: new Types.ObjectId(post.id),
        target: 'profile',
      }),
      expect.objectContaining({
        $set: expect.objectContaining({ isDeleted: true }),
      }),
      { returnDocument: 'after' },
    );
    expect(postsRepository.incrementShareCount).toHaveBeenCalledWith(post.id, -1);
    expect(result).toEqual({
      message: 'Post unshared successfully',
      postId: post.id,
      shareCount: 3,
      isSharedByMe: false,
    });
  });

  it('normalizes trailing slash in SHARE_BASE_URL when building post links', async () => {
    const viewerId = new Types.ObjectId().toString();
    const post = buildPost();
    const { service, postsRepository } = createService({
      shareBaseUrl: 'https://oudelaa.com/',
    });
    postsRepository.findById.mockResolvedValue(post);

    const result = await service.getShareSheet(viewerId, post.id);

    expect(result.shareUrl).toBe(`https://oudelaa.com/posts/${post.id}`);
  });

  it('does not use hosted.ghaymah.systems for post share URLs by default', async () => {
    const viewerId = new Types.ObjectId().toString();
    const post = buildPost();
    const { service, postsRepository } = createService();
    postsRepository.findById.mockResolvedValue(post);

    const result = await service.getShareSheet(viewerId, post.id);

    expect(result.shareUrl).not.toContain('hosted.ghaymah.systems');
  });
});

describe('PostsService production behavior', () => {
  const userId = '507f1f77bcf86cd799439011';
  const ownerId = '507f191e810c19729de860ea';
  const postId = '507f1f77bcf86cd799439012';
  const file = (mimetype: string, size = 32) => ({
    mimetype,
    size,
    buffer: Buffer.alloc(size, 1),
    originalname: 'upload.bin',
  });
  const post = (overrides: Record<string, unknown> = {}) => ({
    id: postId,
    _id: new Types.ObjectId(postId),
    authorId: new Types.ObjectId(ownerId),
    content: 'post content',
    imageUrls: [],
    videoUrl: '',
    hlsUrl: '',
    audioUrl: '',
    thumbnailUrl: '',
    taggedUserIds: [],
    collaboratorIds: [],
    mentionUsernames: [],
    mentionedUserIds: [],
    visibility: PostVisibility.PUBLIC,
    postType: PostType.TEXT,
    isDeleted: false,
    isArchived: false,
    moderationStatus: ModerationStatus.ACTIVE,
    shareCount: 2,
    viewCount: 3,
    playCount: 4,
    ...overrides,
  });

  it.each([
    [{ imageUrls: Array(11).fill('x') }, [], undefined, undefined, undefined, 'up to 10 images'],
    [{ imageUrls: ['url'] }, [file('image/jpeg')], undefined, undefined, undefined, 'either imageFiles or imageUrls'],
    [{}, [], file('video/mp4'), file('audio/mpeg'), undefined, 'either images, video, or audio'],
    [{ videoUrl: 'url' }, [], file('video/mp4'), undefined, undefined, 'either videoFile or videoUrl'],
    [{ audioUrl: 'url' }, [], undefined, file('audio/mpeg'), undefined, 'either audioFile or audioUrl'],
    [{ thumbnailUrl: 'url' }, [], undefined, undefined, file('image/jpeg'), 'either coverImageFile or thumbnailUrl'],
    [{ content: 'x' }, [], undefined, undefined, file('image/jpeg'), 'allowed only with video or audio'],
    [{ videoUrl: 'video', imageUrls: ['image'] }, [], undefined, undefined, undefined, 'either images or video'],
    [{ audioUrl: 'audio', imageUrls: ['image'] }, [], undefined, undefined, undefined, 'either images or audio'],
  ] as const)(
    'rejects conflicting create media input %#',
    async (dto, images, video, audio, cover, message) => {
      const { service, postsRepository } = createService();
      await expect(
        service.create(userId, dto as any, images as any, video as any, audio as any, cover as any),
      ).rejects.toThrow(message);
      expect(postsRepository.create).not.toHaveBeenCalled();
    },
  );

  it('requires text or media and applies automatic moderation to accepted posts', async () => {
    const empty = createService();
    await expect(empty.service.create(userId, {})).rejects.toThrow('Post must contain caption or media');

    const ctx = createService();
    ctx.contentModerationService.analyze.mockReturnValue({
      flagged: true,
      score: 90,
      reasons: ['spam'],
    });
    ctx.postsRepository.create.mockResolvedValue({ id: postId });
    ctx.postsRepository.findById.mockResolvedValue({ id: postId, moderationStatus: ModerationStatus.FLAGGED });
    await ctx.service.create(userId, {
      content: '  flagged #LOUD  ',
      visibility: PostVisibility.FOLLOWERS,
      commentsDisabled: true,
    });
    expect(ctx.postsRepository.create).toHaveBeenCalledWith(
      userId,
      expect.objectContaining({
        content: 'flagged #LOUD',
        postType: PostType.TEXT,
        hashtags: ['loud'],
        moderationStatus: ModerationStatus.FLAGGED,
        visibility: PostVisibility.FOLLOWERS,
        commentsDisabled: true,
      }),
    );
    expect(ctx.usersRepository.incrementPostsCount).toHaveBeenCalledWith(userId, 1);
    expect(ctx.feedVersionService.bumpGlobalVersion).toHaveBeenCalled();
  });

  it('cleans uploaded media when persistence fails', async () => {
    const ctx = createService();
    const savedAsset = {
      primaryUrl: 'https://cdn.example.com/images/main.jpg',
      variants: {
        sm: 'https://cdn.example.com/images/sm.jpg',
        md: 'https://cdn.example.com/images/md.jpg',
        lg: 'https://cdn.example.com/images/lg.jpg',
        original: 'https://cdn.example.com/images/main.jpg',
      },
    };
    jest.spyOn(ctx.service as any, 'saveImageFiles').mockResolvedValue([savedAsset]);
    const cleanup = jest.spyOn(ctx.service as any, 'deleteSavedImageAsset').mockResolvedValue(undefined);
    ctx.postsRepository.create.mockRejectedValue(new Error('database down'));

    await expect(ctx.service.create(userId, { content: 'image' }, [file('image/jpeg')])).rejects.toThrow(
      'database down',
    );
    expect(cleanup).toHaveBeenCalledWith(savedAsset);
    expect(ctx.usersRepository.incrementPostsCount).not.toHaveBeenCalled();
  });

  it('validates update existence, ownership and conflicting media input', async () => {
    const missing = createService();
    missing.postsRepository.findById.mockResolvedValue(null);
    await expect(missing.service.update(userId, postId, { content: 'new' })).rejects.toThrow('Post not found');

    const foreign = createService();
    foreign.postsRepository.findById.mockResolvedValue(post());
    await expect(foreign.service.update(userId, postId, { content: 'new' })).rejects.toThrow(
      'You can only update your own posts',
    );

    const own = createService();
    own.postsRepository.findById.mockResolvedValue(post({ authorId: new Types.ObjectId(userId) }));
    await expect(
      own.service.update(userId, postId, { imageUrls: ['url'] }, [file('image/jpeg')]),
    ).rejects.toThrow('either imageFiles or imageUrls');
  });

  it('updates an owned post, removes incompatible media and invalidates feed cache', async () => {
    const ctx = createService();
    const existing = post({
      authorId: new Types.ObjectId(userId),
      videoUrl: 'https://cdn.example.com/old.mp4',
      hlsUrl: 'https://cdn.example.com/old/master.m3u8',
      thumbnailUrl: 'https://cdn.example.com/old.jpg',
      postType: PostType.VIDEO,
    });
    const updated = post({
      authorId: new Types.ObjectId(userId),
      content: 'new #image',
      imageUrls: ['https://cdn.example.com/new.jpg'],
      videoUrl: '',
      hlsUrl: '',
      thumbnailUrl: '',
      postType: PostType.IMAGE,
    });
    ctx.postsRepository.findById.mockResolvedValue(existing);
    ctx.postsRepository.updateById.mockResolvedValue(updated);

    await expect(
      ctx.service.update(userId, postId, {
        content: ' new #image ',
        imageUrls: ['https://cdn.example.com/new.jpg'],
        videoUrl: '',
      }),
    ).resolves.toBe(updated);
    expect(ctx.postsRepository.updateById).toHaveBeenCalledWith(
      postId,
      expect.objectContaining({
        content: 'new #image',
        imageUrls: ['https://cdn.example.com/new.jpg'],
        videoUrl: '',
        hlsUrl: '',
        audioUrl: '',
        postType: PostType.IMAGE,
      }),
    );
    expect(ctx.storageService.deleteFile).toHaveBeenCalledWith('https://cdn.example.com/old.mp4');
    expect(ctx.storageService.deleteContainingDirectory).toHaveBeenCalledWith(
      'https://cdn.example.com/old/master.m3u8',
    );
    expect(ctx.feedVersionService.bumpGlobalVersion).toHaveBeenCalled();
  });

  it('removes only owned posts, updates counters and tolerates cleanup failures', async () => {
    const missing = createService();
    await expect(missing.service.remove(userId, postId)).rejects.toThrow('Post not found');

    const foreign = createService();
    foreign.postsRepository.findById.mockResolvedValue(post());
    await expect(foreign.service.remove(userId, postId)).rejects.toThrow('delete your own posts');

    const ctx = createService();
    ctx.postsRepository.findById.mockResolvedValue(
      post({
        authorId: new Types.ObjectId(userId),
        videoUrl: 'https://cdn.example.com/video.mp4',
        hlsUrl: 'https://cdn.example.com/hls/master.m3u8',
      }),
    );
    ctx.storageService.deleteContainingDirectory.mockRejectedValue(new Error('cleanup failed'));
    await expect(ctx.service.remove(userId, postId)).resolves.toBeUndefined();
    expect(ctx.postsRepository.deleteById).toHaveBeenCalledWith(postId, userId);
    expect(ctx.usersRepository.incrementPostsCount).toHaveBeenCalledWith(userId, -1);
  });

  it('returns a formatted visible post and marks an active profile share', async () => {
    const ctx = createService();
    const visible = post({
      authorId: new Types.ObjectId(ownerId),
      toObject: () => ({ _id: new Types.ObjectId(postId), authorId: ownerId, shareCount: 2 }),
    });
    ctx.postsRepository.findById.mockResolvedValue(visible);
    ctx.usersRepository.findById.mockResolvedValue({ id: ownerId, isDisabled: false });
    ctx.collectionMock.find.mockReturnValue({
      sort: jest.fn().mockReturnThis(),
      limit: jest.fn().mockReturnThis(),
      project: jest.fn().mockReturnThis(),
      toArray: jest.fn().mockResolvedValue([{ postId: new Types.ObjectId(postId) }]),
    });

    await expect(ctx.service.findById(postId, userId)).resolves.toMatchObject({
      id: postId,
      type: 'post',
      shareCount: 2,
      isSharedByMe: true,
    });

    ctx.postsRepository.findById.mockResolvedValue(null);
    await expect(ctx.service.findById(postId, userId)).rejects.toThrow('Post not found');
  });

  it('rejects invalid profile ids and hides blocked user profiles', async () => {
    const invalid = createService();
    await expect(invalid.service.findUserPosts('bad', {})).rejects.toThrow('Invalid user id');

    const blocked = createService();
    jest.spyOn(blocked.service as any, 'hasBlockBetween').mockResolvedValue(true);
    await expect(blocked.service.findUserPosts(ownerId, {}, userId)).resolves.toMatchObject({
      items: [],
      total: 0,
    });
    expect(blocked.postsRepository.findMany).not.toHaveBeenCalled();
  });

  it('builds complete platform-admin post filters', async () => {
    const ctx = createService();
    ctx.postsRepository.findManyAdmin.mockResolvedValue([{ id: postId }]);
    ctx.postsRepository.countAdmin.mockResolvedValue(8);
    const result = await ctx.service.findPlatformPosts({
      page: 2,
      limit: 3,
      sortOrder: SortOrder.ASC,
      sortBy: 'likesCount' as any,
      visibility: PostVisibility.FOLLOWERS,
      postType: PostType.AUDIO,
      authorId: ownerId,
      q: ' a+b ',
      hashtag: '#OUD',
      moderationStatus: ModerationStatus.FLAGGED,
    });
    expect(result).toMatchObject({ total: 8, page: 2, limit: 3 });
    expect(ctx.postsRepository.findManyAdmin).toHaveBeenCalledWith(
      {
        visibility: PostVisibility.FOLLOWERS,
        postType: PostType.AUDIO,
        authorId: new Types.ObjectId(ownerId),
        content: { $regex: 'a\\+b', $options: 'i' },
        hashtags: 'oud',
        moderationStatus: ModerationStatus.FLAGGED,
      },
      3,
      3,
      { likesCount: 1 },
    );
  });

  it('validates reel creation and delegates valid reel payloads to create', async () => {
    const ctx = createService();
    await expect(ctx.service.createReel(userId, {})).rejects.toThrow('Reel requires');
    await expect(
      ctx.service.createReel(userId, { videoUrl: 'url' }, file('video/mp4')),
    ).rejects.toThrow('either videoFile or videoUrl');

    const create = jest.spyOn(ctx.service, 'create').mockResolvedValue({ id: postId } as any);
    await ctx.service.createReel(userId, {
      videoUrl: 'https://cdn.example.com/reel.mp4',
      content: 'reel',
      visibility: PostVisibility.FOLLOWERS,
    });
    expect(create).toHaveBeenCalledWith(
      userId,
      expect.objectContaining({
        videoUrl: 'https://cdn.example.com/reel.mp4',
        content: 'reel',
        visibility: PostVisibility.FOLLOWERS,
      }),
      [],
      undefined,
      undefined,
      undefined,
    );
  });

  it('lists reels with filters, escaped query and pagination', async () => {
    const ctx = createService();
    ctx.postsRepository.findMany.mockResolvedValue([{ id: postId }]);
    ctx.postsRepository.count.mockResolvedValue(1);
    const result = await ctx.service.findReels(
      {
        page: 2,
        limit: 5,
        visibility: PostVisibility.PUBLIC,
        authorId: ownerId,
        q: 'a+b',
        sortOrder: SortOrder.ASC,
        sortBy: 'playCount' as any,
      },
      userId,
    );
    expect(result.total).toBe(1);
    expect(ctx.postsRepository.findMany).toHaveBeenCalledWith(
      {
        postType: PostType.VIDEO,
        authorId: new Types.ObjectId(ownerId),
        content: { $regex: 'a\\+b', $options: 'i' },
        $and: [
          {
            $or: [
              { visibility: PostVisibility.PUBLIC },
              { authorId: new Types.ObjectId(userId) },
            ],
          },
          { visibility: PostVisibility.PUBLIC },
        ],
      },
      5,
      5,
      { playCount: 1 },
    );
  });

  it('excludes private and blocked authors from the viewer reels query', async () => {
    const ctx = createService();
    const privateAuthorId = new Types.ObjectId().toString();
    const blockedAuthorId = new Types.ObjectId().toString();
    ctx.usersRepository.findPrivateUserIds.mockResolvedValue([privateAuthorId]);
    jest.spyOn(ctx.service as any, 'findInvisibleUserIds').mockResolvedValue([blockedAuthorId]);

    await ctx.service.findReels({ page: 1, limit: 20 }, userId);

    expect(ctx.postsRepository.findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        authorId: {
          $nin: [
            new Types.ObjectId(blockedAuthorId),
            new Types.ObjectId(privateAuthorId),
          ],
        },
      }),
      0,
      20,
      expect.any(Object),
    );
  });

  it('deduplicates post views and media plays through cache keys', async () => {
    const ctx = createService();
    ctx.postsRepository.findById.mockResolvedValue(
      post({ authorId: new Types.ObjectId(userId), postType: PostType.VIDEO }),
    );
    await expect(ctx.service.registerView(userId, postId)).resolves.toEqual({
      success: true,
      postId,
      viewCount: 4,
    });
    expect(ctx.postsRepository.incrementViewCount).toHaveBeenCalledWith(postId, 1);

    ctx.appCacheService.setIfAbsent.mockResolvedValue(false);
    await expect(ctx.service.registerPlay(userId, postId)).resolves.toEqual({
      success: true,
      postId,
      playCount: 4,
    });
    expect(ctx.postsRepository.incrementPlayCount).not.toHaveBeenCalled();

    ctx.postsRepository.findById.mockResolvedValue(
      post({ authorId: new Types.ObjectId(userId), postType: PostType.TEXT }),
    );
    await expect(ctx.service.registerPlay(userId, postId)).rejects.toThrow('only for audio or video');

    ctx.postsRepository.findById.mockResolvedValue(null);
    await expect(ctx.service.registerView(userId, postId)).rejects.toThrow('Post not found');
  });

  it('routes friend shares through chat and records the complete event', async () => {
    const friendId = '507f191e810c19729de860eb';
    const ctx = createService();
    ctx.postsRepository.findById.mockResolvedValue(post());
    ctx.usersRepository.findById.mockImplementation(async (id: string) => ({ id, isDisabled: false }));
    const result = await ctx.service.registerShare(userId, postId, {
      target: PostShareTarget.FRIEND,
      friendId,
      caption: ' listen ',
    });
    expect(result).toMatchObject({ message: 'Post shared successfully', shareCount: 3, isSharedByMe: false });
    expect(ctx.chatService.createConversation).toHaveBeenCalledWith(userId, {
      participantIds: [friendId],
      isGroup: false,
    });
    expect(ctx.chatService.sendMessage).toHaveBeenCalledWith(
      userId,
      expect.objectContaining({
        conversationId: '507f191e810c19729de860f1',
        content: expect.stringContaining(`shared_post:${postId}`),
      }),
    );
    expect(ctx.collectionMock.insertOne).toHaveBeenCalledWith(
      expect.objectContaining({
        target: PostShareTarget.FRIEND,
        friendId: new Types.ObjectId(friendId),
        conversationId: new Types.ObjectId('507f191e810c19729de860f1'),
        messageId: new Types.ObjectId('507f191e810c19729de860f2'),
      }),
    );

    await expect(
      ctx.service.registerShare(userId, postId, { target: PostShareTarget.FRIEND }),
    ).rejects.toThrow('friendId is required');
  });

  it('validates share friends against ids, self, disabled accounts and blocks', async () => {
    const ctx = createService();
    await expect((ctx.service as any).assertShareFriend(userId, 'bad')).rejects.toThrow('Invalid friend id');
    await expect((ctx.service as any).assertShareFriend(userId, userId)).rejects.toThrow(
      'friendId cannot be the current user',
    );
    ctx.usersRepository.findById.mockResolvedValue(null);
    await expect((ctx.service as any).assertShareFriend(userId, ownerId)).rejects.toThrow('Friend not found');

    ctx.usersRepository.findById.mockResolvedValue({ id: ownerId, isDisabled: false });
    ctx.blocksRepository.findAnyBetween.mockResolvedValue({ id: 'block' });
    await expect((ctx.service as any).assertShareFriend(userId, ownerId)).rejects.toThrow(
      'You cannot share with this user',
    );
  });

  it('shares externally and maps copy-link channels consistently', async () => {
    const ctx = createService();
    const register = jest.spyOn(ctx.service, 'registerShare').mockResolvedValue({} as any);
    await ctx.service.sharePostExternal(userId, postId, { channel: PostShareChannel.COPY_LINK });
    expect(register).toHaveBeenCalledWith(userId, postId, {
      target: PostShareTarget.COPY_LINK,
      channel: PostShareChannel.COPY_LINK,
    });
    await ctx.service.sharePostExternal(userId, postId, {});
    expect(register).toHaveBeenLastCalledWith(userId, postId, {
      target: PostShareTarget.EXTERNAL,
      channel: PostShareChannel.OTHER,
    });
  });

  it('shares to profile by creating a repost and linking the share event', async () => {
    const ctx = createService();
    ctx.postsRepository.findById
      .mockResolvedValueOnce(post())
      .mockResolvedValueOnce(post({ shareCount: 5 }));
    ctx.usersRepository.findById.mockResolvedValue({ id: ownerId, isDisabled: false });
    const repost = { id: '507f191e810c19729de860ec' };
    jest.spyOn(ctx.service, 'createRepost').mockResolvedValue(repost as any);

    await expect(
      ctx.service.sharePostToProfile(userId, postId, { caption: '  my caption  ' }),
    ).resolves.toEqual({ shared: true, shareCount: 5, post: repost });
    expect(ctx.service.createRepost).toHaveBeenCalledWith(userId, postId, {
      content: 'my caption',
      visibility: PostVisibility.PUBLIC,
    });
    expect(ctx.collectionMock.insertOne).toHaveBeenCalledWith(
      expect.objectContaining({ sharedPostId: new Types.ObjectId(repost.id) }),
    );
  });

  it('updates normalized comment settings and supports pin/archive lifecycle', async () => {
    const methods = [
      ['pinToProfile', { pinnedToProfile: true }],
      ['unpinFromProfile', { pinnedToProfile: false }],
      ['archive', { isArchived: true, pinnedToProfile: false }],
      ['restoreArchived', { isArchived: false }],
    ] as const;
    for (const [method, payload] of methods) {
      const ctx = createService();
      const owned = post({ authorId: new Types.ObjectId(userId) });
      ctx.postsRepository.findById.mockResolvedValue(owned);
      ctx.postsRepository.updateById.mockResolvedValue({ ...owned, ...payload });
      await expect((ctx.service as any)[method](userId, postId)).resolves.toMatchObject(payload);
      expect(ctx.postsRepository.updateById).toHaveBeenCalledWith(postId, payload);
    }

    const settings = createService();
    settings.postsRepository.findById.mockResolvedValue(post({ authorId: new Types.ObjectId(userId) }));
    settings.postsRepository.updateById.mockResolvedValue({ id: postId });
    await settings.service.updateCommentSettings(userId, postId, {
      commentsDisabled: true,
      commentsFollowersOnly: false,
      commentFilterKeywords: [' Spam ', 'spam', '', 'Abuse'],
    });
    expect(settings.postsRepository.updateById).toHaveBeenCalledWith(postId, {
      commentsDisabled: true,
      commentsFollowersOnly: false,
      commentFilterKeywords: ['spam', 'abuse'],
    });
  });

  it('handles owner-action repository races and owner authorization errors', async () => {
    const missing = createService();
    await expect(missing.service.pinToProfile(userId, postId)).rejects.toThrow('Post not found');
    const foreign = createService();
    foreign.postsRepository.findById.mockResolvedValue(post());
    await expect(foreign.service.archive(userId, postId)).rejects.toThrow('update only your own posts');

    const race = createService();
    race.postsRepository.findById.mockResolvedValue(post({ authorId: new Types.ObjectId(userId) }));
    race.postsRepository.updateById.mockResolvedValue(null);
    await expect(race.service.restoreArchived(userId, postId)).rejects.toThrow('Post not found');
    await expect(race.service.updateCommentSettings(userId, postId, {})).rejects.toThrow('Post not found');
  });

  it('deletes posts as superadmin with counter, cache and audit updates', async () => {
    const ctx = createService();
    ctx.postsRepository.findById.mockResolvedValue(
      post({
        imageUrls: ['https://cdn.example.com/image.jpg'],
        videoUrl: 'https://cdn.example.com/video.mp4',
        hlsUrl: 'https://cdn.example.com/hls/master.m3u8',
      }),
    );
    await ctx.service.removeBySuperAdmin('admin@example.com', postId);
    expect(ctx.postsRepository.deleteById).toHaveBeenCalledWith(postId, 'admin@example.com');
    expect(ctx.usersRepository.incrementPostsCount).toHaveBeenCalledWith(ownerId, -1);
    expect(ctx.auditService.logSuperAdminAction).toHaveBeenCalledWith(
      'admin@example.com',
      'post_delete',
      'post',
      postId,
      { authorId: ownerId },
    );
    await expect(createService().service.removeBySuperAdmin('admin', postId)).rejects.toThrow(
      'Post not found',
    );
  });

  it('updates post moderation status and records the previous state', async () => {
    const ctx = createService();
    const updated = post({ moderationStatus: ModerationStatus.HIDDEN });
    ctx.postsRepository.findById.mockResolvedValue(post({ moderationStatus: ModerationStatus.FLAGGED }));
    ctx.postsRepository.updateModerationStatus.mockResolvedValue(updated);
    await expect(
      ctx.service.updateModerationStatusBySuperAdmin('admin', postId, {
        status: ModerationStatus.HIDDEN,
        reason: '  abuse  ',
      }),
    ).resolves.toBe(updated);
    expect(ctx.postsRepository.updateModerationStatus).toHaveBeenCalledWith(postId, {
      moderationStatus: ModerationStatus.HIDDEN,
      moderationReason: 'abuse',
    });
    expect(ctx.auditService.logSuperAdminAction).toHaveBeenCalledWith(
      'admin',
      'post_moderation_status_update',
      'post',
      postId,
      {
        previousStatus: ModerationStatus.FLAGGED,
        nextStatus: ModerationStatus.HIDDEN,
        reason: 'abuse',
      },
    );

    await expect(
      createService().service.updateModerationStatusBySuperAdmin('admin', postId, {
        status: ModerationStatus.ACTIVE,
      }),
    ).rejects.toThrow('Post not found');
  });

  it('enforces visibility, author state, blocks and followers in canViewerSeePost', async () => {
    const ctx = createService();
    ctx.usersRepository.findById.mockResolvedValue({ id: ownerId, isDisabled: false });
    await expect(ctx.service.canViewerSeePost(undefined, post() as any)).resolves.toBe(true);
    await expect(
      ctx.service.canViewerSeePost(undefined, post({ visibility: PostVisibility.PRIVATE }) as any),
    ).resolves.toBe(false);
    await expect(
      ctx.service.canViewerSeePost(undefined, post({ isArchived: true }) as any),
    ).resolves.toBe(false);

    ctx.usersRepository.findById.mockResolvedValue({ id: ownerId, isDisabled: true });
    await expect(ctx.service.canViewerSeePost(userId, post() as any)).resolves.toBe(false);

    ctx.usersRepository.findById.mockResolvedValue({ id: ownerId, isDisabled: false });
    jest.spyOn(ctx.service as any, 'hasBlockBetween').mockResolvedValue(true);
    await expect(ctx.service.canViewerSeePost(userId, post() as any)).resolves.toBe(false);

    (ctx.service as any).hasBlockBetween.mockResolvedValue(false);
    jest.spyOn(ctx.service as any, 'isFollowing').mockResolvedValue(false);
    await expect(
      ctx.service.canViewerSeePost(userId, post({ visibility: PostVisibility.FOLLOWERS }) as any),
    ).resolves.toBe(false);
    (ctx.service as any).isFollowing.mockResolvedValue(true);
    await expect(
      ctx.service.canViewerSeePost(userId, post({ visibility: PostVisibility.FOLLOWERS }) as any),
    ).resolves.toBe(true);

    ctx.usersRepository.findById.mockResolvedValue({
      id: ownerId,
      isDisabled: false,
      isPrivate: true,
    });
    (ctx.service as any).isFollowing.mockResolvedValue(false);
    await expect(
      ctx.service.canViewerSeePost(userId, post({ visibility: PostVisibility.PUBLIC }) as any),
    ).resolves.toBe(false);
    (ctx.service as any).isFollowing.mockResolvedValue(true);
    await expect(
      ctx.service.canViewerSeePost(userId, post({ visibility: PostVisibility.PUBLIC }) as any),
    ).resolves.toBe(true);
  });

  it('protects direct public posts owned by private accounts', async () => {
    const ctx = createService();
    ctx.postsRepository.findById.mockResolvedValue(
      post({ visibility: PostVisibility.PUBLIC }),
    );
    ctx.usersRepository.findById.mockResolvedValue({
      id: ownerId,
      isDisabled: false,
      isPrivate: true,
    });
    jest.spyOn(ctx.service as any, 'hasBlockBetween').mockResolvedValue(false);
    jest.spyOn(ctx.service as any, 'isFollowing').mockResolvedValue(false);
    await expect(ctx.service.findById(postId, userId)).rejects.toThrow('Post not found');
    (ctx.service as any).isFollowing.mockResolvedValue(true);
    await expect(ctx.service.findById(postId, userId)).resolves.toBeDefined();
  });

  it('redacts a populated repost original when its private author is no longer accessible', async () => {
    const ctx = createService();
    const originalAuthorId = '507f191e810c19729de860ef';
    const original = post({
      id: '507f191e810c19729de860ee',
      _id: new Types.ObjectId('507f191e810c19729de860ee'),
      authorId: new Types.ObjectId(originalAuthorId),
      visibility: PostVisibility.PUBLIC,
    });
    ctx.postsRepository.findById.mockResolvedValue(
      post({ repostOfPostId: original }),
    );
    ctx.usersRepository.findById.mockImplementation(async (id: string) => ({
      id,
      isDisabled: false,
      isPrivate: id === originalAuthorId,
    }));
    jest.spyOn(ctx.service as any, 'hasBlockBetween').mockResolvedValue(false);
    jest.spyOn(ctx.service as any, 'isFollowing').mockResolvedValue(false);

    const result = await ctx.service.findById(postId, userId);
    expect(result.repostOfPostId).toBeNull();
  });

  it('returns no profile posts to non-followers of private accounts', async () => {
    const ctx = createService();
    ctx.usersRepository.findById.mockResolvedValue({
      id: ownerId,
      isDisabled: false,
      isPrivate: true,
    });
    jest.spyOn(ctx.service as any, 'isFollowing').mockResolvedValue(false);
    const hidden = await ctx.service.findUserPosts(ownerId, { page: 1, limit: 20 }, userId);
    expect(hidden.items).toEqual([]);
    expect(ctx.postsRepository.findMany).not.toHaveBeenCalled();

    (ctx.service as any).isFollowing.mockResolvedValue(true);
    await ctx.service.findUserPosts(ownerId, { page: 1, limit: 20 }, userId);
    expect(ctx.postsRepository.findMany).toHaveBeenCalled();
  });

  it('resolves mutually exclusive post types and reel filters', () => {
    const { service } = createService();
    const internals = service as any;
    expect(internals.resolvePostType()).toBe(PostType.TEXT);
    expect(internals.resolvePostType(['image'])).toBe(PostType.IMAGE);
    expect(internals.resolvePostType([], 'video')).toBe(PostType.VIDEO);
    expect(internals.resolvePostType([], '', 'audio')).toBe(PostType.AUDIO);
    expect(() => internals.resolvePostType(['image'], 'video')).toThrow('either images, video, or audio');
    expect(internals.resolvePostTypeFilter()).toBeNull();
    expect(internals.resolvePostTypeFilter('reel')).toBe(PostType.VIDEO);
    expect(internals.resolvePostTypeFilter('audio')).toBe(PostType.AUDIO);
  });

  it('normalizes media metadata and generates audio waveform variants', () => {
    const { service } = createService();
    const internals = service as any;
    expect(internals.normalizeMediaMetadata({}, PostType.TEXT)).toEqual({
      durationSeconds: null,
      thumbnailUrl: '',
      style: '',
      maqam: '',
      rhythmSignature: '',
      waveformPeaks: [],
      waveformPeaksPreview: [],
      waveformPeaksDetailed: [],
    });
    expect(() => internals.normalizeMediaMetadata({ durationSeconds: 2 }, PostType.IMAGE)).toThrow(
      'allowed only for audio or video',
    );
    expect(() => internals.normalizeMediaMetadata({ waveformPeaks: [0.2] }, PostType.VIDEO)).toThrow(
      'waveformPeaks is allowed only for audio',
    );
    const audio = internals.normalizeMediaMetadata(
      {
        durationSeconds: 9,
        thumbnailUrl: ' cover ',
        style: ' classic ',
        maqam: ' bayati ',
        rhythmSignature: ' 4/4 ',
        waveformPeaks: [0, 0.5, 1],
      },
      PostType.AUDIO,
    );
    expect(audio).toMatchObject({
      durationSeconds: 9,
      thumbnailUrl: 'cover',
      style: 'classic',
      maqam: 'bayati',
      rhythmSignature: '4/4',
    });
    expect(audio.waveformPeaks.length).toBeGreaterThan(0);
    expect(audio.waveformPeaksPreview.length).toBeGreaterThan(0);
    expect(audio.waveformPeaksDetailed.length).toBeGreaterThan(0);
  });

  it('extracts normalized unique mentions, hashtags and composed post text', () => {
    const { service } = createService();
    const internals = service as any;
    expect(internals.combinePostText(' bottom ', ' top ', ' middle ')).toBe('top\nmiddle\nbottom');
    expect(internals.extractMentions('Hi @Artist and @artist و @عازف')).toEqual(['artist', 'عازف']);
    expect(internals.normalizeMentionUsernames([' @ARTIST ', '@@artist', '', undefined])).toEqual([
      'artist',
    ]);
    expect(internals.extractHashtags('#OUD #oud #مقام invalid')).toEqual(['oud', 'مقام']);
  });

  it('resolves mentions by username and id while excluding disabled users and the author', async () => {
    const mentionedId = '507f191e810c19729de860eb';
    const secondId = '507f191e810c19729de860ec';
    const ctx = createService();
    ctx.usersRepository.findByUsernames.mockResolvedValue([
      { id: mentionedId, username: 'Artist', isDisabled: false, name: 'Artist' },
      { id: secondId, username: 'disabled', isDisabled: true },
    ]);
    ctx.usersRepository.findMany.mockResolvedValue([
      { id: mentionedId, username: 'artist', isDisabled: false },
      { id: userId, username: 'self', isDisabled: false },
    ]);
    const result = await (ctx.service as any).resolveMentionTargets(
      ['@artist', '@disabled'],
      [mentionedId, userId, 'bad'],
      '@Artist hello',
      userId,
    );
    expect(result.mentionUsernames).toEqual(['artist']);
    expect(result.mentionedUserIds.map(String)).toEqual([mentionedId]);
    expect(result.mentionedUsers[0]).toMatchObject({ id: mentionedId, username: 'artist' });

    const tooMany = Array.from({ length: 31 }, (_, index) => `user${index}`);
    await expect(
      (ctx.service as any).resolveMentionTargets(tooMany, undefined, '', userId),
    ).rejects.toThrow('up to 30 users');
  });

  it('normalizes locations and image captions with fallback values', () => {
    const { service } = createService();
    const internals = service as any;
    expect(internals.normalizeLocation({ location: ' Riyadh ', latitude: 24.7, longitude: 46.6 })).toEqual({
      location: 'Riyadh',
      latitude: 24.7,
      longitude: 46.6,
    });
    expect(() => internals.normalizeLocation({ latitude: 24.7 })).toThrow(
      'latitude and longitude must be provided together',
    );
    expect(
      internals.buildImageItems(
        ['one', 'two'],
        [' first '],
        [undefined, ' second alt '],
        [{ url: 'old', caption: 'old caption', altText: 'old alt', order: 0 }],
      ),
    ).toEqual([
      { url: 'one', caption: 'first', altText: 'old alt', order: 0 },
      { url: 'two', caption: '', altText: 'second alt', order: 1 },
    ]);
  });

  it('validates tagged users and collaborator lists against the database', async () => {
    const firstId = '507f191e810c19729de860eb';
    const secondId = '507f191e810c19729de860ec';
    const ctx = createService();
    ctx.usersRepository.findMany.mockResolvedValue([{ id: firstId }, { id: secondId }]);
    await expect(
      (ctx.service as any).normalizeTaggedUserIds([firstId, firstId, userId, secondId], userId),
    ).resolves.toEqual([new Types.ObjectId(firstId), new Types.ObjectId(secondId)]);
    await expect(
      (ctx.service as any).normalizeUserIdList([firstId, secondId], userId, 2, 'collaborator'),
    ).resolves.toHaveLength(2);
    await expect((ctx.service as any).normalizeTaggedUserIds(['bad'], userId)).rejects.toThrow(
      'Invalid tagged user id',
    );
    await expect(
      (ctx.service as any).normalizeUserIdList([firstId, secondId], userId, 1, 'collaborator'),
    ).rejects.toThrow('up to 1 collaborator');
    ctx.usersRepository.findMany.mockResolvedValue([]);
    await expect((ctx.service as any).normalizeTaggedUserIds([firstId], userId)).rejects.toThrow(
      'tagged users do not exist',
    );
  });

  it('isolates mention notification failures from successful post operations', async () => {
    const ctx = createService();
    ctx.notificationsService.createMentionNotification.mockRejectedValue('offline');
    await expect(
      (ctx.service as any).notifyMentionedUsers(
        userId,
        postId,
        [{ id: ownerId, username: 'owner' }],
        'preview',
      ),
    ).resolves.toBeUndefined();
    await expect((ctx.service as any).notifyMentionedUsers(userId, postId, [], '')).resolves.toBeUndefined();
  });

  it('validates and stores individual media files in their correct folders', async () => {
    const ctx = createService();
    const tinyImage = { mimetype: 'image/jpeg', size: 8, buffer: Buffer.alloc(8), originalname: 'x.jpg' };
    const tinyAudio = { mimetype: 'audio/mpeg', size: 8, buffer: Buffer.alloc(8), originalname: 'x.mp3' };
    expect((ctx.service as any).validateMediaFile('image', tinyImage)).toBe('.jpg');
    expect(() => (ctx.service as any).validateMediaFile('image', file('text/plain'))).toThrow(
      'imageFiles must be',
    );
    await (ctx.service as any).saveMediaFile('audio', tinyAudio);
    expect(ctx.storageService.saveFile).toHaveBeenCalledWith({
      folderSegments: ['posts', 'audio'],
      extension: '.mp3',
      buffer: tinyAudio.buffer,
      contentType: 'audio/mpeg',
      fileNamePrefix: 'audio',
    });
  });

  it('saves responsive image variants and chooses resilient primary fallbacks', async () => {
    const ctx = createService();
    const tinyImage = { mimetype: 'image/jpeg', size: 8, buffer: Buffer.alloc(8), originalname: 'x.jpg' };
    const processed = {
      primaryVariantName: 'medium',
      variants: [
        { name: 'original', relativePath: 'original.jpg', buffer: Buffer.from('o'), contentType: 'image/jpeg' },
        { name: 'low', relativePath: 'low.jpg', buffer: Buffer.from('l'), contentType: 'image/jpeg' },
        { name: 'high', relativePath: 'high.jpg', buffer: Buffer.from('h'), contentType: 'image/jpeg' },
      ],
    };
    ctx.imageProcessingService.processForResponsiveDelivery.mockResolvedValue(processed);
    ctx.storageService.saveFiles.mockResolvedValue({
      'original.jpg': 'original-url',
      'low.jpg': 'low-url',
      'high.jpg': 'high-url',
    });
    await expect((ctx.service as any).saveResponsiveImageAsset('images', tinyImage)).resolves.toEqual({
      primaryUrl: 'high-url',
      variants: {
        originalUrl: 'original-url',
        lowUrl: 'low-url',
        mediumUrl: 'high-url',
        highUrl: 'high-url',
      },
    });
    expect((ctx.service as any).resolvePrimaryVariantUrl({
      originalUrl: '', lowUrl: 'low', mediumUrl: '', highUrl: 'high',
    }, 'original')).toBe('high');
  });

  it('cleans partial image batches and variant groups safely', async () => {
    const ctx = createService();
    const first = {
      primaryUrl: 'primary',
      variants: { originalUrl: 'primary', lowUrl: 'low', mediumUrl: 'medium', highUrl: 'high' },
    };
    jest
      .spyOn(ctx.service as any, 'saveResponsiveImageAsset')
      .mockResolvedValueOnce(first)
      .mockRejectedValueOnce(new Error('processing failed'));
    const cleanup = jest.spyOn(ctx.service as any, 'deleteSavedImageAsset');
    await expect(
      (ctx.service as any).saveImageFiles([
        { mimetype: 'image/jpeg', size: 1, buffer: Buffer.alloc(1) },
        { mimetype: 'image/jpeg', size: 1, buffer: Buffer.alloc(1) },
      ]),
    ).rejects.toThrow('processing failed');
    expect(cleanup).toHaveBeenCalledWith(first);

    await (ctx.service as any).deleteSavedImageAsset(first);
    expect(ctx.storageService.deleteContainingDirectory).toHaveBeenCalledWith('medium');
    await (ctx.service as any).deleteThumbnailAsset('thumbnail', first.variants);
    expect(ctx.storageService.deleteContainingDirectory).toHaveBeenCalledWith('medium');
  });

  it('creates video uploads with HLS and generated thumbnail metadata', async () => {
    const ctx = createService();
    const tinyVideo = { mimetype: 'video/mp4', size: 8, buffer: Buffer.alloc(8), originalname: 'x.mp4' };
    ctx.videoProcessingService.optimizeForPlayback.mockResolvedValue({
      file: tinyVideo,
      generatedHls: {
        files: [{ relativePath: 'master.m3u8', buffer: Buffer.from('hls'), contentType: 'application/x-mpegURL' }],
        playlistRelativePath: 'master.m3u8',
      },
      generatedThumbnail: {
        buffer: Buffer.alloc(8),
        contentType: 'image/jpeg',
        extension: '.jpg',
      },
    });
    ctx.storageService.saveFile.mockResolvedValue('video-url');
    ctx.storageService.saveFiles.mockResolvedValue({ 'master.m3u8': 'hls-url' });
    jest.spyOn(ctx.service as any, 'saveResponsiveImageAsset').mockResolvedValue({
      primaryUrl: 'thumb-url',
      variants: { originalUrl: 'thumb-url', lowUrl: 'low', mediumUrl: 'medium', highUrl: 'high' },
    });
    await expect((ctx.service as any).saveVideoUpload(tinyVideo)).resolves.toEqual({
      videoUrl: 'video-url',
      hlsUrl: 'hls-url',
      thumbnailUrl: 'thumb-url',
      thumbnailVariants: { originalUrl: 'thumb-url', lowUrl: 'low', mediumUrl: 'medium', highUrl: 'high' },
      durationSeconds: 12,
    });
  });

  it('builds waveforms from provided, buffered, fallback and seeded sources', () => {
    const { service } = createService();
    const internals = service as any;
    expect(internals.resolveAudioWaveformPeaks([0, 1], undefined, undefined, {}, 2).waveformPeaks.length).toBeGreaterThan(0);
    expect(internals.resolveAudioWaveformPeaks(undefined, Buffer.from([0, 1, 2]), undefined, {}, 2).waveformPeaks.length).toBeGreaterThan(0);
    expect(
      internals.resolveAudioWaveformPeaks(undefined, undefined, undefined, { waveformPeaksDetailed: [0, 1] }, 2)
        .waveformPeaks.length,
    ).toBeGreaterThan(0);
    expect(
      internals.resolveAudioWaveformPeaks(undefined, undefined, 'seed', {}, 2).waveformPeaks.length,
    ).toBeGreaterThan(0);
  });

  it('creates quote and plain reposts while protecting empty self-reposts', async () => {
    const missing = createService();
    await expect(missing.service.createRepost(userId, postId, {})).rejects.toThrow(
      'Source post not found',
    );

    const self = createService();
    self.postsRepository.findById.mockResolvedValue(post({ authorId: new Types.ObjectId(userId) }));
    await expect(self.service.createRepost(userId, postId, {})).rejects.toThrow(
      'cannot repost your own post without a quote',
    );

    const quote = createService();
    quote.postsRepository.findById
      .mockResolvedValueOnce(post())
      .mockResolvedValueOnce({ id: 'quote-populated' });
    quote.postsRepository.create.mockResolvedValue({ id: '507f191e810c19729de860ed' });
    await expect(
      quote.service.createRepost(userId, postId, {
        content: '  Great #OUD  ',
        visibility: PostVisibility.FOLLOWERS,
      }),
    ).resolves.toEqual({ id: 'quote-populated' });
    expect(quote.postsRepository.create).toHaveBeenCalledWith(
      userId,
      expect.objectContaining({
        content: 'Great #OUD',
        repostOfPostId: null,
        quoteOfPostId: new Types.ObjectId(postId),
        hashtags: ['oud'],
        visibility: PostVisibility.FOLLOWERS,
      }),
    );
    expect(quote.postsRepository.incrementShareCount).toHaveBeenCalledWith(postId, 1);

    const plain = createService();
    plain.postsRepository.findById.mockResolvedValue(post());
    plain.postsRepository.create.mockResolvedValue({ id: '507f191e810c19729de860ee' });
    await plain.service.createRepost(userId, postId, {});
    expect(plain.postsRepository.create).toHaveBeenCalledWith(
      userId,
      expect.objectContaining({
        repostOfPostId: new Types.ObjectId(postId),
        quoteOfPostId: null,
      }),
    );
  });

  it('rejects reposting a source that the viewer cannot access', async () => {
    const ctx = createService();
    ctx.postsRepository.findById.mockResolvedValue(
      post({ visibility: PostVisibility.PUBLIC }),
    );
    ctx.usersRepository.findById.mockResolvedValue({
      id: ownerId,
      isDisabled: false,
      isPrivate: true,
    });
    jest.spyOn(ctx.service as any, 'hasBlockBetween').mockResolvedValue(false);
    jest.spyOn(ctx.service as any, 'isFollowing').mockResolvedValue(false);

    await expect(ctx.service.createRepost(userId, postId, {})).rejects.toThrow(
      'Source post not found',
    );
    expect(ctx.postsRepository.create).not.toHaveBeenCalled();
  });

  it('enforces every share-availability privacy boundary', async () => {
    const invalid = createService();
    await expect((invalid.service as any).assertPostAvailableToViewer(userId, 'bad')).rejects.toThrow(
      'Invalid post id',
    );

    const hidden = createService();
    hidden.postsRepository.findById.mockResolvedValue(post({ isArchived: true }));
    await expect((hidden.service as any).assertPostAvailableToViewer(userId, postId)).rejects.toThrow(
      'Post not found',
    );

    const noAuthor = createService();
    noAuthor.postsRepository.findById.mockResolvedValue(post({ authorId: null }));
    await expect((noAuthor.service as any).assertPostAvailableToViewer(userId, postId)).rejects.toThrow(
      'Post not found',
    );

    const disabled = createService();
    disabled.postsRepository.findById.mockResolvedValue(post());
    disabled.usersRepository.findById.mockResolvedValue({ id: ownerId, isDisabled: true });
    await expect((disabled.service as any).assertPostAvailableToViewer(userId, postId)).rejects.toThrow(
      'Post not found',
    );

    const owner = createService();
    owner.postsRepository.findById.mockResolvedValue(post({ authorId: new Types.ObjectId(userId) }));
    owner.usersRepository.findById.mockResolvedValue({ id: userId, isDisabled: false });
    await expect((owner.service as any).assertPostAvailableToViewer(userId, postId)).resolves.toBeDefined();

    const blocked = createService();
    blocked.postsRepository.findById.mockResolvedValue(post());
    blocked.usersRepository.findById.mockResolvedValue({ id: ownerId, isDisabled: false });
    jest.spyOn(blocked.service as any, 'hasBlockBetween').mockResolvedValue(true);
    await expect((blocked.service as any).assertPostAvailableToViewer(userId, postId)).rejects.toThrow(
      'cannot access this post',
    );

    const privatePost = createService();
    privatePost.postsRepository.findById.mockResolvedValue(post({ visibility: PostVisibility.PRIVATE }));
    privatePost.usersRepository.findById.mockResolvedValue({ id: ownerId, isDisabled: false });
    jest.spyOn(privatePost.service as any, 'hasBlockBetween').mockResolvedValue(false);
    await expect((privatePost.service as any).assertPostAvailableToViewer(userId, postId)).rejects.toThrow(
      'cannot access this post',
    );

    const followerPost = createService();
    followerPost.postsRepository.findById.mockResolvedValue(
      post({ visibility: PostVisibility.FOLLOWERS }),
    );
    followerPost.usersRepository.findById.mockResolvedValue({ id: ownerId, isDisabled: false });
    jest.spyOn(followerPost.service as any, 'hasBlockBetween').mockResolvedValue(false);
    jest.spyOn(followerPost.service as any, 'isFollowing').mockResolvedValue(false);
    await expect(
      (followerPost.service as any).assertPostAvailableToViewer(userId, postId),
    ).rejects.toThrow('cannot access this post');
    (followerPost.service as any).isFollowing.mockResolvedValue(true);
    await expect(
      (followerPost.service as any).assertPostAvailableToViewer(userId, postId),
    ).resolves.toBeDefined();
  });

  it('filters suggested share friends and formats stable display names', async () => {
    const firstId = '507f191e810c19729de860eb';
    const blockedId = '507f191e810c19729de860ec';
    const ctx = createService();
    ctx.blocksRepository.findBlockingOrBlockedIds.mockResolvedValue([blockedId]);
    ctx.followsRepository.findMany.mockResolvedValue([
      {
        followingId: {
          _id: new Types.ObjectId(firstId),
          stageName: ' Star ',
          username: 'first',
          avatar: '/uploads/a.jpg',
          isVerified: true,
        },
      },
      { followingId: { _id: new Types.ObjectId(blockedId), username: 'blocked' } },
      { followingId: { _id: new Types.ObjectId(firstId), username: 'duplicate' } },
      { followingId: null },
    ]);
    const result = await (ctx.service as any).findSuggestedShareFriends(userId, 10);
    expect(result).toEqual([
      expect.objectContaining({
        id: firstId,
        displayName: 'Star',
        username: 'first',
        isVerified: true,
        isFollowing: true,
      }),
    ]);
    expect((ctx.service as any).uniqueById([{ id: '' }, { id: firstId }, { id: firstId }])).toEqual([
      { id: firstId },
    ]);
  });

  it('matches shared profile posts against media, text and hashtag filters', () => {
    const { service } = createService();
    const candidate = post({
      postType: PostType.VIDEO,
      content: 'legacy',
      contentTop: 'Top Oud',
      contentBottom: 'Bottom',
      hashtags: ['oud'],
    });
    expect((service as any).postMatchesProfileQuery(candidate, { postType: 'reel' })).toBe(true);
    expect((service as any).postMatchesProfileQuery(candidate, { postType: PostType.AUDIO })).toBe(false);
    expect((service as any).postMatchesProfileQuery(candidate, { q: 'top oud' })).toBe(true);
    expect((service as any).postMatchesProfileQuery(candidate, { q: 'missing' })).toBe(false);
    expect((service as any).postMatchesProfileQuery(candidate, { hashtag: '#OUD' })).toBe(true);
    expect((service as any).postMatchesProfileQuery(candidate, { hashtag: '#other' })).toBe(false);
  });

  it('recovers a concurrent duplicate profile share and propagates unrecoverable failures', async () => {
    const duplicate = {
      _id: new Types.ObjectId(),
      userId: new Types.ObjectId(userId),
      postId: new Types.ObjectId(postId),
    };
    const ctx = createService();
    ctx.collectionMock.findOne.mockResolvedValueOnce(null).mockResolvedValueOnce(duplicate);
    ctx.collectionMock.insertOne.mockRejectedValue({ code: 11000 });
    await expect(
      (ctx.service as any).createProfileShareRecord(userId, postId, post(), {}),
    ).resolves.toEqual({ share: duplicate, created: false });

    const failure = createService();
    failure.collectionMock.findOne.mockResolvedValue(null);
    failure.collectionMock.insertOne.mockRejectedValue(new Error('storage down'));
    await expect(
      (failure.service as any).createProfileShareRecord(userId, postId, post(), {}),
    ).rejects.toThrow('storage down');
  });

});

describe('Post schema response aliases', () => {
  it('returns isPinned as a stable alias for pinnedToProfile', () => {
    const transform = PostSchema.get('toObject')?.transform as (
      doc: unknown,
      ret: Record<string, any>,
    ) => Record<string, any>;
    const ret = transform(null, {
      postType: 'text',
      pinnedToProfile: true,
      waveformPeaks: [],
      mentionedUserIds: [],
    });

    expect(ret.pinnedToProfile).toBe(true);
    expect(ret.isPinned).toBe(true);
  });
});
