import { Decimal } from '@prisma/client/runtime/library';
import { DepositService } from './deposit.service';
import { expectAppError } from '../../testing/prisma-mock';

describe('DepositService', () => {
  const tx = {
    $queryRaw: jest.fn(),
    depositOrder: {
      findUnique: jest.fn(),
      findFirst: jest.fn(),
      update: jest.fn(),
      delete: jest.fn(),
    },
    paymentMethod: {
      findUnique: jest.fn(),
    },
    bet: {
      findMany: jest.fn(),
    },
    user: {
      findFirst: jest.fn(),
      findUnique: jest.fn(),
    },
    agentProfile: {
      findUnique: jest.fn(),
    },
    depositOrderAuditLog: {
      create: jest.fn(),
    },
  };
  const prisma = {
    ...tx,
    $transaction: jest.fn(async (fn: (client: typeof tx) => Promise<unknown>) => fn(tx)),
  };
  const funds = {
    deposit: jest.fn(),
    withdraw: jest.fn(),
  };
  const credit = {
    recalculateUsedCredit: jest.fn(),
  };
  const playerMessages = {
    createDepositApprovedMessage: jest.fn(),
    createDepositRejectedMessage: jest.fn(),
  };
  const systemConfig = {
    getInboxNotifySettings: jest.fn().mockResolvedValue({
      inboxEnabled: true,
      deposit: true,
      banner: true,
      announcement: true,
    }),
  };

  let service: DepositService;

  beforeEach(() => {
    jest.clearAllMocks();
    service = new DepositService(
      prisma as never,
      funds as never,
      credit as never,
      playerMessages as never,
      systemConfig as never,
    );
    tx.$queryRaw.mockResolvedValue([{ id: 1n }]);
    tx.depositOrder.findUnique.mockResolvedValue({
      id: 1n,
      orderNo: 'DEP-1',
      playerId: 7n,
      amount: new Decimal(100),
      approvedAmount: new Decimal(100),
      status: 'APPROVED',
      reviewedAt: new Date(),
      screenshotUrl: '/uploads/a.png',
    });
    tx.bet.findMany.mockResolvedValue([]);
    tx.user.findFirst.mockResolvedValue(null);
    tx.user.findUnique.mockResolvedValue({ locale: 'en-US' });
    tx.agentProfile.findUnique.mockResolvedValue(null);
    credit.recalculateUsedCredit.mockResolvedValue(undefined);
    playerMessages.createDepositApprovedMessage.mockResolvedValue({});
    playerMessages.createDepositRejectedMessage.mockResolvedValue({});
  });

  it('posts approved deposits as player wallet transactions and refreshes parent credit', async () => {
    tx.depositOrder.findUnique.mockResolvedValue({
      id: 1n,
      orderNo: 'DEP-1',
      playerId: 7n,
      amount: new Decimal(100),
      approvedAmount: null,
      status: 'PENDING',
      reviewedAt: null,
      screenshotUrl: '/uploads/a.png',
    });
    tx.depositOrderAuditLog.create.mockResolvedValue({ id: 501n });
    tx.user.findFirst.mockResolvedValue({ parentId: 3n });
    tx.agentProfile.findUnique.mockResolvedValue({
      creditLimit: new Decimal(1000),
      usedCredit: new Decimal(100),
    });

    await service.approveDepositOrder(1n, 2n, 100, 'Bank receipt checked');

    expect(tx.depositOrder.update).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          status: 'APPROVED',
          approvedAmount: new Decimal(100),
        }),
      }),
    );
    expect(funds.deposit).toHaveBeenCalledWith(
      expect.objectContaining({
        userId: 7n,
        amount: new Decimal(100),
        operatorId: 2n,
        remark: 'Bank receipt checked',
        referenceId: 'DEP-1',
        transactionType: 'PLAYER_DEPOSIT',
        businessKey: 'deposit:DEP-1:approve:501',
        tx,
      }),
    );
    expect(credit.recalculateUsedCredit).toHaveBeenCalledTimes(2);
    expect(credit.recalculateUsedCredit).toHaveBeenNthCalledWith(1, 3n, tx);
    expect(credit.recalculateUsedCredit).toHaveBeenNthCalledWith(2, 3n, tx);
  });

  it('uses a fresh business key when re-approving after revoke', async () => {
    const reviewedAt = new Date('2026-06-15T14:08:48.000Z');
    tx.depositOrder.findUnique.mockResolvedValue({
      id: 1n,
      orderNo: 'DEP-1',
      playerId: 7n,
      amount: new Decimal(2),
      approvedAmount: null,
      status: 'PENDING',
      reviewedAt: null,
      screenshotUrl: '/uploads/a.png',
    });
    tx.depositOrderAuditLog.create.mockResolvedValueOnce({ id: 601n });

    await service.approveDepositOrder(1n, 2n, 2, 'Second approval');

    expect(funds.deposit).toHaveBeenCalledWith(
      expect.objectContaining({
        amount: new Decimal(2),
        businessKey: 'deposit:DEP-1:approve:601',
      }),
    );
  });

  it('uses approval cycle key when revoking a funded deposit for re-review', async () => {
    const reviewedAt = new Date();
    tx.depositOrder.findUnique.mockResolvedValue({
      id: 1n,
      orderNo: 'DEP-1',
      playerId: 7n,
      amount: new Decimal(21),
      approvedAmount: new Decimal(21),
      status: 'APPROVED',
      reviewedAt,
      screenshotUrl: '/uploads/a.png',
    });
    tx.depositOrderAuditLog.create.mockResolvedValue({ id: 701n });

    await service.reopenDepositOrderForReview(1n, 2n);

    expect(funds.withdraw).toHaveBeenCalledWith(
      expect.objectContaining({
        amount: new Decimal(21),
        businessKey: `deposit:DEP-1:reopen-reverse:${reviewedAt.getTime()}`,
      }),
    );
  });

  it('blocks approved deposit revoke when bets exist after approval', async () => {
    tx.bet.findMany.mockResolvedValue([{ id: 99n }]);

    await expect(
      service.reopenDepositOrderForReview(1n, 2n),
    ).rejects.toMatchObject(expectAppError('DEPOSIT_REVOKE_SETTLED_BETS'));

    expect(funds.withdraw).not.toHaveBeenCalled();
    expect(tx.depositOrder.update).not.toHaveBeenCalled();
  });

  it('does not delete a funded deposit order', async () => {
    await expect(
      service.deleteDepositOrder(1n, 2n),
    ).rejects.toMatchObject(expectAppError('DEPOSIT_ORDER_FUNDED_DELETE_FORBIDDEN'));

    expect(tx.depositOrder.delete).not.toHaveBeenCalled();
  });

  it('reapplies a rejected deposit on the same order for the player', async () => {
    tx.depositOrder.findUnique
      .mockResolvedValueOnce({
        id: 1n,
        orderNo: 'DEP-1',
        playerId: 7n,
        paymentMethodId: 5n,
        methodType: 'BANK',
        amount: new Decimal(11),
        approvedAmount: null,
        status: 'REJECTED',
        reviewedAt: new Date(),
        rejectReason: 'Screenshot unclear',
        remark: 'Screenshot unclear',
        screenshotUrl: '/uploads/deposits/old.png',
      })
      .mockResolvedValueOnce({
        id: 1n,
        orderNo: 'DEP-1',
        playerId: 7n,
        amount: new Decimal(11),
        status: 'PENDING',
        createdAt: new Date(),
      });
    tx.depositOrder.findFirst.mockResolvedValue(null);
    tx.paymentMethod.findUnique.mockResolvedValue({
      id: 5n,
      methodType: 'BANK',
      isActive: true,
    });

    const result = await service.reapplyDepositOrder(
      7n,
      1n,
      '/uploads/deposits/new.png',
      11,
    );

    expect(tx.depositOrder.update).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { id: 1n },
        data: expect.objectContaining({
          status: 'PENDING',
          screenshotUrl: '/uploads/deposits/new.png',
          rejectReason: null,
          remark: null,
          reviewerId: null,
          reviewedAt: null,
        }),
      }),
    );
    expect(tx.depositOrderAuditLog.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          action: 'REOPENED',
          actorId: 7n,
          actorType: 'PLAYER',
          statusBefore: 'REJECTED',
          statusAfter: 'PENDING',
        }),
      }),
    );
    expect(result?.orderNo).toBe('DEP-1');
  });

  it('blocks reapply when player already has another pending deposit', async () => {
    tx.depositOrder.findUnique.mockResolvedValue({
      id: 1n,
      orderNo: 'DEP-1',
      playerId: 7n,
      paymentMethodId: 5n,
      methodType: 'BANK',
      amount: new Decimal(11),
      status: 'REJECTED',
      screenshotUrl: '/uploads/deposits/old.png',
    });
    tx.depositOrder.findFirst.mockResolvedValue({ id: 99n });

    await expect(
      service.reapplyDepositOrder(7n, 1n, '/uploads/deposits/new.png'),
    ).rejects.toMatchObject(expectAppError('DEPOSIT_PENDING_ORDER_EXISTS'));

    expect(tx.depositOrder.update).not.toHaveBeenCalled();
  });

  it('blocks reapply for non-rejected deposit orders', async () => {
    tx.depositOrder.findUnique.mockResolvedValue({
      id: 1n,
      orderNo: 'DEP-1',
      playerId: 7n,
      paymentMethodId: 5n,
      methodType: 'BANK',
      amount: new Decimal(11),
      status: 'PENDING',
      screenshotUrl: '/uploads/deposits/old.png',
    });

    await expect(
      service.reapplyDepositOrder(7n, 1n, '/uploads/deposits/new.png'),
    ).rejects.toMatchObject(expectAppError('ORDER_NOT_REJECTED'));
  });
});
