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,
        update: jest.fn().mockResolvedValue({}),
      },
      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('keeps a parlay pending until every match in the ticket is settled', 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(1);
    expect(preview.lostOnThisMatch).toBe(0);
    expect(preview.items.items).toEqual([
      expect.objectContaining({
        betNo: 'PARLAY-PENDING',
        result: 'PENDING_OTHER_MATCHES',
        payout: '0',
        note: '本场腿已出结果，待其他场次结算后统一结算',
      }),
    ]);
  });

  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 }),
        },
        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() });
  });
});
