import { BadRequestException } from '@nestjs/common';
import { Types } from 'mongoose';
import { UsersService } from './users.service';

const chain = <T>(value: T) => ({
  sort: jest.fn().mockReturnThis(),
  limit: jest.fn().mockReturnThis(),
  project: jest.fn().mockReturnThis(),
  toArray: jest.fn().mockResolvedValue(value),
});

const createService = (options: {
  user?: Record<string, any> | null;
  postStats?: Record<string, number>;
  topContent?: Record<string, any>[];
  collaborationsCount?: number;
  newFollowersThisWeek?: number;
  newFollowersThisMonth?: number;
  recentActivity?: Record<string, any>[];
  publicWebUrl?: string;
  shareBaseUrl?: string;
} = {}) => {
  const userId = new Types.ObjectId().toString();
  const user = options.user ?? {
    _id: new Types.ObjectId(userId),
    name: 'Artist',
    username: 'artist_one',
    stageName: 'Stage',
    bio: 'Bio',
    avatar: '',
    coverImage: '',
    followersCount: 10,
    followingCount: 4,
    isVerified: true,
    isDisabled: false,
    preferredMood: '',
    get(field: string) {
      return (this as Record<string, unknown>)[field];
    },
    toObject() {
      return {
        _id: userId,
        name: this.name,
        username: this.username,
        stageName: this.stageName,
        bio: this.bio,
        avatar: this.avatar,
        coverImage: this.coverImage,
        followersCount: this.followersCount,
        followingCount: this.followingCount,
        isVerified: this.isVerified,
        preferredMood: this.preferredMood,
      };
    },
  };

  const postsAggregate = jest
    .fn()
    .mockReturnValueOnce({
      toArray: jest.fn().mockResolvedValue(options.postStats ? [options.postStats] : []),
    })
    .mockReturnValueOnce({
      toArray: jest.fn().mockResolvedValue(options.topContent ?? []),
    });

  const postsCollection = {
    aggregate: postsAggregate,
    countDocuments: jest.fn().mockResolvedValue(options.collaborationsCount ?? 0),
  };
  const followsCollection = {
    countDocuments: jest
      .fn()
      .mockResolvedValueOnce(options.newFollowersThisWeek ?? 0)
      .mockResolvedValueOnce(options.newFollowersThisMonth ?? 0),
  };
  const followRequestsCollection = {
    countDocuments: jest.fn().mockResolvedValue(0),
  };
  const blocksCollection = {
    countDocuments: jest.fn().mockResolvedValue(0),
  };
  const notificationsCollection = {
    find: jest.fn().mockReturnValue(chain(options.recentActivity ?? [])),
  };
  const connection = {
    collection: jest.fn((name: string) => {
      if (name === 'posts') {
        return postsCollection;
      }
      if (name === 'follows') {
        return followsCollection;
      }
      if (name === 'followrequests') {
        return followRequestsCollection;
      }
      if (name === 'blocks') {
        return blocksCollection;
      }
      if (name === 'notifications') {
        return notificationsCollection;
      }
      throw new Error(`Unexpected collection ${name}`);
    }),
  };
  const usersRepository = {
    findById: jest.fn().mockResolvedValue(user),
    findOne: jest.fn().mockResolvedValue(user),
    updateById: jest.fn().mockImplementation((_id: string, payload: Record<string, unknown>) =>
      Promise.resolve({
        ...user,
        ...payload,
      }),
    ),
  };
  const service = new UsersService(
    connection as any,
    usersRepository as any,
    { logSuperAdminAction: jest.fn() } as any,
    {
      get: jest.fn((key: string) => {
        if (key === 'shareBaseUrl') {
          return options.shareBaseUrl;
        }
        if (key === 'publicWebUrl') {
          return options.publicWebUrl;
        }
        return undefined;
      }),
    } as any,
    { saveFile: jest.fn(), deleteFile: jest.fn() } as any,
    {
      bumpGlobalVersion: jest.fn().mockResolvedValue(1),
      bumpUserVersion: jest.fn().mockResolvedValue(1),
    } as any,
  );

  return {
    service,
    userId,
    usersRepository,
    postsCollection,
    followsCollection,
    followRequestsCollection,
    blocksCollection,
  };
};

describe('UsersService preferredMood', () => {
  it('updates and returns preferredMood from music setup', async () => {
    const { service, userId, usersRepository } = createService();

    const result = await service.updateMusicSetup(userId, { preferredMood: 'طربي' });

    expect(usersRepository.updateById).toHaveBeenCalledWith(userId, { preferredMood: 'طربي' });
    expect((result as any).preferredMood).toBe('طربي');
  });
});

