import { describe, it, expect, beforeEach, vi } from 'vitest';
import { ValidationException } from '@modules/shared';
import { ListingEntity } from '../../domain/entities/listing.entity';
import { type IListingRepository } from '../../domain/repositories/listing.repository';
import { Price } from '../../domain/value-objects/price.vo';
import { BulkUpdateListingsCommand } from '../commands/bulk-update-listings/bulk-update-listings.command';
import {
  BULK_UPDATE_MAX_ITEMS,
  BulkUpdateListingsHandler,
} from '../commands/bulk-update-listings/bulk-update-listings.handler';

function makeListing(
  id: string,
  sellerId = 'seller-1',
  agentId: string | null = null,
  status: 'DRAFT' | 'PENDING_REVIEW' | 'ACTIVE' = 'ACTIVE',
): ListingEntity {
  const price = Price.create(2_000_000_000n).unwrap();
  const listing = ListingEntity.createNew(
    id,
    `prop-${id}`,
    sellerId,
    'SALE',
    price,
    80,
    agentId ?? undefined,
  );
  if (status === 'PENDING_REVIEW') listing.submitForReview();
  if (status === 'ACTIVE') {
    listing.submitForReview();
    listing.approve();
  }
  listing.clearDomainEvents();
  return listing;
}

describe('BulkUpdateListingsHandler', () => {
  let handler: BulkUpdateListingsHandler;
  let mockListingRepo: { [K in keyof IListingRepository]: ReturnType<typeof vi.fn> };
  let mockPrisma: { $transaction: ReturnType<typeof vi.fn>; listing: { update: ReturnType<typeof vi.fn> } };
  let mockEventBus: { publish: ReturnType<typeof vi.fn> };
  let mockCache: {
    invalidate: ReturnType<typeof vi.fn>;
    invalidateByPrefix: ReturnType<typeof vi.fn>;
  };
  let mockLogger: {
    error: ReturnType<typeof vi.fn>;
    warn: ReturnType<typeof vi.fn>;
    log: ReturnType<typeof vi.fn>;
  };

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

    mockPrisma = {
      $transaction: vi.fn().mockResolvedValue([]),
      listing: { update: vi.fn().mockReturnValue({ __op: 'update' }) },
    };

    mockEventBus = { publish: vi.fn() };
    mockCache = {
      invalidate: vi.fn().mockResolvedValue(undefined),
      invalidateByPrefix: vi.fn().mockResolvedValue(undefined),
    };
    mockLogger = { error: vi.fn(), warn: vi.fn(), log: vi.fn() };

    handler = new BulkUpdateListingsHandler(
      mockListingRepo as any,
      mockPrisma as any,
      mockEventBus as any,
      mockCache as any,
      mockLogger as any,
    );
  });

  // ─── Input validation ───────────────────────────────────────
  it('rejects empty id list', async () => {
    const cmd = new BulkUpdateListingsCommand([], { priceVND: 1n }, 'u', 'USER');
    await expect(handler.execute(cmd)).rejects.toThrow(ValidationException);
  });

  it(`rejects more than ${BULK_UPDATE_MAX_ITEMS} ids`, async () => {
    const ids = Array.from({ length: BULK_UPDATE_MAX_ITEMS + 1 }, (_, i) => `id-${i}`);
    const cmd = new BulkUpdateListingsCommand(ids, { priceVND: 1n }, 'u', 'USER');
    await expect(handler.execute(cmd)).rejects.toThrow(/Vượt quá giới hạn/);
  });

  it('rejects duplicate ids', async () => {
    const cmd = new BulkUpdateListingsCommand(['a', 'a'], { priceVND: 1n }, 'u', 'USER');
    await expect(handler.execute(cmd)).rejects.toThrow(/trùng lặp/);
  });

  it('rejects empty patch', async () => {
    const cmd = new BulkUpdateListingsCommand(['a'], {}, 'u', 'USER');
    await expect(handler.execute(cmd)).rejects.toThrow(/Patch trống/);
  });

  it('rejects featured=true without featuredDays', async () => {
    const cmd = new BulkUpdateListingsCommand(['a'], { featured: true }, 'u', 'USER');
    await expect(handler.execute(cmd)).rejects.toThrow(/featuredDays/);
  });

  // ─── Per-item ownership / not-found ────────────────────────
  it('rolls back when one listing is missing', async () => {
    mockListingRepo.findById.mockImplementation((id: string) =>
      id === 'l1' ? makeListing('l1', 'seller-1') : null,
    );

    const cmd = new BulkUpdateListingsCommand(
      ['l1', 'l2'],
      { priceVND: 3_000_000_000n },
      'seller-1',
      'USER',
    );
    const result = await handler.execute(cmd);

    expect(result.succeeded).toBe(false);
    expect(result.totalSucceeded).toBe(0);
    expect(result.items.find((i) => i.id === 'l2')?.reason).toBe('NOT_FOUND');
    // l1 was valid but rolled back
    expect(result.items.find((i) => i.id === 'l1')?.reason).toBe('ROLLED_BACK');
    expect(mockPrisma.$transaction).not.toHaveBeenCalled();
  });

  it('rolls back when caller is not owner/agent/admin for any listing', async () => {
    mockListingRepo.findById.mockImplementation((id: string) =>
      makeListing(id, id === 'l1' ? 'seller-1' : 'other'),
    );

    const cmd = new BulkUpdateListingsCommand(
      ['l1', 'l2'],
      { priceVND: 3_000_000_000n },
      'seller-1',
      'USER',
    );
    const result = await handler.execute(cmd);

    expect(result.succeeded).toBe(false);
    expect(result.items.find((i) => i.id === 'l2')?.reason).toBe('FORBIDDEN');
    expect(mockPrisma.$transaction).not.toHaveBeenCalled();
  });

  // ─── Happy path ────────────────────────────────────────────
  it('commits all updates atomically when every item passes', async () => {
    mockListingRepo.findById.mockImplementation((id: string) => makeListing(id, 'seller-1'));

    const cmd = new BulkUpdateListingsCommand(
      ['l1', 'l2'],
      { priceVND: 4_500_000_000n, featured: true, featuredDays: 7 },
      'seller-1',
      'USER',
    );
    const result = await handler.execute(cmd);

    expect(result.succeeded).toBe(true);
    expect(result.totalSucceeded).toBe(2);
    expect(result.totalFailed).toBe(0);
    expect(mockPrisma.$transaction).toHaveBeenCalledTimes(1);
    // 2 listing.update statements queued
    expect(mockPrisma.listing.update).toHaveBeenCalledTimes(2);
    // Cache invalidated for each + the search prefix
    expect(mockCache.invalidate).toHaveBeenCalledTimes(2);
    expect(mockCache.invalidateByPrefix).toHaveBeenCalledTimes(1);
  });

  it('admin can update any listing they do not own', async () => {
    mockListingRepo.findById.mockImplementation((id: string) => makeListing(id, 'someone-else'));

    const cmd = new BulkUpdateListingsCommand(['l1'], { priceVND: 9n }, 'admin-1', 'ADMIN');
    const result = await handler.execute(cmd);

    expect(result.succeeded).toBe(true);
  });

  it('flags invalid status transitions per item and rolls back', async () => {
    // Two ACTIVE listings — DRAFT is not a valid transition from ACTIVE
    mockListingRepo.findById.mockImplementation((id: string) => makeListing(id, 'seller-1'));

    const cmd = new BulkUpdateListingsCommand(
      ['l1', 'l2'],
      { status: 'DRAFT' as any },
      'seller-1',
      'USER',
    );
    const result = await handler.execute(cmd);

    expect(result.succeeded).toBe(false);
    expect(mockPrisma.$transaction).not.toHaveBeenCalled();
    expect(result.items.every((i) => !i.success)).toBe(true);
  });
});
