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

describe('BetsService', () => {
  const tx = {
    bet: {
      findUnique: jest.fn(),
      create: jest.fn(),
    },
    marketSelection: {
      findUnique: jest.fn(),
    },
  };
  const prisma = {
    ...tx,
    $transaction: jest.fn(async (fn: (client: typeof tx) => Promise<unknown>) => fn(tx)),
  };
  const funds = {
    freezeBet: jest.fn(),
  };
  const bettingLimits = {
    validateBet: jest.fn(),
  };

  let service: BetsService;

  function selection(id: bigint, oddsVersion: bigint, matchId = 88n) {
    return {
      id,
      marketId: id + 100n,
      selectionCode: id === 1n ? 'HOME' : 'AWAY',
      selectionName: `Selection ${id}`,
      nameI18n: null,
      odds: new Decimal(id === 1n ? 1.9 : 2.1),
      oddsVersion,
      status: 'OPEN',
      market: {
        matchId,
        marketType: 'FT_1X2',
        period: 'FT',
        nameI18n: null,
        lineValue: null,
        allowSingle: true,
        allowParlay: true,
        showOnPlayer: true,
        status: 'OPEN',
        match: {
          status: 'PUBLISHED',
          sportType: 'FOOTBALL',
          isOutright: false,
          startTime: new Date(Date.now() + 60 * 60 * 1000),
        },
      },
    };
  }

  beforeEach(() => {
    jest.clearAllMocks();
    service = new BetsService(prisma as never, funds as never, bettingLimits as never);
    tx.bet.findUnique.mockResolvedValue(null);
    tx.bet.create.mockResolvedValue({ id: 123n, betNo: 'BET-OK', selections: [] });
    bettingLimits.validateBet.mockResolvedValue(undefined);
    tx.marketSelection.findUnique.mockImplementation(({ where }: { where: { id: bigint } }) =>
      Promise.resolve(selection(where.id, where.id)),
    );
  });

  it('rejects a parlay with multiple legs from the same match', async () => {
    await expect(
      service.placeParlayBet(
        7n,
        3n,
        [
          { selectionId: 1n, oddsVersion: 1n },
          { selectionId: 2n, oddsVersion: 2n },
        ],
        50,
        'REQ-SAME-MATCH',
      ),
    ).rejects.toMatchObject(expectAppError('PARLAY_SAME_MATCH_FORBIDDEN'));

    expect(tx.bet.create).not.toHaveBeenCalled();
    expect(bettingLimits.validateBet).not.toHaveBeenCalled();
    expect(funds.freezeBet).not.toHaveBeenCalled();
  });

  it('allows a parlay only when every leg is from a different match', async () => {
    tx.marketSelection.findUnique.mockImplementation(({ where }: { where: { id: bigint } }) =>
      Promise.resolve(selection(where.id, where.id, where.id === 1n ? 88n : 99n)),
    );

    await service.placeParlayBet(
      7n,
      3n,
      [
        { selectionId: 1n, oddsVersion: 1n },
        { selectionId: 2n, oddsVersion: 2n },
      ],
      50,
      'REQ-SAME-MATCH',
    );

    expect(tx.bet.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          betType: 'PARLAY',
          selections: expect.objectContaining({
            create: expect.arrayContaining([
              expect.objectContaining({ matchId: 88n, selectionId: 1n }),
              expect.objectContaining({ matchId: 99n, selectionId: 2n }),
            ]),
          }),
        }),
      }),
    );
    expect(bettingLimits.validateBet).toHaveBeenCalledWith(
      expect.objectContaining({ betType: 'PARLAY', tx }),
    );
    expect(funds.freezeBet).toHaveBeenCalledWith(
      expect.objectContaining({ userId: 7n, stake: expect.any(Decimal), tx }),
    );
  });

  it('rejects a single bet when the market disables single betting', async () => {
    tx.marketSelection.findUnique.mockResolvedValue({
      ...selection(1n, 1n),
      market: {
        ...selection(1n, 1n).market,
        allowSingle: false,
      },
    });

    await expect(
      service.placeSingleBet(7n, 3n, 1n, 1n, 50, 'REQ-SINGLE-DISABLED'),
    ).rejects.toMatchObject(expectAppError('SINGLE_MARKET_NOT_ALLOWED'));

    expect(tx.bet.create).not.toHaveBeenCalled();
    expect(bettingLimits.validateBet).not.toHaveBeenCalled();
    expect(funds.freezeBet).not.toHaveBeenCalled();
  });

  it('revalidates odds version inside the placement transaction', async () => {
    tx.marketSelection.findUnique.mockResolvedValue(selection(1n, 2n));

    await expect(
      service.placeSingleBet(7n, 3n, 1n, 1n, 50, 'REQ-ODDS-CHANGED'),
    ).rejects.toMatchObject(expectAppError('ODDS_CHANGED'));

    expect(tx.bet.create).not.toHaveBeenCalled();
    expect(funds.freezeBet).not.toHaveBeenCalled();
  });
});
