import { type EscrowStatus } from '@prisma/client';
import { PrismaEscrowRepository } from '../repositories/prisma-escrow.repository';

describe('PrismaEscrowRepository', () => {
  let repository: PrismaEscrowRepository;
  let mockPrisma: {
    escrow: {
      findUnique: ReturnType<typeof vi.fn>;
      create: ReturnType<typeof vi.fn>;
      update: ReturnType<typeof vi.fn>;
    };
  };

  const now = new Date('2026-04-01T10:00:00Z');
  const later = new Date('2026-04-01T10:05:00Z');
  const heldDate = new Date('2026-04-01T10:02:00Z');

  const mockPrismaEscrow = {
    id: 'escrow-1',
    orderId: 'order-1',
    amountVND: 500_000_000n,
    feeVND: 25_000_000n,
    status: 'PENDING' as EscrowStatus,
    heldAt: null as Date | null,
    releasedAt: null as Date | null,
    disputeReason: null as string | null,
    disputedAt: null as Date | null,
    createdAt: now,
    updatedAt: later,
  };

  beforeEach(() => {
    mockPrisma = {
      escrow: {
        findUnique: vi.fn(),
        create: vi.fn(),
        update: vi.fn(),
      },
    };
    repository = new PrismaEscrowRepository(mockPrisma as any);
  });

  describe('findById', () => {
    it('returns domain entity when escrow exists', async () => {
      mockPrisma.escrow.findUnique.mockResolvedValue(mockPrismaEscrow);

      const result = await repository.findById('escrow-1');

      expect(mockPrisma.escrow.findUnique).toHaveBeenCalledWith({
        where: { id: 'escrow-1' },
      });
      expect(result).not.toBeNull();
      expect(result!.id).toBe('escrow-1');
      expect(result!.orderId).toBe('order-1');
      expect(result!.amount.value).toBe(500_000_000n);
      expect(result!.fee.value).toBe(25_000_000n);
      expect(result!.status).toBe('PENDING');
      expect(result!.heldAt).toBeNull();
      expect(result!.releasedAt).toBeNull();
      expect(result!.disputeReason).toBeNull();
      expect(result!.disputedAt).toBeNull();
    });

    it('returns null when escrow does not exist', async () => {
      mockPrisma.escrow.findUnique.mockResolvedValue(null);

      const result = await repository.findById('nonexistent');

      expect(result).toBeNull();
    });
  });

  describe('findByOrderId', () => {
    it('returns domain entity when escrow exists for order', async () => {
      mockPrisma.escrow.findUnique.mockResolvedValue(mockPrismaEscrow);

      const result = await repository.findByOrderId('order-1');

      expect(mockPrisma.escrow.findUnique).toHaveBeenCalledWith({
        where: { orderId: 'order-1' },
      });
      expect(result).not.toBeNull();
      expect(result!.orderId).toBe('order-1');
    });

    it('returns null when no escrow exists for order', async () => {
      mockPrisma.escrow.findUnique.mockResolvedValue(null);

      const result = await repository.findByOrderId('order-999');

      expect(result).toBeNull();
    });
  });

  describe('save', () => {
    it('persists a new escrow with correct field mapping', async () => {
      mockPrisma.escrow.create.mockResolvedValue(mockPrismaEscrow);

      const { EscrowEntity } = await import('../../domain/entities/escrow.entity');
      const { Money } = await import('../../domain/value-objects/money.vo');

      const amount = Money.create(500_000_000n).unwrap();
      const fee = Money.create(25_000_000n).unwrap();

      const entity = new EscrowEntity('escrow-1', {
        orderId: 'order-1',
        amount,
        fee,
        status: 'PENDING',
        heldAt: null,
        releasedAt: null,
        disputeReason: null,
        disputedAt: null,
      });

      await repository.save(entity);

      expect(mockPrisma.escrow.create).toHaveBeenCalledWith({
        data: {
          id: 'escrow-1',
          orderId: 'order-1',
          amountVND: 500_000_000n,
          feeVND: 25_000_000n,
          status: 'PENDING',
          heldAt: null,
          releasedAt: null,
          disputeReason: null,
          disputedAt: null,
        },
      });
    });

    it('persists escrow with held status and heldAt date', async () => {
      const heldEscrow = {
        ...mockPrismaEscrow,
        status: 'HELD' as EscrowStatus,
        heldAt: heldDate,
      };
      mockPrisma.escrow.create.mockResolvedValue(heldEscrow);

      const { EscrowEntity } = await import('../../domain/entities/escrow.entity');
      const { Money } = await import('../../domain/value-objects/money.vo');

      const amount = Money.create(500_000_000n).unwrap();
      const fee = Money.create(25_000_000n).unwrap();

      const entity = new EscrowEntity('escrow-1', {
        orderId: 'order-1',
        amount,
        fee,
        status: 'HELD',
        heldAt: heldDate,
        releasedAt: null,
        disputeReason: null,
        disputedAt: null,
      });

      await repository.save(entity);

      expect(mockPrisma.escrow.create).toHaveBeenCalledWith({
        data: expect.objectContaining({
          status: 'HELD',
          heldAt: heldDate,
        }),
      });
    });
  });

  describe('update', () => {
    it('updates status and temporal fields', async () => {
      const releasedDate = new Date('2026-04-01T12:00:00Z');
      mockPrisma.escrow.update.mockResolvedValue({
        ...mockPrismaEscrow,
        status: 'RELEASED',
        heldAt: heldDate,
        releasedAt: releasedDate,
      });

      const { EscrowEntity } = await import('../../domain/entities/escrow.entity');
      const { Money } = await import('../../domain/value-objects/money.vo');

      const amount = Money.create(500_000_000n).unwrap();
      const fee = Money.create(25_000_000n).unwrap();

      const entity = new EscrowEntity('escrow-1', {
        orderId: 'order-1',
        amount,
        fee,
        status: 'RELEASED',
        heldAt: heldDate,
        releasedAt: releasedDate,
        disputeReason: null,
        disputedAt: null,
      });

      await repository.update(entity);

      expect(mockPrisma.escrow.update).toHaveBeenCalledWith({
        where: { id: 'escrow-1' },
        data: {
          status: 'RELEASED',
          heldAt: heldDate,
          releasedAt: releasedDate,
          disputeReason: null,
          disputedAt: null,
        },
      });
    });

    it('updates dispute fields when escrow is disputed', async () => {
      const disputedDate = new Date('2026-04-02T08:00:00Z');
      mockPrisma.escrow.update.mockResolvedValue({
        ...mockPrismaEscrow,
        status: 'DISPUTED',
        heldAt: heldDate,
        disputeReason: 'Hàng không đúng mô tả',
        disputedAt: disputedDate,
      });

      const { EscrowEntity } = await import('../../domain/entities/escrow.entity');
      const { Money } = await import('../../domain/value-objects/money.vo');

      const amount = Money.create(500_000_000n).unwrap();
      const fee = Money.create(25_000_000n).unwrap();

      const entity = new EscrowEntity('escrow-1', {
        orderId: 'order-1',
        amount,
        fee,
        status: 'DISPUTED',
        heldAt: heldDate,
        releasedAt: null,
        disputeReason: 'Hàng không đúng mô tả',
        disputedAt: disputedDate,
      });

      await repository.update(entity);

      expect(mockPrisma.escrow.update).toHaveBeenCalledWith({
        where: { id: 'escrow-1' },
        data: expect.objectContaining({
          status: 'DISPUTED',
          disputeReason: 'Hàng không đúng mô tả',
          disputedAt: disputedDate,
        }),
      });
    });
  });

  describe('toDomain mapping', () => {
    it('correctly maps bigint amounts to Money value objects', async () => {
      mockPrisma.escrow.findUnique.mockResolvedValue({
        ...mockPrismaEscrow,
        amountVND: 999_999_999_999n,
        feeVND: 50_000_000n,
      });

      const result = await repository.findById('escrow-1');

      expect(result!.amount.value).toBe(999_999_999_999n);
      expect(result!.fee.value).toBe(50_000_000n);
    });

    it('preserves nullable temporal fields', async () => {
      const disputedDate = new Date('2026-04-03T09:00:00Z');
      mockPrisma.escrow.findUnique.mockResolvedValue({
        ...mockPrismaEscrow,
        status: 'DISPUTED',
        heldAt: heldDate,
        disputeReason: 'Test dispute',
        disputedAt: disputedDate,
      });

      const result = await repository.findById('escrow-1');

      expect(result!.heldAt).toEqual(heldDate);
      expect(result!.releasedAt).toBeNull();
      expect(result!.disputeReason).toBe('Test dispute');
      expect(result!.disputedAt).toEqual(disputedDate);
    });

    it('preserves createdAt and updatedAt timestamps', async () => {
      mockPrisma.escrow.findUnique.mockResolvedValue(mockPrismaEscrow);

      const result = await repository.findById('escrow-1');

      expect(result!.createdAt).toEqual(now);
      expect(result!.updatedAt).toEqual(later);
    });

    it('computes netPayout correctly from mapped amounts', async () => {
      mockPrisma.escrow.findUnique.mockResolvedValue({
        ...mockPrismaEscrow,
        amountVND: 1_000_000_000n,
        feeVND: 50_000_000n,
      });

      const result = await repository.findById('escrow-1');

      expect(result!.netPayout).toBe(950_000_000n);
    });
  });
});
