import { ListingEntity } from '@modules/listings/domain/entities/listing.entity';
import { type IListingRepository } from '@modules/listings/domain/repositories/listing.repository';
import { Price } from '@modules/listings/domain/value-objects/price.vo';
import { ForbiddenException, NotFoundException } from '@modules/shared';
import { DeleteListingCommand } from '../commands/delete-listing/delete-listing.command';
import { DeleteListingHandler } from '../commands/delete-listing/delete-listing.handler';

function createListing(id = 'listing-1', sellerId = 'seller-1'): ListingEntity {
  const price = Price.create(2_000_000_000n).unwrap();
  const listing = ListingEntity.createNew(id, 'prop-1', sellerId, 'SALE', price, 80);
  listing.clearDomainEvents();
  return listing;
}

describe('DeleteListingHandler', () => {
  let handler: DeleteListingHandler;
  let mockListingRepo: { [K in keyof IListingRepository]: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    mockListingRepo = {
      findById: vi.fn(),
      findByIdWithProperty: vi.fn(),
      save: vi.fn(),
      update: vi.fn(),
      delete: vi.fn().mockResolvedValue(undefined),
      search: vi.fn(),
      findByStatus: vi.fn(),
      findBySellerId: vi.fn(),
    };

    handler = new DeleteListingHandler(mockListingRepo as unknown as IListingRepository);
  });

  it('deletes the listing when caller is the owner', async () => {
    const listing = createListing('listing-1', 'seller-1');
    mockListingRepo.findById.mockResolvedValue(listing);

    const command = new DeleteListingCommand('listing-1', 'seller-1');
    await handler.execute(command);

    expect(mockListingRepo.findById).toHaveBeenCalledWith('listing-1');
    expect(mockListingRepo.delete).toHaveBeenCalledTimes(1);
    expect(mockListingRepo.delete).toHaveBeenCalledWith('listing-1');
  });

  it('deletes the listing when caller is an admin (override)', async () => {
    const listing = createListing('listing-1', 'seller-1');
    mockListingRepo.findById.mockResolvedValue(listing);

    const command = new DeleteListingCommand('listing-1', 'admin-9', 'ADMIN');
    await handler.execute(command);

    expect(mockListingRepo.delete).toHaveBeenCalledTimes(1);
    expect(mockListingRepo.delete).toHaveBeenCalledWith('listing-1');
  });

  it('throws NotFoundException when listing does not exist', async () => {
    mockListingRepo.findById.mockResolvedValue(null);

    const command = new DeleteListingCommand('missing', 'seller-1');

    await expect(handler.execute(command)).rejects.toBeInstanceOf(NotFoundException);
    expect(mockListingRepo.delete).not.toHaveBeenCalled();
  });

  it('throws ForbiddenException when caller is neither owner nor admin', async () => {
    const listing = createListing('listing-1', 'seller-1');
    mockListingRepo.findById.mockResolvedValue(listing);

    const command = new DeleteListingCommand('listing-1', 'someone-else', 'USER');

    await expect(handler.execute(command)).rejects.toBeInstanceOf(ForbiddenException);
    await expect(handler.execute(command)).rejects.toThrow(/người bán hoặc quản trị viên/);
    expect(mockListingRepo.delete).not.toHaveBeenCalled();
  });

  it('throws ForbiddenException when userRole is undefined and caller is not the seller', async () => {
    const listing = createListing('listing-1', 'seller-1');
    mockListingRepo.findById.mockResolvedValue(listing);

    const command = new DeleteListingCommand('listing-1', 'guest');

    await expect(handler.execute(command)).rejects.toBeInstanceOf(ForbiddenException);
    expect(mockListingRepo.delete).not.toHaveBeenCalled();
  });

  it('propagates repository errors so the underlying $transaction rollback surfaces to the caller', async () => {
    const listing = createListing('listing-1', 'seller-1');
    mockListingRepo.findById.mockResolvedValue(listing);

    // PrismaListingRepository.delete wraps savedListing/inquiry/priceHistory/order/transaction/listing
    // deletes in a single prisma.$transaction([...]). If any step throws, the whole tx rolls back
    // and the error bubbles up — the handler must not swallow it.
    const txError = new Error('simulated tx step failure (rollback expected)');
    mockListingRepo.delete.mockRejectedValueOnce(txError);

    const command = new DeleteListingCommand('listing-1', 'seller-1');

    await expect(handler.execute(command)).rejects.toBe(txError);
    expect(mockListingRepo.delete).toHaveBeenCalledTimes(1);
  });

  it('only invokes the repository delete after authorization passes', async () => {
    const listing = createListing('listing-1', 'seller-1');
    mockListingRepo.findById.mockResolvedValue(listing);

    const order: string[] = [];
    mockListingRepo.findById.mockImplementationOnce(async () => {
      order.push('findById');
      return listing;
    });
    mockListingRepo.delete.mockImplementationOnce(async () => {
      order.push('delete');
    });

    await handler.execute(new DeleteListingCommand('listing-1', 'seller-1'));

    expect(order).toEqual(['findById', 'delete']);
  });
});
