import { SettlementService } from './settlement.service';
import { Decimal } from '@prisma/client/runtime/library';

describe('SettlementService outright winner flow', () => {
  const matchId = BigInt(100);
  const operatorId = BigInt(1);
  const winnerTeamId = BigInt(10);
  const batchId = BigInt(500);
  const winningBetId = BigInt(1001);
  const losingBetId = BigInt(1002);
  const winningSelId = BigInt(201);
  const losingSelId = BigInt(202);

  const outrightMatch = {
    id: matchId,
    isOutright: true,
    status: 'CLOSED',
    deletedAt: null,
  };

  const winnerTeam = { id: winnerTeamId, code: 'BRA' };

  const winningBet = {
    id: winningBetId,
    betNo: 'BET-WIN',
    betType: 'SINGLE',
    status: 'PENDING',
    stake: new Decimal(100),
    agentId: null,
    userId: BigInt(50),
    user: { id: BigInt(50) },
    selections: [
      {
        id: BigInt(301),
        matchId,
        marketType: 'OUTRIGHT_WINNER',
        selectionId: winningSelId,
        selectionNameSnapshot: '巴西',
        handicapLine: null,
        totalLine: null,
        odds: new Decimal(3),
        resultStatus: null,
        sortOrder: 0,
      },
    ],
  };

  const losingBet = {
    id: losingBetId,
    betNo: 'BET-LOSE',
    betType: 'SINGLE',
    status: 'PENDING',
    stake: new Decimal(50),
    agentId: null,
    userId: BigInt(51),
    user: { id: BigInt(51) },
    selections: [
      {
        id: BigInt(302),
        matchId,
        marketType: 'OUTRIGHT_WINNER',
        selectionId: losingSelId,
        selectionNameSnapshot: '阿根廷',
        handicapLine: null,
        totalLine: null,
        odds: new Decimal(5),
        resultStatus: null,
        sortOrder: 0,
      },
    ],
  };

  let matchScoreUpsert: jest.Mock;
  let matchFindFirst: jest.Mock;
  let matchUpdate: jest.Mock;
  let teamFindUnique: jest.Mock;
  let marketFindMany: jest.Mock;
  let marketSelectionFindFirst: jest.Mock;
  let marketSelectionFindMany: jest.Mock;
  let matchScoreFindUnique: jest.Mock;
  let settlementBatchCreate: jest.Mock;
  let settlementBatchFindUnique: jest.Mock;
  let betFindMany: jest.Mock;
  let transaction: jest.Mock;
  let wallet: { settleBet: jest.Mock };
  let agents: Record<string, jest.Mock>;
  let service: SettlementService;

  beforeEach(() => {
    matchScoreUpsert = jest.fn().mockResolvedValue({});
    matchFindFirst = jest.fn().mockResolvedValue(outrightMatch);
    matchUpdate = jest.fn().mockResolvedValue({});
    teamFindUnique = jest.fn().mockResolvedValue(winnerTeam);
    marketFindMany = jest.fn().mockResolvedValue([]);
    marketSelectionFindFirst = jest
      .fn()
      .mockResolvedValue({ id: winningSelId, selectionCode: 'BRA' });
    marketSelectionFindMany = jest.fn().mockResolvedValue([
      { id: winningSelId, selectionCode: 'BRA' },
      { id: losingSelId, selectionCode: 'ARG' },
    ]);
    matchScoreFindUnique = jest.fn();
    settlementBatchCreate = jest.fn().mockResolvedValue({
      id: batchId,
      matchId,
      htHomeScore: 0,
      htAwayScore: 0,
      ftHomeScore: 0,
      ftAwayScore: 0,
      status: 'PREVIEW',
    });
    settlementBatchFindUnique = jest.fn();
    betFindMany = jest.fn();
    transaction = jest.fn(async (fn: (client: unknown) => Promise<void>) =>
      fn({
        team: { findUnique: teamFindUnique },
        market: { findMany: marketFindMany },
        marketSelection: { findMany: marketSelectionFindMany },
        matchScore: { upsert: matchScoreUpsert, findUnique: matchScoreFindUnique },
        bet: {
          findMany: betFindMany,
          update: jest.fn().mockResolvedValue({}),
          updateMany: jest.fn().mockResolvedValue({ count: 1 }),
        },
        betSelection: { update: jest.fn().mockResolvedValue({}) },
        settlementItem: { create: jest.fn().mockResolvedValue({}) },
        settlementBatch: {
          findUnique: settlementBatchFindUnique,
          update: jest.fn().mockResolvedValue({}),
          updateMany: jest.fn().mockResolvedValue({ count: 1 }),
        },
        match: { update: jest.fn().mockResolvedValue({}) },
      }),
    );

    const prisma = {
      match: { count: jest.fn().mockResolvedValue(0), findFirst: matchFindFirst, update: matchUpdate },
      team: { findUnique: teamFindUnique },
      market: { findMany: marketFindMany },
      marketSelection: {
        findFirst: marketSelectionFindFirst,
        findMany: marketSelectionFindMany,
      },
      matchScore: {
        findUnique: matchScoreFindUnique,
        upsert: matchScoreUpsert,
      },
      settlementBatch: {
        create: settlementBatchCreate,
        findUnique: settlementBatchFindUnique,
        findFirst: jest.fn().mockResolvedValue(null),
        update: jest.fn().mockResolvedValue({}),
        updateMany: jest.fn().mockResolvedValue({ count: 0 }),
      },
      bet: { findMany: betFindMany },
      $transaction: transaction,
    };

    wallet = { settleBet: jest.fn().mockResolvedValue(undefined) };
    agents = { recalculateUsedCredit: jest.fn().mockResolvedValue(undefined) };

    service = new SettlementService(prisma as never, wallet as never, agents as never);
  });

  it('previewSettlement persists winnerTeamId and previews WIN/LOSE', async () => {
    betFindMany.mockResolvedValue([winningBet, losingBet]);

    const preview = await service.previewSettlement(matchId, operatorId, {
      winnerTeamId,
    });

    expect(matchScoreUpsert).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { matchId },
        create: expect.objectContaining({ winnerTeamId }),
        update: expect.objectContaining({ winnerTeamId }),
      }),
    );
    expect(preview.winnerTeamCode).toBe('BRA');
    expect(preview.items.items).toEqual(
      expect.arrayContaining([
        expect.objectContaining({ betNo: 'BET-WIN', result: 'WIN', payout: '300' }),
        expect.objectContaining({ betNo: 'BET-LOSE', result: 'LOSE', payout: '0' }),
      ]),
    );
  });

  it('previewSettlement requires stats for visible stats markets', async () => {
    matchFindFirst.mockResolvedValue({
      id: matchId,
      isOutright: false,
      status: 'CLOSED',
      deletedAt: null,
    });
    marketFindMany.mockResolvedValue([{ marketType: 'FT_CORNERS_OVER_UNDER' }]);
    betFindMany.mockResolvedValue([]);

    try {
      await service.previewSettlement(matchId, operatorId, {
        htHome: 0,
        htAway: 0,
        ftHome: 1,
        ftAway: 1,
      });
      throw new Error('Expected previewSettlement to reject');
    } catch (err) {
      const response = (err as { getResponse?: () => unknown }).getResponse?.();
      expect(response).toEqual(
        expect.objectContaining({
          code: 'SETTLEMENT_FACTS_REQUIRED',
          params: expect.objectContaining({ fields: 'homeCorners,awayCorners' }),
        }),
      );
    }
  });

  it('derives total cards from yellow and red cards for settlement previews', async () => {
    matchFindFirst.mockResolvedValue({
      id: matchId,
      isOutright: false,
      status: 'CLOSED',
      deletedAt: null,
    });
    marketFindMany.mockResolvedValue([{ marketType: 'FT_CARDS_OVER_UNDER' }]);
    betFindMany.mockResolvedValue([]);

    await service.previewSettlement(matchId, operatorId, {
      htHome: 0,
      htAway: 0,
      ftHome: 2,
      ftAway: 1,
      homeYellowCards: 2,
      homeRedCards: 1,
      awayYellowCards: 4,
      awayRedCards: 0,
    });

    expect(settlementBatchCreate).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          homeYellowCards: 2,
          homeRedCards: 1,
          awayYellowCards: 4,
          awayRedCards: 0,
          homeCards: 3,
          awayCards: 4,
        }),
      }),
    );
  });

  it('shows parlay as lost when this match leg loses even if other legs are pending', async () => {
    matchFindFirst.mockResolvedValue({
      id: matchId,
      isOutright: false,
      status: 'CLOSED',
      deletedAt: null,
    });
    marketSelectionFindMany.mockResolvedValue([
      { id: BigInt(501), selectionCode: 'HOME' },
      { id: BigInt(502), selectionCode: 'AWAY' },
    ]);
    betFindMany.mockResolvedValue([
      {
        id: BigInt(2001),
        betNo: 'PARLAY-PENDING',
        betType: 'PARLAY',
        status: 'PENDING',
        stake: new Decimal(100),
        agentId: null,
        userId: BigInt(60),
        user: { id: BigInt(60) },
        selections: [
          {
            id: BigInt(3001),
            matchId,
            marketType: 'FT_1X2',
            selectionId: BigInt(501),
            selectionNameSnapshot: 'Home',
            handicapLine: null,
            totalLine: null,
            odds: new Decimal(2),
            resultStatus: null,
            sortOrder: 0,
          },
          {
            id: BigInt(3002),
            matchId: BigInt(101),
            marketType: 'FT_1X2',
            selectionId: BigInt(502),
            selectionNameSnapshot: 'Away',
            handicapLine: null,
            totalLine: null,
            odds: new Decimal(2),
            resultStatus: null,
            sortOrder: 1,
          },
        ],
      },
    ]);

    const preview = await service.previewSettlement(matchId, operatorId, {
      htHome: 0,
      htAway: 0,
      ftHome: 0,
      ftAway: 1,
    });

    expect(preview.pendingOtherMatches).toBe(0);
    expect(preview.lostOnThisMatch).toBe(1);
    expect(preview.items.items).toEqual([
      expect.objectContaining({
        betNo: 'PARLAY-PENDING',
        result: 'LOST',
        payout: '0',
      }),
    ]);
  });

  it('previews a parlay result only after the other legs already have results', async () => {
    matchFindFirst.mockResolvedValue({
      id: matchId,
      isOutright: false,
      status: 'CLOSED',
      deletedAt: null,
    });
    marketSelectionFindMany.mockResolvedValue([
      { id: BigInt(501), selectionCode: 'HOME' },
      { id: BigInt(502), selectionCode: 'AWAY' },
    ]);
    betFindMany.mockResolvedValue([
      {
        id: BigInt(2002),
        betNo: 'PARLAY-READY',
        betType: 'PARLAY',
        status: 'PENDING',
        stake: new Decimal(100),
        agentId: null,
        userId: BigInt(61),
        user: { id: BigInt(61) },
        selections: [
          {
            id: BigInt(3003),
            matchId,
            marketType: 'FT_1X2',
            selectionId: BigInt(501),
            selectionNameSnapshot: 'Home',
            handicapLine: null,
            totalLine: null,
            odds: new Decimal(2),
            resultStatus: null,
            sortOrder: 0,
          },
          {
            id: BigInt(3004),
            matchId: BigInt(101),
            marketType: 'FT_1X2',
            selectionId: BigInt(502),
            selectionNameSnapshot: 'Away',
            handicapLine: null,
            totalLine: null,
            odds: new Decimal(2),
            resultStatus: 'WIN',
            sortOrder: 1,
          },
        ],
      },
    ]);

    const preview = await service.previewSettlement(matchId, operatorId, {
      htHome: 0,
      htAway: 0,
      ftHome: 0,
      ftAway: 1,
    });

    expect(preview.pendingOtherMatches).toBe(0);
    expect(preview.items.items).toEqual([
      expect.objectContaining({
        betNo: 'PARLAY-READY',
        result: 'LOST',
        payout: '0',
      }),
    ]);
  });

  it('confirmSettlement settles outright bets as WON/LOST using stored winnerTeamId', async () => {
    const txBetUpdate = jest.fn().mockResolvedValue({});
    const txBetUpdateMany = jest.fn().mockResolvedValue({ count: 1 });
    const txMatchUpdate = jest.fn().mockResolvedValue({});
    transaction.mockImplementation(async (fn: (client: unknown) => Promise<void>) => {
      await fn({
        team: { findUnique: teamFindUnique },
        market: { findMany: marketFindMany },
        marketSelection: { findMany: marketSelectionFindMany },
        matchScore: { upsert: matchScoreUpsert, findUnique: matchScoreFindUnique },
        bet: {
          findMany: betFindMany,
          update: txBetUpdate,
          updateMany: txBetUpdateMany,
        },
        betSelection: { update: jest.fn().mockResolvedValue({}) },
        settlementItem: { create: jest.fn().mockResolvedValue({}) },
        settlementBatch: {
          findUnique: settlementBatchFindUnique,
          update: jest.fn().mockResolvedValue({}),
          updateMany: jest.fn().mockResolvedValue({ count: 1 }),
          findFirst: jest.fn().mockResolvedValue({ id: batchId }),
        },
        match: { update: txMatchUpdate },
      });
    });
    settlementBatchFindUnique.mockResolvedValue({
      id: batchId,
      batchNo: 'SETTLE-001',
      matchId,
      status: 'PREVIEW',
      htHomeScore: 0,
      htAwayScore: 0,
      ftHomeScore: 0,
      ftAwayScore: 0,
      match: { ...outrightMatch, status: 'PENDING_SETTLEMENT' },
    });
    matchScoreFindUnique.mockResolvedValue({
      matchId,
      htHomeScore: 0,
      htAwayScore: 0,
      ftHomeScore: 0,
      ftAwayScore: 0,
      winnerTeamId,
    });
    betFindMany.mockResolvedValue([winningBet, losingBet]);

    const result = await service.confirmSettlement(batchId, operatorId);

    expect(matchScoreUpsert).toHaveBeenCalledWith(
      expect.objectContaining({
        update: expect.objectContaining({ winnerTeamId }),
      }),
    );
    expect(wallet.settleBet).toHaveBeenCalledTimes(2);
    expect(wallet.settleBet.mock.calls[0][0]).toEqual(
      expect.objectContaining({
        userId: winningBet.userId,
        betNo: 'BET-WIN',
        batchNo: 'SETTLE-001',
        result: 'WIN',
      }),
    );
    expect(wallet.settleBet.mock.calls[1][0]).toEqual(
      expect.objectContaining({
        userId: losingBet.userId,
        betNo: 'BET-LOSE',
        batchNo: 'SETTLE-001',
        result: 'LOSE',
      }),
    );
    expect(txBetUpdateMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { id: winningBetId, status: 'PENDING' },
        data: expect.objectContaining({ status: 'WON' }),
      }),
    );
    expect(txBetUpdateMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { id: losingBetId, status: 'PENDING' },
        data: expect.objectContaining({ status: 'LOST' }),
      }),
    );
    expect(txMatchUpdate).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { id: matchId },
        data: { status: 'SETTLED', isHot: false },
      }),
    );
    expect(result).toEqual({ success: true, batchId: batchId.toString() });
  });
});

