import { BadRequestException, NotFoundException } from '@nestjs/common';
import { Types } from 'mongoose';
import { ModerationStatus } from '../../common/enums/moderation-status.enum';
import { SuperAdminService } from './superadmin.service';

describe('SuperAdminService', () => {
  const resourceId = '507f1f77bcf86cd799439011';
  const authorId = new Types.ObjectId('507f191e810c19729de860ea');
  const now = new Date();

  function query<T>(value: T) {
    const chain: any = { exec: jest.fn().mockResolvedValue(value) };
    for (const method of ['sort', 'limit', 'select', 'lean', 'populate', 'skip']) {
      chain[method] = jest.fn(() => chain);
    }
    return chain;
  }

  function model(rows: any[], single: any) {
    return {
      countDocuments: jest.fn(() => query(2)),
      find: jest.fn(() => query(rows)),
      findOne: jest.fn(() => query(single)),
      findById: jest.fn(() => query(single)),
      findByIdAndUpdate: jest.fn(() => query(single)),
      findByIdAndDelete: jest.fn(() => query(single)),
      findOneAndUpdate: jest.fn(() => query(single)),
      aggregate: jest.fn(() => query([{ _id: 'unknown', value: 1, count: 1 }])),
      create: jest.fn().mockResolvedValue(single),
    };
  }

  const setup = () => {
    const user = {
      _id: authorId,
      id: authorId.toHexString(),
      name: 'Member',
      username: 'member',
      email: 'member@example.com',
      role: 'user',
      isDisabled: false,
      createdAt: now,
    };
    const post = {
      _id: new Types.ObjectId(resourceId),
      id: resourceId,
      authorId,
      content: 'A useful post',
      postType: 'text',
      moderationStatus: ModerationStatus.ACTIVE,
      isDeleted: false,
      createdAt: now,
      save: jest.fn().mockResolvedValue(undefined),
    };
    const comment = {
      _id: new Types.ObjectId(resourceId),
      id: resourceId,
      authorId,
      postId: new Types.ObjectId(resourceId),
      content: 'A useful comment',
      moderationStatus: ModerationStatus.ACTIVE,
      isDeleted: false,
      createdAt: now,
      save: jest.fn().mockResolvedValue(undefined),
    };
    const listing = {
      _id: new Types.ObjectId(resourceId),
      id: resourceId,
      title: 'Guitar',
      listingCategory: 'musical_instrument',
      isActive: true,
      imageUrls: ['/uploads/listing.jpg'],
      ownerAdminId: { name: 'Shop owner', shopName: 'Music Shop' },
      createdAt: now,
    };
    const repairShop = {
      _id: new Types.ObjectId(resourceId),
      id: resourceId,
      name: 'Repair Lab',
      location: 'Riyadh',
      isActive: true,
      imageUrls: ['/uploads/repair.jpg'],
      ownerAdminId: { name: 'Shop owner', shopName: 'Music Shop' },
      createdAt: now,
    };
    const notification = { read: false, createdAt: now };
    const auditRow = {
      actorType: 'superadmin',
      actorIdentifier: 'root@example.com',
      action: 'post_hide',
      targetType: 'post',
      targetId: resourceId,
      createdAt: now,
    };
    const caseRow = {
      id: resourceId,
      title: 'Review case',
      description: 'Needs review',
      caseType: 'content_moderation',
      resourceType: 'post',
      resourceId,
      status: 'open',
      priority: 'high',
      assignedTo: '',
      createdBy: 'root@example.com',
      updatedBy: 'root@example.com',
      resolution: '',
      events: [] as any[],
      createdAt: now,
      updatedAt: now,
      save: jest.fn().mockResolvedValue(undefined),
    };
    const history = {
      id: resourceId,
      scope: 'default',
      updatedBy: 'root@example.com',
      changedFields: ['siteName'],
      previousSettings: { siteName: 'Old' },
      nextSettings: { siteName: 'New' },
      createdAt: now,
    };

    const userModel = model([user], user);
    const postModel = model([post], post);
    const commentModel = model([comment], comment);
    const instrumentModel = model([listing], listing);
    const repairShopModel = model([repairShop], repairShop);
    const notificationModel = model([notification], notification);
    const auditLogModel = model([auditRow], auditRow);
    const settingsModel = model([{ siteName: 'Stored' }], { siteName: 'Stored', scope: 'default' });
    const settingsHistoryModel = model([history], history);
    const superAdminCaseModel = model([caseRow], caseRow);
    superAdminCaseModel.findOne.mockImplementation(() => query(null));
    const outboxEventModel = model([], null);
    const refreshTokenModel = model([], null);
    const connection = { readyState: 1, name: 'oudelaa-test' };
    const configValues: Record<string, unknown> = {
      nodeEnv: 'test',
      host: '127.0.0.1',
      port: 3000,
      globalPrefix: 'api/v1',
      publicBaseUrl: 'https://api.example.com',
      responseEnvelopeEnabled: true,
      'email.enabled': true,
      'cors.origins': ['https://app.example.com'],
      'swagger.path': 'docs',
      'storage.provider': 'local',
      'storage.basePath': 'uploads',
      'queue.enabled': true,
      'queue.name': 'jobs',
      'redis.enabled': true,
      'redis.socketAdapterEnabled': true,
      'superAdmin.email': 'root@example.com',
    };
    const config = { get: jest.fn((key: string) => configValues[key]) };
    const audit = { logSuperAdminAction: jest.fn().mockResolvedValue(undefined) };
    const postsService = {
      updateModerationStatusBySuperAdmin: jest.fn().mockResolvedValue(post),
      removeBySuperAdmin: jest.fn().mockResolvedValue(undefined),
    };
    const commentsService = {
      updateModerationStatusBySuperAdmin: jest.fn().mockResolvedValue(comment),
      removeBySuperAdmin: jest.fn().mockResolvedValue(undefined),
    };
    const usersService = {
      disableUserBySuperAdmin: jest.fn().mockResolvedValue(user),
      enableUserBySuperAdmin: jest.fn().mockResolvedValue(user),
    };
    const redisClient = { ping: jest.fn().mockResolvedValue('PONG') };
    const redis = {
      isEnabled: jest.fn().mockReturnValue(true),
      getClient: jest.fn().mockReturnValue(redisClient),
    };
    const storage = { deleteFile: jest.fn().mockResolvedValue(undefined) };
    const feedVersion = { bumpGlobalVersion: jest.fn().mockResolvedValue(2) };

    const service = new SuperAdminService(
      connection as any,
      userModel as any,
      postModel as any,
      commentModel as any,
      instrumentModel as any,
      repairShopModel as any,
      notificationModel as any,
      auditLogModel as any,
      settingsModel as any,
      settingsHistoryModel as any,
      superAdminCaseModel as any,
      outboxEventModel as any,
      refreshTokenModel as any,
      config as any,
      audit as any,
      postsService as any,
      commentsService as any,
      usersService as any,
      redis as any,
      storage as any,
      feedVersion as any,
    );

    return {
      service,
      connection,
      userModel,
      postModel,
      commentModel,
      instrumentModel,
      repairShopModel,
      settingsModel,
      settingsHistoryModel,
      superAdminCaseModel,
      audit,
      postsService,
      commentsService,
      usersService,
      redis,
      redisClient,
      storage,
      feedVersion,
      post,
      comment,
      user,
      listing,
      repairShop,
      caseRow,
      history,
    };
  };

  it('returns complete overview, chart, report, activity, and session projections', async () => {
    const { service } = setup();

    await expect(service.getOverview()).resolves.toEqual(
      expect.objectContaining({ metrics: expect.objectContaining({ usersCount: 2, generalMarketplaceListingsCount: 0 }) }),
    );
    await expect(service.getCharts({ range: '7d' } as any)).resolves.toEqual(
      expect.objectContaining({ range: '7d', days: 7, series: expect.any(Object), breakdowns: expect.any(Object) }),
    );
    await expect(service.getCharts({ range: '90d' } as any)).resolves.toEqual(expect.objectContaining({ days: 90 }));
    await expect(service.getRecentActivity({ limit: 20 } as any)).resolves.toEqual({
      items: expect.arrayContaining([expect.objectContaining({ type: 'user' }), expect.objectContaining({ type: 'post' })]),
    });
    await expect(service.getReports({ limit: 4 } as any)).resolves.toEqual(
      expect.objectContaining({ summary: expect.objectContaining({ flaggedPostsCount: 2 }) }),
    );
    expect(service.getSession({ email: 'owner@example.com', permissions: ['users.read'] })).toEqual(
      expect.objectContaining({ superAdmin: { email: 'owner@example.com' }, permissions: ['users.read'] }),
    );
    expect(service.getSession({})).toEqual(expect.objectContaining({ superAdmin: { email: 'root@example.com' } }));
  });

  it('lists, creates, and updates investigation cases with normalized metadata', async () => {
    const { service, superAdminCaseModel, audit, caseRow } = setup();
    await service.getCases({
      q: '  guitar.*  ',
      status: 'open',
      priority: 'high',
      resourceType: ' listing ',
      assignedTo: ' root ',
      page: 2,
      limit: 5,
      sortOrder: 'asc',
    } as any);
    await service.createCase('root@example.com', {
      title: '  Review listing  ',
      description: '  suspicious  ',
      caseType: ' marketplace_review ',
      resourceType: ' listing ',
      resourceId: ` ${resourceId} `,
      assignedTo: ' root@example.com ',
      note: '  created  ',
    } as any);

    superAdminCaseModel.findById.mockReturnValueOnce(query(caseRow));
    superAdminCaseModel.findByIdAndUpdate.mockReturnValueOnce(query({ ...caseRow, status: 'resolved' }));
    await service.updateCase('root@example.com', resourceId, {
      title: '  Done  ',
      description: '  Complete  ',
      caseType: ' content ',
      resourceType: ' post ',
      resourceId: ` ${resourceId} `,
      status: 'resolved',
      priority: 'normal',
      assignedTo: ' root ',
      tags: ['resolved'],
      note: '  fixed  ',
    } as any);

    expect(superAdminCaseModel.create).toHaveBeenCalledWith(expect.objectContaining({ title: 'Review listing' }));
    expect(audit.logSuperAdminAction).toHaveBeenCalled();
  });

  it('rejects updates for missing cases', async () => {
    const { service, superAdminCaseModel } = setup();
    superAdminCaseModel.findById.mockReturnValueOnce(query(null));
    await expect(service.updateCase('root', resourceId, {} as any)).rejects.toBeInstanceOf(NotFoundException);
    superAdminCaseModel.findByIdAndUpdate.mockReturnValueOnce(query(null));
    await expect(service.updateCase('root', resourceId, {} as any)).rejects.toBeInstanceOf(NotFoundException);
  });

  it('reads, updates, restores, and paginates settings with immutable history', async () => {
    const { service, settingsModel, settingsHistoryModel, audit, history } = setup();
    await expect(service.getSettings()).resolves.toEqual(
      expect.objectContaining({ settings: expect.objectContaining({ siteName: 'Stored' }), runtime: expect.any(Object) }),
    );
    await service.getSettingsHistory({ page: 2, limit: 5, sortOrder: 'asc' } as any);
    await service.updateSettings('root@example.com', { siteName: 'New name', maintenanceMode: true } as any);
    settingsHistoryModel.findById.mockReturnValueOnce(query(history));
    await service.restoreSettingsVersion('root@example.com', resourceId);

    expect(settingsModel.findOneAndUpdate).toHaveBeenCalled();
    expect(settingsHistoryModel.create).toHaveBeenCalledTimes(2);
    expect(audit.logSuperAdminAction).toHaveBeenCalled();
  });

  it('rejects restoring a missing settings version', async () => {
    const { service, settingsHistoryModel } = setup();
    settingsHistoryModel.findById.mockReturnValueOnce(query(null));
    await expect(service.restoreSettingsVersion('root', resourceId)).rejects.toBeInstanceOf(NotFoundException);
  });

  it('reports connected, degraded, unreachable, and disabled operational dependencies', async () => {
    const { service, redis, redisClient, connection } = setup();
    await expect(service.getOps()).resolves.toEqual(
      expect.objectContaining({ services: expect.objectContaining({ mongodb: { status: 'connected', database: 'oudelaa-test' } }) }),
    );
    redisClient.ping.mockResolvedValueOnce('NOPE');
    await expect(service.getOps()).resolves.toEqual(
      expect.objectContaining({ services: expect.objectContaining({ redis: { enabled: true, status: 'degraded' } }) }),
    );
    redisClient.ping.mockRejectedValueOnce(new Error('offline'));
    await expect(service.getOps()).resolves.toEqual(
      expect.objectContaining({ services: expect.objectContaining({ redis: { enabled: true, status: 'unreachable' } }) }),
    );
    redis.isEnabled.mockReturnValue(false);
    connection.readyState = 99;
    await expect(service.getOps()).resolves.toEqual(
      expect.objectContaining({ services: expect.objectContaining({ mongodb: { status: 'unknown', database: 'oudelaa-test' } }) }),
    );
  });

  it('moderates, deletes, and restores posts and comments while repairing counters', async () => {
    const { service, postModel, commentModel, post, comment, feedVersion } = setup();
    await service.updatePostStatus('root', resourceId, { status: ModerationStatus.FLAGGED, reason: '  review  ' } as any);
    await service.deletePost('root', resourceId);

    const deletedPost = { ...post, isDeleted: true, save: jest.fn().mockResolvedValue(undefined) };
    postModel.findOne.mockReturnValueOnce(query(deletedPost));
    await service.restorePost('root', resourceId);

    await service.updateCommentStatus('root', resourceId, { status: ModerationStatus.ACTIVE } as any);
    await service.deleteComment('root', resourceId);
    const deletedComment = { ...comment, isDeleted: true, save: jest.fn().mockResolvedValue(undefined) };
    commentModel.findOne.mockReturnValueOnce(query(deletedComment));
    await service.restoreComment('root', resourceId);

    expect(deletedPost.save).toHaveBeenCalled();
    expect(deletedComment.save).toHaveBeenCalled();
    expect(feedVersion.bumpGlobalVersion).toHaveBeenCalledTimes(2);
  });

  it('returns not found when deleted content cannot be located', async () => {
    const { service, postModel, commentModel } = setup();
    postModel.findById.mockReturnValueOnce(query(null));
    await expect(service.deletePost('root', resourceId)).rejects.toBeInstanceOf(NotFoundException);
    postModel.findOne.mockReturnValueOnce(query(null));
    await expect(service.restorePost('root', resourceId)).rejects.toBeInstanceOf(NotFoundException);
    commentModel.findById.mockReturnValueOnce(query(null));
    await expect(service.deleteComment('root', resourceId)).rejects.toBeInstanceOf(NotFoundException);
    commentModel.findOne.mockReturnValueOnce(query(null));
    await expect(service.restoreComment('root', resourceId)).rejects.toBeInstanceOf(NotFoundException);
  });

  it('disables and enables users and records review cases', async () => {
    const { service, usersService } = setup();
    await service.updateUserStatus('root', resourceId, { isDisabled: true, reason: ' abuse ' } as any);
    await service.updateUserStatus('root', resourceId, { isDisabled: false } as any);
    expect(usersService.disableUserBySuperAdmin).toHaveBeenCalled();
    expect(usersService.enableUserBySuperAdmin).toHaveBeenCalled();
  });

  it('dispatches all bulk content and user actions and rejects unsupported actions', async () => {
    const { service } = setup();
    const internals = service as any;
    jest.spyOn(service, 'updatePostStatus').mockResolvedValue({} as any);
    jest.spyOn(service, 'deletePost').mockResolvedValue({ success: true, postId: resourceId });
    jest.spyOn(service, 'restorePost').mockResolvedValue({} as any);
    jest.spyOn(service, 'updateCommentStatus').mockResolvedValue({} as any);
    jest.spyOn(service, 'deleteComment').mockResolvedValue({ success: true, commentId: resourceId });
    jest.spyOn(service, 'restoreComment').mockResolvedValue({} as any);
    jest.spyOn(service, 'updateUserStatus').mockResolvedValue({} as any);

    for (const action of ['activate', 'flag', 'hide', 'delete', 'restore']) {
      await internals.performPostBulkAction('root', resourceId, { action });
      await internals.performCommentBulkAction('root', resourceId, { action });
    }
    for (const action of ['disable', 'enable']) {
      await internals.performUserBulkAction('root', resourceId, { action });
    }
    await expect(internals.performPostBulkAction('root', resourceId, { action: 'bad' })).rejects.toBeInstanceOf(
      BadRequestException,
    );
    await expect(internals.performCommentBulkAction('root', resourceId, { action: 'bad' })).rejects.toBeInstanceOf(
      BadRequestException,
    );
    await expect(internals.performUserBulkAction('root', resourceId, { action: 'bad' })).rejects.toBeInstanceOf(
      BadRequestException,
    );
  });

  it('executes every marketplace bulk action and best-effort file cleanup', async () => {
    const { service, storage } = setup();
    const internals = service as any;
    for (const action of ['activate', 'deactivate', 'delete']) {
      await internals.performListingBulkAction('root', resourceId, { action, reason: ' policy ' });
      await internals.performRepairShopBulkAction('root', resourceId, { action, reason: ' policy ' });
    }
    storage.deleteFile.mockRejectedValueOnce(new Error('already gone'));
    await expect(internals.safeDeleteManagedUrl('/uploads/missing.jpg')).resolves.toBeUndefined();
    await expect(internals.performListingBulkAction('root', resourceId, { action: 'bad' })).rejects.toBeInstanceOf(
      BadRequestException,
    );
    await expect(internals.performRepairShopBulkAction('root', resourceId, { action: 'bad' })).rejects.toBeInstanceOf(
      BadRequestException,
    );
  });

  it('runs bulk dispatch across resource types and reports partial failures', async () => {
    const { service, audit } = setup();
    const internals = service as any;
    for (const resourceType of ['post', 'comment', 'user', 'listing', 'repair_shop']) {
      jest.spyOn(internals, `perform${resourceType === 'repair_shop' ? 'RepairShop' : resourceType[0].toUpperCase() + resourceType.slice(1)}BulkAction`).mockResolvedValueOnce(undefined);
      await internals.performSingleBulkAction('root', resourceType, resourceId, { action: 'activate' });
    }
    await expect(internals.performSingleBulkAction('root', 'unknown', resourceId, {})).rejects.toBeInstanceOf(
      BadRequestException,
    );

    jest.spyOn(internals, 'performSingleBulkAction').mockResolvedValueOnce(undefined).mockRejectedValueOnce(new Error('failed'));
    jest.spyOn(internals, 'applyBulkCaseMetadata').mockResolvedValue(undefined);
    const result = await service.performBulkAction('root', {
      resourceType: 'post',
      action: 'hide',
      targetIds: [resourceId, authorId.toHexString()],
    } as any);
    expect(result).toEqual(expect.objectContaining({ succeeded: 1, failed: 1 }));
    expect(audit.logSuperAdminAction).toHaveBeenCalled();
  });

  it('updates existing case records and optional assignment metadata', async () => {
    const { service, superAdminCaseModel, caseRow } = setup();
    const internals = service as any;
    superAdminCaseModel.findOne.mockReturnValue(query(caseRow));
    await internals.recordResourceCase({
      actor: 'root',
      caseType: 'content_moderation',
      resourceType: 'post',
      resourceId,
      title: 'Post moderation: title',
      description: 'review',
      action: 'post_flagged',
      note: 'review',
      status: 'in_review',
      priority: 'high',
      assignedTo: 'root',
    });
    await internals.applyBulkCaseMetadata('root', 'post', resourceId, {
      assignToMe: true,
      priority: 'critical',
      reason: ' urgent ',
    });
    await internals.applyBulkCaseMetadata('root', 'post', resourceId, {});
    superAdminCaseModel.findOne.mockReturnValueOnce(query(null));
    await internals.applyBulkCaseMetadata('root', 'post', resourceId, { assignToMe: true });
    expect(caseRow.save).toHaveBeenCalled();
    expect(caseRow.assignedTo).toBe('root');
  });

  it('covers private range, title, date, and default-setting helpers', () => {
    const { service } = setup();
    const internals = service as any;
    expect(internals.resolveRangeDays('30d')).toBe(30);
    expect(internals.resolveRangeDays('unexpected')).toBe(30);
    expect(internals.buildContentCaseTitle('Post', '   ')).toContain('untitled');
    expect(internals.createDateBuckets(3)).toHaveLength(3);
    expect(internals.formatDateKey(new Date('2026-01-02T10:00:00Z'))).toBe('2026-01-02');
    expect(internals.buildDefaultSettings()).toEqual(expect.objectContaining({ dashboardApiBaseUrl: 'https://api.example.com/api/v1' }));
    expect(internals.buildMusicalInstrumentFilter()).toEqual(expect.objectContaining({ $or: expect.any(Array) }));
  });
});
