import { Decimal } from '@prisma/client/runtime/library';
import { BET_LIMIT_KEYS } from '../../betting/betting-limits.service';
import type { AgentsService } from '../../agent/agents.service';
import type { BetsService } from '../../betting/bets.service';
import type { SettlementService } from '../../settlement/settlement.service';
import type { WalletService } from '../../ledger/wallet.service';
import type { PrismaService } from '../../../shared/prisma/prisma.service';
import { expectAppErrorThrows, expectEqual, expectTrue } from './smoke-test.helpers';
import type { SmokeTestCaseDef } from './smoke-test.cases';
import {
  BetFlowFixtureIds,
  createBetFlowFixture,
  teardownBetFlowFixture,
} from './smoke-test.bet-flow.fixture';

export const BET_FLOW_PROBE_COUNT = 13;

export type BetFlowProbeDeps = {
  prisma: PrismaService;
  wallet: WalletService;
  bets: BetsService;
  settlement: SettlementService;
  agents: AgentsService;
};

async function upsertSystemConfig(
  prisma: PrismaService,
  key: string,
  value: string,
  snapshots: Array<{ key: string; value: string | null }>,
) {
  const row = await prisma.systemConfig.findUnique({ where: { configKey: key } });
  snapshots.push({ key, value: row?.configValue ?? null });
  await prisma.systemConfig.upsert({
    where: { configKey: key },
    create: { configKey: key, configValue: value, description: 'smoke test' },
    update: { configValue: value },
  });
}

async function restoreSystemConfigs(
  prisma: PrismaService,
  snapshots: Array<{ key: string; value: string | null }>,
) {
  for (const snap of snapshots) {
    if (snap.value === null) {
      await prisma.systemConfig.deleteMany({ where: { configKey: snap.key } });
    } else {
      await prisma.systemConfig.upsert({
        where: { configKey: snap.key },
        create: { configKey: snap.key, configValue: snap.value, description: 'smoke test restore' },
        update: { configValue: snap.value },
      });
    }
  }
}

async function confirmMatchSettlement(
  deps: BetFlowProbeDeps,
  fx: BetFlowFixtureIds,
  score: { htHome: number; htAway: number; ftHome: number; ftAway: number },
) {
  await deps.prisma.match.update({
    where: { id: fx.matchId },
    data: { status: 'CLOSED', closeTime: new Date() },
  });
  const preview = await deps.settlement.previewSettlement(fx.matchId, fx.operatorId, {
    htHome: score.htHome,
    htAway: score.htAway,
    ftHome: score.ftHome,
    ftAway: score.ftAway,
  });
  await deps.settlement.confirmSettlement(preview.batch.id, fx.operatorId);
}