describe('SettlementService hardening', () => {
  const matchId = BigInt(200);
  const operatorId = BigInt(1);
  const batchId = BigInt(900);
  const otherBatchId = BigInt(901);

  const fixtureMatch = {
    id: matchId,
    isOutright: false,
    status: 'PENDING_SETTLEMENT',
    deletedAt: null,
  };

  const pendingSingleBet = {
    id: BigInt(3001),
    betNo: 'BET-SINGLE',
    betType: 'SINGLE',
    status: 'PENDING',
    stake: new Decimal(100),
    agentId: null,
    userId: BigInt(70),
    user: { id: BigInt(70) },
    selections: [
      {
        id: BigInt(4001),
        matchId,
        marketType: 'FT_1X2',
        selectionId: BigInt(501),
        selectionNameSnapshot: 'Home',
        handicapLine: null,
        totalLine: null,
        odds: new Decimal(2),
        resultStatus: null,
        sortOrder: 0,
      },
    ],
  };

  function buildService(overrides: {
    prisma?: Record<string, unknown>;
    wallet?: Record<string, jest.Mock>;
    transactionClient?: Record<string, unknown>;
  } = {}) {
    const wallet = {
      settleBet: jest.fn().mockResolvedValue(undefined),
      voidBet: jest.fn().mockResolvedValue(undefined),
      ...(overrides.wallet ?? {}),
    };
    const agents = { recalculateUsedCredit: jest.fn().mockResolvedValue(undefined) };
    const transaction = jest.fn(async (fn: (client: unknown) => Promise<void>) => {
      const defaultClient = {
        team: { findUnique: jest.fn().mockResolvedValue(null) },
        market: { findMany: jest.fn().mockResolvedValue([]) },
        marketSelection: { findMany: jest.fn().mockResolvedValue([{ id: BigInt(501), selectionCode: 'HOME' }]) },
        matchScore: {
          upsert: jest.fn().mockResolvedValue({}),
          findUnique: jest.fn().mockResolvedValue({
            matchId,
            htHomeScore: 0,
            htAwayScore: 0,
            ftHomeScore: 1,
            ftAwayScore: 0,
            winnerTeamId: null,
          }),
        },
        bet: {
          findMany: jest.fn().mockResolvedValue([pendingSingleBet]),
          updateMany: jest.fn().mockResolvedValue({ count: 1 }),
        },
        betSelection: { update: jest.fn().mockResolvedValue({}), findMany: jest.fn().mockResolvedValue([]) },
        settlementItem: { create: jest.fn().mockResolvedValue({}) },
        settlementBatch: {
          findUnique: jest.fn().mockResolvedValue({
            id: batchId,
            batchNo: 'STL-001',
            matchId,
            status: 'PREVIEW',
            htHomeScore: 0,
            htAwayScore: 0,
            ftHomeScore: 1,
            ftAwayScore: 0,
            match: fixtureMatch,
          }),
          update: jest.fn().mockResolvedValue({}),
          updateMany: jest.fn().mockResolvedValue({ count: 1 }),
          findFirst: jest.fn().mockResolvedValue({ id: batchId }),
        },
        match: { update: jest.fn().mockResolvedValue({}) },
        ...(overrides.transactionClient ?? {}),
      };
      await fn(defaultClient);
    });

    const prisma = {
      match: {
        count: jest.fn().mockResolvedValue(0),
        findFirst: jest.fn().mockResolvedValue({ ...fixtureMatch, status: 'CLOSED' }),
        update: jest.fn().mockResolvedValue({}),
      },
      team: { findUnique: jest.fn().mockResolvedValue(null) },
      market: { findMany: jest.fn().mockResolvedValue([]) },
      marketSelection: {
        findFirst: jest.fn().mockResolvedValue(null),
        findMany: jest.fn().mockResolvedValue([{ id: BigInt(501), selectionCode: 'HOME' }]),
      },
      matchScore: {
        findUnique: jest.fn().mockResolvedValue(null),
        upsert: jest.fn().mockResolvedValue({}),
      },
      settlementBatch: {
        create: jest.fn().mockResolvedValue({ id: batchId, matchId, status: 'PREVIEW' }),
        findUnique: jest.fn(),
        findFirst: jest.fn().mockResolvedValue(null),
        updateMany: jest.fn().mockResolvedValue({ count: 0 }),
      },
      bet: { findMany: jest.fn().mockResolvedValue([]) },
      $transaction: transaction,
      ...(overrides.prisma ?? {}),
    };

    return {
      service: new SettlementService(prisma as never, wallet as never, agents as never),
      prisma,
      wallet,
      transaction,
    };
  }

  it('recordScore rejects half-time scores greater than full-time', async () => {
    const { service } = buildService();
    try {
      await service.recordScore(matchId, 2, 0, 1, 0, operatorId);
      throw new Error('Expected recordScore to reject');
    } catch (err) {
      const response = (err as { getResponse?: () => unknown }).getResponse?.();
      expect(response).toEqual(
        expect.objectContaining({ code: 'SETTLEMENT_SCORE_INVALID' }),
      );
    }
  });

  it('previewSettlement cancels older preview batches before creating a new one', async () => {
    const updateMany = jest.fn().mockResolvedValue({ count: 1 });
    const { service } = buildService({
      prisma: {
        bet: { findMany: jest.fn().mockResolvedValue([]) },
        settlementBatch: {
          create: jest.fn().mockResolvedValue({ id: batchId, matchId, status: 'PREVIEW' }),
          updateMany,
        },
      },
    });

    await service.previewSettlement(matchId, operatorId, {
      htHome: 0,
      htAway: 0,
      ftHome: 1,
      ftAway: 0,
    });

    expect(updateMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { matchId, status: 'PREVIEW', isResettle: false },
        data: { status: 'CANCELLED' },
      }),
    );
  });

  it('confirmSettlement rejects stale preview batches', async () => {
    const staleBatch = {
      id: otherBatchId,
      batchNo: 'STL-OLD',
      matchId,
      status: 'PREVIEW',
      htHomeScore: 0,
      htAwayScore: 0,
      ftHomeScore: 1,
      ftAwayScore: 0,
      match: fixtureMatch,
    };
    const { service } = buildService({
      prisma: {
        settlementBatch: {
          findUnique: jest.fn().mockResolvedValue(staleBatch),
        },
      },
      transactionClient: {
        settlementBatch: {
          findUnique: jest.fn().mockResolvedValue(staleBatch),
          update: jest.fn().mockResolvedValue({}),
          updateMany: jest.fn().mockResolvedValue({ count: 1 }),
          findFirst: jest.fn().mockResolvedValue({ id: batchId }),
        },
      },
    });

    try {
      await service.confirmSettlement(otherBatchId, operatorId);
      throw new Error('Expected confirmSettlement to reject');
    } catch (err) {
      const response = (err as { getResponse?: () => unknown }).getResponse?.();
      expect(response).toEqual(
        expect.objectContaining({ code: 'SETTLEMENT_BATCH_STALE' }),
      );
    }
  });

  it('confirmSettlement throws when bet status update fails', async () => {
    const { service } = buildService({
      prisma: {
        settlementBatch: {
          findUnique: jest.fn().mockResolvedValue({
            id: batchId,
            batchNo: 'STL-001',
            matchId,
            status: 'PREVIEW',
            htHomeScore: 0,
            htAwayScore: 0,
            ftHomeScore: 1,
            ftAwayScore: 0,
            match: fixtureMatch,
          }),
        },
      },
      transactionClient: {
        bet: {
          findMany: jest.fn().mockResolvedValue([pendingSingleBet]),
          updateMany: jest.fn().mockResolvedValue({ count: 0 }),
        },
      },
    });

    try {
      await service.confirmSettlement(batchId, operatorId);
      throw new Error('Expected confirmSettlement to reject');
    } catch (err) {
      const response = (err as { getResponse?: () => unknown }).getResponse?.();
      expect(response).toEqual(
        expect.objectContaining({
          code: 'SETTLEMENT_BET_UPDATE_FAILED',
          params: expect.objectContaining({ betNo: 'BET-SINGLE' }),
        }),
      );
    }
  });

  it('voidMatchBets settles cross-match parlay as lost when voided leg joins an existing losing leg', async () => {
    const parlayBet = {
      id: BigInt(5001),
      betNo: 'PARLAY-VOID',
      betType: 'PARLAY',
      status: 'PENDING',
      stake: new Decimal(100),
      agentId: null,
      userId: BigInt(80),
      selections: [
        {
          id: BigInt(6001),
          matchId,
          marketType: 'FT_1X2',
          selectionId: BigInt(501),
          selectionNameSnapshot: 'Home',
          handicapLine: null,
          totalLine: null,
          odds: new Decimal(2),
          resultStatus: null,
          sortOrder: 0,
        },
        {
          id: BigInt(6002),
          matchId: BigInt(201),
          marketType: 'FT_1X2',
          selectionId: BigInt(502),
          selectionNameSnapshot: 'Away',
          handicapLine: null,
          totalLine: null,
          odds: new Decimal(2),
          resultStatus: 'LOSE',
          sortOrder: 1,
        },
      ],
    };

    const betSelectionUpdate = jest.fn().mockResolvedValue({});
    const betSelectionFindMany = jest
      .fn()
      .mockResolvedValue([
        { ...parlayBet.selections[0], resultStatus: 'VOID', odds: new Decimal(2) },
        { ...parlayBet.selections[1], resultStatus: 'LOSE', odds: new Decimal(2) },
      ]);
    const betUpdateMany = jest.fn().mockResolvedValue({ count: 1 });
    const funds = { settleBet: jest.fn().mockResolvedValue(undefined), voidBet: jest.fn() };

    const transaction = jest.fn(async (fn: (client: unknown) => Promise<unknown>) =>
      fn({
        match: { update: jest.fn().mockResolvedValue({}) },
        bet: {
          findMany: jest.fn().mockResolvedValue([parlayBet]),
          updateMany: betUpdateMany,
        },
        betSelection: {
          update: betSelectionUpdate,
          findMany: betSelectionFindMany,
        },
      }),
    );

    const service = new SettlementService(
      { $transaction: transaction } as never,
      funds as never,
      { recalculateUsedCredit: jest.fn() } as never,
    );

    const result = await service.voidMatchBets(matchId);

    expect(result.voidedCount).toBe(1);
    expect(funds.voidBet).not.toHaveBeenCalled();
    expect(funds.settleBet).toHaveBeenCalledWith(
      expect.objectContaining({
        betNo: 'PARLAY-VOID',
        result: 'LOSE',
        batchNo: `void:${matchId}`,
      }),
    );
    expect(funds.settleBet.mock.calls[0][0].payout.toString()).toBe('0');
  });

  it('previewResettlement treats single multi-leg tickets like parlays', async () => {
    const multiLegSingle = {
      id: BigInt(7001),
      betNo: 'SINGLE-MULTI',
      betType: 'SINGLE',
      status: 'WON',
      stake: new Decimal(100),
      actualReturn: new Decimal(360),
      agentId: null,
      userId: BigInt(90),
      selections: [
        {
          id: BigInt(8001),
          matchId,
          marketType: 'FT_1X2',
          selectionId: BigInt(501),
          selectionNameSnapshot: 'Home',
          handicapLine: null,
          totalLine: null,
          odds: new Decimal(2),
          resultStatus: 'WIN',
          sortOrder: 0,
        },
        {
          id: BigInt(8002),
          matchId,
          marketType: 'FT_1X2',
          selectionId: BigInt(502),
          selectionNameSnapshot: 'Away',
          handicapLine: null,
          totalLine: null,
          odds: new Decimal(2),
          resultStatus: 'WIN',
          sortOrder: 1,
        },
      ],
    };

    const { service } = buildService({
      prisma: {
        match: {
          count: jest.fn().mockResolvedValue(0),
          findFirst: jest.fn().mockResolvedValue({ ...fixtureMatch, status: 'SETTLED' }),
          update: jest.fn().mockResolvedValue({}),
        },
        bet: { findMany: jest.fn().mockResolvedValue([multiLegSingle]) },
        settlementBatch: {
          create: jest.fn().mockImplementation(({ data }) =>
            Promise.resolve({ id: batchId, ...data }),
          ),
        },
      },
    });

    const preview = await service.previewResettlement(
      matchId,
      {
        htHome: 0,
        htAway: 0,
        ftHome: 0,
        ftAway: 1,
      },
      operatorId,
    );

    expect(preview.items).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          betNo: 'SINGLE-MULTI',
          newStatus: 'LOST',
        }),
      ]),
    );
    expect(preview.items[0].newPayout.toString()).toBe('0');
  });

  it('previewSettlement rejects unsupported market types on pending bets', async () => {
    const { service } = buildService({
      prisma: {
        bet: {
          findMany: jest.fn().mockResolvedValue([
            {
              ...pendingSingleBet,
              selections: [
                {
                  ...pendingSingleBet.selections[0],
                  marketType: 'UNKNOWN_MARKET',
                },
              ],
            },
          ]),
        },
      },
    });

    try {
      await service.previewSettlement(matchId, operatorId, {
        htHome: 0,
        htAway: 0,
        ftHome: 1,
        ftAway: 0,
      });
      throw new Error('Expected previewSettlement to reject');
    } catch (err) {
      const response = (err as { getResponse?: () => unknown }).getResponse?.();
      expect(response).toEqual(
        expect.objectContaining({ code: 'SETTLEMENT_MARKET_UNSUPPORTED' }),
      );
    }
  });
});
