import { BadRequestException, ForbiddenException, NotFoundException } from '@nestjs/common';
import { Types } from 'mongoose';
import { UserRole } from '../../common/enums/user-role.enum';
import { MarketplaceListingCategory } from './enums/marketplace-listing-category.enum';
import { MarketplaceListingCondition } from './enums/marketplace-listing-condition.enum';
import { MarketplaceService } from './marketplace.service';

describe('MarketplaceService', () => {
  const adminId = '507f1f77bcf86cd799439011';
  const otherAdminId = '507f191e810c19729de860ea';
  const listingId = '507f1f77bcf86cd799439012';
  const shopId = '507f1f77bcf86cd799439013';

  const setup = () => {
    const admin = {
      _id: new Types.ObjectId(adminId),
      id: adminId,
      name: 'Admin',
      username: 'admin',
      email: 'admin@example.com',
      role: UserRole.ADMIN,
      isDisabled: false,
      shopName: 'Oudelaa Music',
      shopDescription: 'Musical instruments',
      shopImageUrls: ['/uploads/shop-old.jpg'],
      shopLocation: 'Riyadh',
      shopLatitude: 24.7,
      shopLongitude: 46.6,
    };
    const listing = {
      _id: new Types.ObjectId(listingId),
      id: listingId,
      ownerAdminId: new Types.ObjectId(adminId),
      title: 'Guitar',
      description: 'Acoustic',
      price: 900,
      currency: 'SAR',
      imageUrls: ['/uploads/listing-old.jpg'],
      listingCategory: MarketplaceListingCategory.MUSICAL_INSTRUMENT,
      condition: MarketplaceListingCondition.USED,
      isActive: true,
    };
    const repairShop = {
      _id: new Types.ObjectId(shopId),
      id: shopId,
      ownerAdminId: new Types.ObjectId(adminId),
      name: 'Repair Lab',
      imageUrls: ['/uploads/repair-old.jpg'],
      isActive: true,
    };
    const repository = {
      create: jest.fn().mockResolvedValue(listing),
      findById: jest.fn().mockResolvedValue(listing),
      updateById: jest.fn().mockImplementation(async (_id, payload) => ({ ...listing, ...payload })),
      deleteById: jest.fn().mockResolvedValue(true),
      findManyPublic: jest.fn().mockResolvedValue([listing]),
      countPublic: jest.fn().mockResolvedValue(1),
      createRepairShop: jest.fn().mockResolvedValue(repairShop),
      findRepairShopById: jest.fn().mockResolvedValue(repairShop),
      findRepairShopByOwnerAdminId: jest.fn().mockResolvedValue(null),
      updateRepairShopById: jest.fn().mockImplementation(async (_id, payload) => ({ ...repairShop, ...payload })),
      deleteRepairShopById: jest.fn().mockResolvedValue(true),
      findManyRepairShopsPublic: jest.fn().mockResolvedValue([repairShop]),
      countRepairShopsPublic: jest.fn().mockResolvedValue(1),
    };
    const usersRepository = {
      findById: jest.fn().mockResolvedValue(admin),
      findMany: jest.fn().mockResolvedValue([admin]),
      updateById: jest.fn().mockImplementation(async (_id, payload) => ({ ...admin, ...payload })),
    };
    const storage = {
      saveFile: jest.fn().mockResolvedValue('/uploads/new.jpg'),
      deleteFile: jest.fn().mockResolvedValue(undefined),
    };
    const audit = { logSuperAdminAction: jest.fn().mockResolvedValue(undefined) };
    const existingCase = {
      status: 'open',
      priority: 'normal',
      title: '',
      description: '',
      resolution: '',
      updatedBy: '',
      events: [] as any[],
      save: jest.fn().mockResolvedValue(undefined),
    };
    const caseExec = jest.fn().mockResolvedValue(null);
    const caseSort = jest.fn().mockReturnValue({ exec: caseExec });
    const caseModel = {
      findOne: jest.fn().mockReturnValue({ sort: caseSort }),
      create: jest.fn().mockResolvedValue({}),
    };
    const service = new MarketplaceService(
      repository as any,
      usersRepository as any,
      storage as any,
      audit as any,
      caseModel as any,
    );
    return {
      service,
      repository,
      usersRepository,
      storage,
      audit,
      caseModel,
      caseExec,
      existingCase,
      admin,
      listing,
      repairShop,
    };
  };

  const image = {
    originalname: 'instrument.jpg',
    mimetype: 'image/jpeg',
    size: 3,
    buffer: Buffer.from([0xff, 0xd8, 0xff]),
  };

  it('creates listings with normalized defaults and managed uploads', async () => {
    const { service, repository, storage } = setup();

    await service.createListingByAdmin(adminId, { title: 'Guitar', price: 900, currency: 'sar' } as any, [image]);

    expect(storage.saveFile).toHaveBeenCalledWith(
      expect.objectContaining({ extension: '.jpg', contentType: 'image/jpeg', fileNamePrefix: 'listing' }),
    );
    expect(repository.create).toHaveBeenCalledWith(
      adminId,
      expect.objectContaining({
        currency: 'SAR',
        description: '',
        imageUrls: ['/uploads/new.jpg'],
        isActive: true,
        condition: MarketplaceListingCondition.USED,
        instrumentType: '',
        listingCategory: MarketplaceListingCategory.MUSICAL_INSTRUMENT,
      }),
    );
  });

  it('supports superadmin and legacy instrument creation entry points', async () => {
    const { service } = setup();
    const listingSpy = jest.spyOn(service, 'createListingByAdmin').mockResolvedValue({ id: listingId } as any);
    const instrumentSpy = jest.spyOn(service, 'createInstrumentByAdmin').mockResolvedValue({ id: listingId } as any);

    await service.createListingBySuperAdmin(adminId, { title: 'Guitar', price: 1 } as any);
    await service.createByAdmin(adminId, { title: 'Guitar', price: 1 } as any);
    await service.createInstrumentBySuperAdmin(adminId, { title: 'Guitar', price: 1 } as any);

    expect(listingSpy).toHaveBeenCalled();
    expect(instrumentSpy).toHaveBeenCalledTimes(2);
  });

  it('updates listing fields without deleting retained images when imageUrls is omitted', async () => {
    const { service, repository, storage } = setup();

    const result = await service.updateListingByAdmin(adminId, listingId, {
      currency: 'usd',
      instrumentType: '  guitar  ',
    } as any);

    expect(result).toEqual(expect.objectContaining({ currency: 'USD', instrumentType: 'guitar' }));
    expect(repository.updateById).toHaveBeenCalledWith(
      listingId,
      expect.not.objectContaining({ imageUrls: expect.anything() }),
    );
    expect(storage.deleteFile).not.toHaveBeenCalled();
  });

  it('replaces images and cleans only managed URLs removed by an update', async () => {
    const { service, storage } = setup();

    await service.updateListingByAdmin(adminId, listingId, { imageUrls: ['/uploads/kept.jpg'] } as any, [image]);

    expect(storage.deleteFile).toHaveBeenCalledWith('/uploads/listing-old.jpg');
  });

  it('protects listing ownership and missing resources', async () => {
    const { service, repository, listing } = setup();
    repository.findById.mockResolvedValueOnce(null);
    await expect(service.updateListingByAdmin(adminId, listingId, {} as any)).rejects.toBeInstanceOf(NotFoundException);

    repository.findById.mockResolvedValueOnce({ ...listing, ownerAdminId: new Types.ObjectId(otherAdminId) });
    await expect(service.updateListingByAdmin(adminId, listingId, {} as any)).rejects.toBeInstanceOf(ForbiddenException);

    repository.updateById.mockResolvedValueOnce(null);
    await expect(service.updateListingByAdmin(adminId, listingId, {}, [image])).rejects.toBeInstanceOf(NotFoundException);
  });

  it('updates and removes musical-instrument listings through compatibility methods', async () => {
    const { service, repository, storage } = setup();
    await service.updateInstrumentByAdmin(adminId, listingId, { title: 'New title' } as any);
    await service.updateByAdmin(adminId, listingId, { title: 'Legacy title' } as any);
    await service.removeInstrumentByAdmin(adminId, listingId);
    await service.removeByAdmin(adminId, listingId);
    await service.removeListingByAdmin(adminId, listingId);

    expect(repository.deleteById).toHaveBeenCalledTimes(3);
    expect(storage.deleteFile).toHaveBeenCalledWith('/uploads/listing-old.jpg');
  });

  it('builds public, owner, superadmin, and home listing views', async () => {
    const { service, repository } = setup();
    const detailedQuery = {
      q: 'gui.tar',
      page: 2,
      limit: 3,
      minPrice: 10,
      maxPrice: 2000,
      isActive: false,
      condition: MarketplaceListingCondition.USED,
      instrumentType: ' guitar ',
      listingCategory: MarketplaceListingCategory.ACCESSORY,
      sortBy: 'price',
      sortOrder: 'asc',
    } as any;

    await service.getMyListings(adminId, detailedQuery);
    await service.getMine(adminId, {} as any);
    await service.getPublicListings({} as any);
    await service.getPublic({} as any);
    await service.getListingsForSuperAdmin(detailedQuery);
    await service.getPublicInstruments({} as any);
    const home = await service.getHome({ onlyActive: false } as any);

    expect(repository.findManyPublic).toHaveBeenCalled();
    expect(home).toEqual(
      expect.objectContaining({
        categories: expect.any(Array),
        summary: expect.objectContaining({ activeListings: 1 }),
        sections: expect.any(Object),
      }),
    );
  });

  it('finds listings while hiding disabled owners and non-instrument categories', async () => {
    const { service, repository, listing } = setup();
    await expect(service.findById(listingId)).resolves.toEqual(expect.objectContaining({ id: listingId }));
    await expect(service.findInstrumentById(listingId)).resolves.toEqual(expect.objectContaining({ id: listingId }));

    repository.findById.mockResolvedValueOnce(null);
    await expect(service.findListingById(listingId)).rejects.toBeInstanceOf(NotFoundException);
    repository.findById.mockResolvedValueOnce({ ...listing, ownerAdminId: { isDisabled: true } });
    await expect(service.findListingById(listingId)).rejects.toBeInstanceOf(NotFoundException);
    repository.findById.mockResolvedValueOnce({ ...listing, listingCategory: MarketplaceListingCategory.ACCESSORY });
    await expect(service.findInstrumentById(listingId)).rejects.toBeInstanceOf(NotFoundException);
  });

  it('creates, updates, queries, and removes repair shops safely', async () => {
    const { service, repository, storage } = setup();
    await service.createRepairShop(
      adminId,
      { name: 'Repair Lab', latitude: 24.7, longitude: 46.6 } as any,
      [image],
    );
    await service.createRepairShopBySuperAdmin(adminId, { name: 'Repair Lab' } as any);
    await service.updateRepairShop(adminId, shopId, { name: 'Repair Lab 2' } as any);
    expect(storage.deleteFile).not.toHaveBeenCalled();
    await service.getMyRepairShops(adminId, { q: 'repair', sortOrder: 'asc' } as any);
    await service.getPublicRepairShops({} as any);
    await service.getRepairShopsForSuperAdmin({ isActive: false } as any);
    await service.findRepairShopById(shopId);
    await service.removeRepairShop(adminId, shopId);

    expect(repository.createRepairShop).toHaveBeenCalledWith(
      adminId,
      expect.objectContaining({ services: [], imageUrls: ['/uploads/new.jpg'], isActive: true }),
    );
    expect(repository.deleteRepairShopById).toHaveBeenCalledWith(shopId);
  });

  it('manages shop profiles and preserves images on text-only updates', async () => {
    const { service, usersRepository, storage } = setup();
    await service.updateMyShopProfile(adminId, { shopName: '  New Shop  ' } as any);
    expect(storage.deleteFile).not.toHaveBeenCalled();
    await service.updateShopProfileBySuperAdmin(adminId, { shopDescription: '  Updated  ' } as any);
    await service.getMyShopProfile(adminId);
    await service.getShopProfileByAdminId(adminId);

    expect(usersRepository.updateById).toHaveBeenCalledWith(
      adminId,
      expect.objectContaining({ shopName: 'New Shop' }),
    );
  });

  it('audits superadmin listing and repair-shop moderation and records cases', async () => {
    const { service, audit, caseModel, caseExec, existingCase } = setup();
    await service.updateListingStatusBySuperAdmin('root@example.com', listingId, {
      isActive: false,
      reason: '  policy  ',
    } as any);
    await service.removeListingBySuperAdmin('root@example.com', listingId);

    caseExec.mockResolvedValue(existingCase);
    await service.updateRepairShopStatusBySuperAdmin('root@example.com', shopId, {
      isActive: true,
    } as any);
    await service.removeRepairShopBySuperAdmin('root@example.com', shopId);

    expect(audit.logSuperAdminAction).toHaveBeenCalledTimes(4);
    expect(caseModel.create).toHaveBeenCalled();
    expect(existingCase.save).toHaveBeenCalled();
    expect(existingCase.events.length).toBeGreaterThan(0);
  });

  it('enforces roles, single-shop limits, upload limits, and coordinate pairs', async () => {
    const { service, usersRepository, repository, admin } = setup();
    usersRepository.findById.mockResolvedValueOnce(null);
    await expect(service.getMyListings(adminId, {} as any)).rejects.toBeInstanceOf(NotFoundException);
    usersRepository.findById.mockResolvedValueOnce({ ...admin, role: UserRole.USER });
    await expect(service.getMyListings(adminId, {} as any)).rejects.toBeInstanceOf(ForbiddenException);
    usersRepository.findById.mockResolvedValueOnce({ ...admin, isDisabled: true });
    await expect(service.createListingBySuperAdmin(adminId, { title: 'x', price: 1 } as any)).rejects.toBeInstanceOf(
      ForbiddenException,
    );

    repository.findRepairShopByOwnerAdminId.mockResolvedValueOnce({ id: shopId });
    await expect(service.createRepairShop(adminId, { name: 'x' } as any)).rejects.toBeInstanceOf(BadRequestException);
    await expect(
      service.createListingByAdmin(adminId, { title: 'x', price: 1, imageUrls: Array(6).fill('/x') } as any),
    ).rejects.toBeInstanceOf(BadRequestException);
    await expect(
      service.createRepairShop(adminId, { name: 'x', imageUrls: Array(9).fill('/x') } as any),
    ).rejects.toBeInstanceOf(BadRequestException);
    await expect(service.createRepairShop(adminId, { name: 'x', latitude: 2 } as any)).rejects.toBeInstanceOf(
      BadRequestException,
    );
  });

  it('requires a complete seller profile before listing', async () => {
    const { service, usersRepository, admin } = setup();
    usersRepository.findById.mockResolvedValueOnce({ ...admin, shopLocation: '' });
    await expect(service.createListingByAdmin(adminId, { title: 'x', price: 1 } as any)).rejects.toBeInstanceOf(
      BadRequestException,
    );
  });
});