describe('UsersService artist dashboard', () => {
  it('returns zeros and safe empty arrays for an authenticated user with no posts', async () => {
    const { service, userId } = createService();

    const result = await service.getMyDashboard(userId);

    expect(result.profile._id).toBe(userId);
    expect(result.stats.postsCount).toBe(0);
    expect(result.stats.viewCount).toBe(0);
    expect(result.stats.playCount).toBe(0);
    expect(result.stats.listenCount).toBe(0);
    expect(result.stats.engagementRate).toBe(0);
    expect(result.stats.earnings).toBe(0);
    expect(result.stats.scorePercentage).toBe(0);
    expect(result.audience.followers).toBe(10);
    expect(result.audience.nonFollowers).toBe(0);
    expect(result.engagementChart).toEqual([]);
    expect(result.topContent).toEqual([]);
    expect(result.meta.chartAvailable).toBe(false);
    expect(result.meta.earningsAvailable).toBe(false);
  });

  it('aggregates owned posts and calculates engagement rate safely', async () => {
    const postId = new Types.ObjectId();
    const { service, userId } = createService({
      postStats: {
        postsCount: 2,
        viewCount: 100,
        playCount: 50,
        likesCount: 12,
        commentsCount: 3,
        savesCount: 2,
        sharesCount: 1,
      },
      topContent: [
        {
          _id: postId,
          postType: 'audio',
          content: 'Track',
          thumbnailUrl: '',
          audioUrl: '/uploads/posts/audios/audio.mp3',
          viewCount: 100,
          playCount: 50,
          likesCount: 12,
          commentsCount: 3,
          savesCount: 2,
          engagementScore: 18,
          createdAt: new Date('2026-06-08T00:00:00.000Z'),
        },
      ],
      collaborationsCount: 2,
      newFollowersThisWeek: 1,
      newFollowersThisMonth: 3,
    });

    const result = await service.getMyDashboard(userId);

    expect(result.stats.postsCount).toBe(2);
    expect(result.stats.viewCount).toBe(100);
    expect(result.stats.playCount).toBe(50);
    expect(result.stats.listenCount).toBe(50);
    expect(result.stats.likesCount).toBe(12);
    expect(result.stats.commentsCount).toBe(3);
    expect(result.stats.savesCount).toBe(2);
    expect(result.stats.sharesCount).toBe(1);
    expect(result.stats.collaborationsCount).toBe(2);
    expect(result.stats.engagementRate).toBe(12);
    expect(result.audience.newFollowersThisWeek).toBe(1);
    expect(result.audience.newFollowersThisMonth).toBe(3);
    expect(result.topContent).toHaveLength(1);
    expect(result.topContent[0]).toEqual(
      expect.objectContaining({
        _id: postId.toString(),
        postType: 'audio',
        content: 'Track',
        engagementScore: 18,
      }),
    );
  });

  it('rejects invalid current user ids', async () => {
    const { service } = createService();

    await expect(service.getMyDashboard('not-object-id')).rejects.toBeInstanceOf(
      BadRequestException,
    );
  });
});

describe('UsersService follow counts', () => {
  it('returns follower counts and viewer state for another profile', async () => {
    const viewerId = new Types.ObjectId().toString();
    const { service, userId, followsCollection, followRequestsCollection, blocksCollection } =
      createService();
    followsCollection.countDocuments = jest
      .fn()
      .mockResolvedValueOnce(23)
      .mockResolvedValueOnce(122)
      .mockResolvedValueOnce(1);
    followRequestsCollection.countDocuments = jest.fn().mockResolvedValueOnce(1);
    blocksCollection.countDocuments = jest.fn().mockResolvedValueOnce(0).mockResolvedValueOnce(0);

    const result = await service.getFollowCounts(userId, viewerId);

    expect(result).toEqual({
      userId,
      followersCount: 23,
      followingCount: 122,
      followRequestsCount: 0,
      isPrivate: false,
      viewerState: {
        isOwnProfile: false,
        isFollowing: true,
        hasPendingFollowRequest: true,
        isBlocked: false,
        isBlockedByUser: false,
      },
    });
  });

  it('returns pending follow requests count only for own profile', async () => {
    const { service, userId, followsCollection, followRequestsCollection } = createService();
    followsCollection.countDocuments = jest.fn().mockResolvedValueOnce(3).mockResolvedValueOnce(5);
    followRequestsCollection.countDocuments = jest.fn().mockResolvedValueOnce(7);

    const result = await service.getFollowCounts(userId, userId);

    expect(result.followRequestsCount).toBe(7);
    expect(result.viewerState).toMatchObject({
      isOwnProfile: true,
      isFollowing: false,
      hasPendingFollowRequest: false,
      isBlocked: false,
      isBlockedByUser: false,
    });
  });
});

describe('UsersService profile sharing', () => {
  it('returns profileShareUrl with username in profile overview', async () => {
    const { service, userId } = createService({ shareBaseUrl: 'https://oudelaa.com/' });

    const result = await service.getProfileOverview(userId, userId);

    expect(result.profileShareUrl).toBe('https://oudelaa.com/u/artist_one');
  });

  it('falls back to profile id when username is missing', async () => {
    const userId = new Types.ObjectId().toString();
    const { service } = createService({
      user: {
        _id: new Types.ObjectId(userId),
        name: 'Artist',
        username: '',
        followersCount: 0,
        followingCount: 0,
        postsCount: 0,
        isDisabled: false,
      },
      shareBaseUrl: 'https://oudelaa.com',
    });

    const result = await service.getProfileOverview(userId, userId);

    expect(result.profileShareUrl).toBe(`https://oudelaa.com/profile/${userId}`);
  });

  it('returns profile overview by username', async () => {
    const { service, userId, usersRepository } = createService({
      publicWebUrl: 'https://oudelaa.com',
    });

    const result = await service.getProfileOverviewByUsername('Artist_One', userId);

    expect(usersRepository.findOne).toHaveBeenCalledWith({ username: 'artist_one' });
    expect(usersRepository.findById).toHaveBeenCalledWith(userId);
    expect(result.profileShareUrl).toBe('https://oudelaa.com/u/artist_one');
  });

  it('throws not found for missing username', async () => {
    const { service, usersRepository, userId } = createService();
    usersRepository.findOne.mockResolvedValueOnce(null);

    await expect(service.getProfileOverviewByUsername('missing', userId)).rejects.toThrow(
      'User not found',
    );
  });
});