export function createBetFlowProbes(deps: BetFlowProbeDeps): SmokeTestCaseDef[] {
  return [
    {
      id: 'BF001',
      suite: 'bet-flow',
      name: '单关赢：冻结后结算派彩',
      uatRef: '8/14',
      description: '下注 100@2.0 主胜，比分 2-1，余额 1000→1100',
      run: async () => {
        const fx = await createBetFlowFixture(deps.prisma, deps.wallet, { initialBalance: 1000 });
        try {
          const bet = await deps.bets.placeSingleBet(
            fx.playerId,
            null,
            fx.homeSelectionId,
            fx.homeOddsVersion,
            100,
            `smoke-win-${fx.runId}`,
          );

          expectEqual('bet.status', bet.status, 'PENDING');
          expectEqual('bet.stake', bet.stake.toString(), '100');

          let w = await deps.wallet.getWallet(fx.playerId);
          expectEqual('available after freeze', w.availableBalance.toString(), '900');
          expectEqual('frozen after freeze', w.frozenBalance.toString(), '100');

          await confirmMatchSettlement(deps, fx, { htHome: 1, htAway: 0, ftHome: 2, ftAway: 1 });

          const settled = await deps.prisma.bet.findUnique({ where: { id: bet.id } });
          expectEqual('settled status', settled?.status, 'WON');
          expectEqual('actualReturn', settled?.actualReturn.toString(), '200');

          w = await deps.wallet.getWallet(fx.playerId);
          expectEqual('available after settle', w.availableBalance.toString(), '1100');
          expectEqual('frozen after settle', w.frozenBalance.toString(), '0');

          const txs = await deps.prisma.walletTransaction.findMany({
            where: { userId: fx.playerId },
            orderBy: { createdAt: 'asc' },
          });
          expectEqual(
            'wallet tx types',
            txs.map((t) => t.transactionType).join(','),
            'MANUAL_DEPOSIT,BET_FREEZE,BET_SETTLE_WIN',
          );
        } finally {
          await teardownBetFlowFixture(deps.prisma, fx);
        }
      },
    },
    {
      id: 'BF002',
      suite: 'bet-flow',
      name: '单关输：冻结本金被消耗',
      uatRef: '14',
      description: '和局选项 + 2-1 比分，余额 1000→900',
      run: async () => {
        const fx = await createBetFlowFixture(deps.prisma, deps.wallet, { initialBalance: 1000 });
        try {
          const bet = await deps.bets.placeSingleBet(
            fx.playerId,
            null,
            fx.drawSelectionId,
            fx.drawOddsVersion,
            100,
            `smoke-lose-${fx.runId}`,
          );

          let w = await deps.wallet.getWallet(fx.playerId);
          expectEqual('available after freeze', w.availableBalance.toString(), '900');
          expectEqual('frozen after freeze', w.frozenBalance.toString(), '100');

          await confirmMatchSettlement(deps, fx, { htHome: 1, htAway: 0, ftHome: 2, ftAway: 1 });

          const settled = await deps.prisma.bet.findUnique({ where: { id: bet.id } });
          expectEqual('settled status', settled?.status, 'LOST');
          expectEqual('actualReturn', settled?.actualReturn.toString(), '0');

          w = await deps.wallet.getWallet(fx.playerId);
          expectEqual('available after settle', w.availableBalance.toString(), '900');
          expectEqual('frozen after settle', w.frozenBalance.toString(), '0');
        } finally {
          await teardownBetFlowFixture(deps.prisma, fx);
        }
      },
    },
    {
      id: 'BF003',
      suite: 'bet-flow',
      name: '下注幂等：相同 requestId 不重复扣款',
      uatRef: '8',
      description: 'userId + requestId 唯一，重复提交返回同一注单',
      run: async () => {
        const fx = await createBetFlowFixture(deps.prisma, deps.wallet, { initialBalance: 500 });
        try {
          const requestId = `smoke-idem-${fx.runId}`;
          const first = await deps.bets.placeSingleBet(
            fx.playerId,
            null,
            fx.homeSelectionId,
            fx.homeOddsVersion,
            50,
            requestId,
          );
          const second = await deps.bets.placeSingleBet(
            fx.playerId,
            null,
            fx.homeSelectionId,
            fx.homeOddsVersion,
            50,
            requestId,
          );

          expectEqual('same bet id', second.id.toString(), first.id.toString());
          const count = await deps.prisma.bet.count({ where: { userId: fx.playerId } });
          expectEqual('bet count', count, 1);

          const w = await deps.wallet.getWallet(fx.playerId);
          expectEqual('available', w.availableBalance.toString(), '450');
          expectEqual('frozen', w.frozenBalance.toString(), '50');
        } finally {
          await teardownBetFlowFixture(deps.prisma, fx);
        }
      },
    },
    {
      id: 'BF004',
      suite: 'bet-flow',
      name: '余额不足：拒绝下注',
      uatRef: '8',
      description: '可用 50 时下 100 注单应失败且无冻结',
      run: async () => {
        const fx = await createBetFlowFixture(deps.prisma, deps.wallet, { initialBalance: 50 });
        try {
          await expectAppErrorThrows(
            'placeSingleBet',
            async () => {
              await deps.bets.placeSingleBet(
                fx.playerId,
                null,
                fx.homeSelectionId,
                fx.homeOddsVersion,
                100,
                `smoke-insuf-${fx.runId}`,
              );
            },
            'INSUFFICIENT_BALANCE',
          );

          const count = await deps.prisma.bet.count({ where: { userId: fx.playerId } });
          expectEqual('bet count', count, 0);

          const w = await deps.wallet.getWallet(fx.playerId);
          expectEqual('available', w.availableBalance.toString(), '50');
          expectEqual('frozen', w.frozenBalance.toString(), '0');
        } finally {
          await teardownBetFlowFixture(deps.prisma, fx);
        }
      },
    },
    {
      id: 'BF006',
      suite: 'bet-flow',
      name: '录分校验：半场比分不能大于全场',
      description: 'HT>FT 时 recordScore 返回 SETTLEMENT_SCORE_INVALID',
      run: async () => {
        const fx = await createBetFlowFixture(deps.prisma, deps.wallet);
        try {
          await deps.prisma.match.update({
            where: { id: fx.matchId },
            data: { status: 'CLOSED', closeTime: new Date() },
          });
          await expectAppErrorThrows(
            'recordScore invalid HT>FT',
            async () => {
              await deps.settlement.recordScore(fx.matchId, 2, 0, 1, 0, fx.operatorId);
            },
            'SETTLEMENT_SCORE_INVALID',
          );
        } finally {
          await teardownBetFlowFixture(deps.prisma, fx);
        }
      },
    },
    {
      id: 'BF007',
      suite: 'bet-flow',
      name: '确认结算：拒绝已作废的旧 PREVIEW 批次',
      description: '新 preview 后旧 batch 变为 CANCELLED，confirm 旧 batchId 失败',
      run: async () => {
        const fx = await createBetFlowFixture(deps.prisma, deps.wallet);
        try {
          await deps.prisma.match.update({
            where: { id: fx.matchId },
            data: { status: 'CLOSED', closeTime: new Date() },
          });
          const previewA = await deps.settlement.previewSettlement(fx.matchId, fx.operatorId, {
            htHome: 0,
            htAway: 0,
            ftHome: 1,
            ftAway: 0,
          });
          await deps.settlement.previewSettlement(fx.matchId, fx.operatorId, {
            htHome: 0,
            htAway: 0,
            ftHome: 2,
            ftAway: 1,
          });
          const stale = await deps.prisma.settlementBatch.findUnique({
            where: { id: previewA.batch.id },
          });
          expectEqual('old batch cancelled', stale?.status, 'CANCELLED');
          await expectAppErrorThrows(
            'confirm cancelled batch',
            async () => {
              await deps.settlement.confirmSettlement(previewA.batch.id, fx.operatorId);
            },
            'SETTLEMENT_BATCH_ALREADY_CONFIRMED',
          );
        } finally {
          await teardownBetFlowFixture(deps.prisma, fx);
        }
      },
    },
    {
      id: 'BF008',
      suite: 'bet-flow',
      name: '串动作废：VOID 腿续算，已有 LOSE 腿不退本',
      description: '跨场串关一场 LOSE 后另一场取消，payout=0 而非退 stake',
      run: async () => {
        const fx = await createBetFlowFixture(deps.prisma, deps.wallet, { initialBalance: 1000 });
        const matchB = await deps.prisma.match.create({
          data: {
            sportType: 'FOOTBALL',
            leagueId: fx.leagueId,
            homeTeamId: fx.homeTeamId,
            awayTeamId: fx.awayTeamId,
            startTime: new Date(Date.now() + 48 * 60 * 60 * 1000),
            status: 'PUBLISHED',
            publishTime: new Date(),
          },
        });
        const marketB = await deps.prisma.market.create({
          data: {
            matchId: matchB.id,
            marketType: 'FT_1X2',
            period: 'FT',
            status: 'OPEN',
            selections: {
              create: [
                {
                  selectionCode: 'HOME',
                  selectionName: 'Home B',
                  odds: new Decimal('2.00'),
                  oddsVersion: BigInt(1),
                  status: 'OPEN',
                  sortOrder: 0,
                },
                {
                  selectionCode: 'AWAY',
                  selectionName: 'Away B',
                  odds: new Decimal('2.00'),
                  oddsVersion: BigInt(1),
                  status: 'OPEN',
                  sortOrder: 1,
                },
              ],
            },
          },
          include: { selections: true },
        });
        const awayB = marketB.selections.find((s) => s.selectionCode === 'AWAY')!;

        try {
          const parlay = await deps.bets.placeParlayBet(
            fx.playerId,
            null,
            [
              { selectionId: fx.homeSelectionId, oddsVersion: fx.homeOddsVersion },
              { selectionId: awayB.id, oddsVersion: awayB.oddsVersion },
            ],
            100,
            `smoke-parlay-void-${fx.runId}`,
          );

          await deps.prisma.match.update({
            where: { id: matchB.id },
            data: { status: 'CLOSED', closeTime: new Date() },
          });
          const previewB = await deps.settlement.previewSettlement(matchB.id, fx.operatorId, {
            htHome: 1,
            htAway: 0,
            ftHome: 2,
            ftAway: 1,
          });
          await deps.settlement.confirmSettlement(previewB.batch.id, fx.operatorId);

          let w = await deps.wallet.getWallet(fx.playerId);
          expectEqual('frozen while pending', w.frozenBalance.toString(), '100');

          await deps.settlement.voidMatchBets(fx.matchId);

          const settled = await deps.prisma.bet.findUnique({ where: { id: parlay.id } });
          expectEqual('settled status', settled?.status, 'LOST');
          expectEqual('actualReturn', settled?.actualReturn.toString(), '0');

          w = await deps.wallet.getWallet(fx.playerId);
          expectEqual('available after void parlay', w.availableBalance.toString(), '900');
          expectEqual('frozen after void parlay', w.frozenBalance.toString(), '0');
        } finally {
          const bets = await deps.prisma.bet.findMany({
            where: { userId: fx.playerId },
            select: { id: true },
          });
          const betIds = bets.map((b) => b.id);
          if (betIds.length) {
            await deps.prisma.betSelection.deleteMany({ where: { betId: { in: betIds } } });
            await deps.prisma.bet.deleteMany({ where: { id: { in: betIds } } });
          }
          const batchIds = (
            await deps.prisma.settlementBatch.findMany({
              where: { matchId: { in: [fx.matchId, matchB.id] } },
              select: { id: true },
            })
          ).map((b) => b.id);
          if (batchIds.length) {
            await deps.prisma.settlementItem.deleteMany({ where: { batchId: { in: batchIds } } });
            await deps.prisma.settlementBatch.deleteMany({ where: { id: { in: batchIds } } });
          }
          await deps.prisma.matchScore.deleteMany({
            where: { matchId: { in: [fx.matchId, matchB.id] } },
          });
          const marketIds = (
            await deps.prisma.market.findMany({
              where: { matchId: matchB.id },
              select: { id: true },
            })
          ).map((m) => m.id);
          if (marketIds.length) {
            await deps.prisma.marketSelection.deleteMany({ where: { marketId: { in: marketIds } } });
            await deps.prisma.market.deleteMany({ where: { id: { in: marketIds } } });
          }
          await deps.prisma.match.deleteMany({ where: { id: matchB.id } });
          await teardownBetFlowFixture(deps.prisma, fx);
        }
      },
    },
    {
      id: 'BF009',
      suite: 'bet-flow',
      name: '赔率版本不一致：BetsService 拒绝下注',
      uatRef: 'B003',
      description: '提交旧 oddsVersion 应返回 ODDS_CHANGED',
      run: async () => {
        const fx = await createBetFlowFixture(deps.prisma, deps.wallet, { initialBalance: 500 });
        try {
          await deps.prisma.marketSelection.update({
            where: { id: fx.homeSelectionId },
            data: { oddsVersion: { increment: 1 } },
          });
          await expectAppErrorThrows(
            'placeSingleBet odds changed',
            async () => {
              await deps.bets.placeSingleBet(
                fx.playerId,
                null,
                fx.homeSelectionId,
                fx.homeOddsVersion,
                50,
                `smoke-odds-${fx.runId}`,
              );
            },
            'ODDS_CHANGED',
          );
          const count = await deps.prisma.bet.count({ where: { userId: fx.playerId } });
          expectEqual('bet count', count, 0);
        } finally {
          await teardownBetFlowFixture(deps.prisma, fx);
        }
      },
    },
    {
      id: 'BF010',
      suite: 'bet-flow',
      name: '赛前限制：开赛后拒绝下注',
      description: 'startTime 已过后 placeSingleBet 返回 PRE_MATCH_ONLY',
      run: async () => {
        const fx = await createBetFlowFixture(deps.prisma, deps.wallet, { initialBalance: 500 });
        try {
          await deps.prisma.match.update({
            where: { id: fx.matchId },
            data: { startTime: new Date(Date.now() - 60_000) },
          });
          await expectAppErrorThrows(
            'placeSingleBet after kickoff',
            async () => {
              await deps.bets.placeSingleBet(
                fx.playerId,
                null,
                fx.homeSelectionId,
                fx.homeOddsVersion,
                50,
                `smoke-prematch-${fx.runId}`,
              );
            },
            'PRE_MATCH_ONLY',
          );
        } finally {
          await teardownBetFlowFixture(deps.prisma, fx);
        }
      },
    },
    {
      id: 'BF011',
      suite: 'bet-flow',
      name: '每日投注上限：超额拒单',
      description: 'dailyStakeLimit=150 时第二笔 100 注单失败',
      run: async () => {
        const fx = await createBetFlowFixture(deps.prisma, deps.wallet, { initialBalance: 1000 });
        const snapshots: Array<{ key: string; value: string | null }> = [];
        try {
          await upsertSystemConfig(deps.prisma, BET_LIMIT_KEYS.dailyStakeLimit, '150', snapshots);
          await deps.bets.placeSingleBet(
            fx.playerId,
            null,
            fx.homeSelectionId,
            fx.homeOddsVersion,
            100,
            `smoke-daily-1-${fx.runId}`,
          );
          await expectAppErrorThrows(
            'placeSingleBet daily limit',
            async () => {
              await deps.bets.placeSingleBet(
                fx.playerId,
                null,
                fx.drawSelectionId,
                fx.drawOddsVersion,
                100,
                `smoke-daily-2-${fx.runId}`,
              );
            },
            'DAILY_STAKE_LIMIT',
          );
        } finally {
          await restoreSystemConfigs(deps.prisma, snapshots);
          await teardownBetFlowFixture(deps.prisma, fx);
        }
      },
    },
    {
      id: 'BF012',
      suite: 'bet-flow',
      name: '串关限额：超 maxStakeParlay 拒单',
      description: 'maxStakeParlay=80 时下 100 串关失败',
      run: async () => {
        const fx = await createBetFlowFixture(deps.prisma, deps.wallet, { initialBalance: 1000 });
        const matchB = await deps.prisma.match.create({
          data: {
            sportType: 'FOOTBALL',
            leagueId: fx.leagueId,
            homeTeamId: fx.homeTeamId,
            awayTeamId: fx.awayTeamId,
            startTime: new Date(Date.now() + 48 * 60 * 60 * 1000),
            status: 'PUBLISHED',
            publishTime: new Date(),
          },
        });
        const marketB = await deps.prisma.market.create({
          data: {
            matchId: matchB.id,
            marketType: 'FT_1X2',
            period: 'FT',
            status: 'OPEN',
            selections: {
              create: [
                {
                  selectionCode: 'AWAY',
                  selectionName: 'Away B',
                  odds: new Decimal('2.00'),
                  oddsVersion: BigInt(1),
                  status: 'OPEN',
                  sortOrder: 0,
                },
              ],
            },
          },
          include: { selections: true },
        });
        const awayB = marketB.selections[0]!;
        const snapshots: Array<{ key: string; value: string | null }> = [];
        try {
          await upsertSystemConfig(deps.prisma, BET_LIMIT_KEYS.maxStakeParlay, '80', snapshots);
          await expectAppErrorThrows(
            'placeParlayBet max stake',
            async () => {
              await deps.bets.placeParlayBet(
                fx.playerId,
                null,
                [
                  { selectionId: fx.homeSelectionId, oddsVersion: fx.homeOddsVersion },
                  { selectionId: awayB.id, oddsVersion: awayB.oddsVersion },
                ],
                100,
                `smoke-parlay-max-${fx.runId}`,
              );
            },
            'MAX_STAKE',
          );
        } finally {
          await restoreSystemConfigs(deps.prisma, snapshots);
          const markets = await deps.prisma.market.findMany({
            where: { matchId: matchB.id },
            select: { id: true },
          });
          const marketIds = markets.map((m) => m.id);
          if (marketIds.length) {
            await deps.prisma.marketSelection.deleteMany({ where: { marketId: { in: marketIds } } });
            await deps.prisma.market.deleteMany({ where: { id: { in: marketIds } } });
          }
          await deps.prisma.match.deleteMany({ where: { id: matchB.id } });
          await teardownBetFlowFixture(deps.prisma, fx);
        }
      },
    },
    {
      id: 'BF013',
      suite: 'bet-flow',
      name: '改 SystemConfig 后拒单：minStake',
      description: 'minStake=200 时下 100 单关返回 MIN_STAKE',
      run: async () => {
        const fx = await createBetFlowFixture(deps.prisma, deps.wallet, { initialBalance: 1000 });
        const snapshots: Array<{ key: string; value: string | null }> = [];
        try {
          await upsertSystemConfig(deps.prisma, BET_LIMIT_KEYS.minStake, '200', snapshots);
          await expectAppErrorThrows(
            'placeSingleBet min stake',
            async () => {
              await deps.bets.placeSingleBet(
                fx.playerId,
                null,
                fx.homeSelectionId,
                fx.homeOddsVersion,
                100,
                `smoke-min-${fx.runId}`,
              );
            },
            'MIN_STAKE',
          );
        } finally {
          await restoreSystemConfigs(deps.prisma, snapshots);
          await teardownBetFlowFixture(deps.prisma, fx);
        }
      },
    },
    {
      id: 'BF005',
      suite: 'bet-flow',
      name: '代理额度：结算后 usedCredit 同步',
      uatRef: '4/14',
      description: '玩家输 100 后代理 usedCredit 1000→900',
      run: async () => {
        const fx = await createBetFlowFixture(deps.prisma, deps.wallet, {
          initialBalance: 1000,
          withAgent: true,
        });
        try {
          expectTrue('agent exists', !!fx.agentId);

          await deps.agents.recalculateUsedCredit(fx.agentId!);
          let profile = await deps.prisma.agentProfile.findUnique({
            where: { userId: fx.agentId! },
          });
          expectEqual('usedCredit before bet', profile?.usedCredit.toString(), '1000');

          await deps.bets.placeSingleBet(
            fx.playerId,
            fx.agentId!,
            fx.drawSelectionId,
            fx.drawOddsVersion,
            100,
            `smoke-agent-${fx.runId}`,
          );

          await confirmMatchSettlement(deps, fx, { htHome: 0, htAway: 0, ftHome: 2, ftAway: 1 });

          await deps.agents.recalculateUsedCredit(fx.agentId!);
          profile = await deps.prisma.agentProfile.findUnique({ where: { userId: fx.agentId! } });
          expectEqual('usedCredit after settle', profile?.usedCredit.toString(), '900');

          const w = await deps.wallet.getWallet(fx.playerId);
          expectEqual('player available', w.availableBalance.toString(), '900');
          expectTrue(
            'directPlayerLiability matches wallet',
            new Decimal(profile!.directPlayerLiability).eq(w.availableBalance.add(w.frozenBalance)),
            {
              liability: profile?.directPlayerLiability.toString(),
              wallet: w.availableBalance.add(w.frozenBalance).toString(),
            },
          );
        } finally {
          await teardownBetFlowFixture(deps.prisma, fx);
        }
      },
    },
  ];
}
