import { Decimal } from '@prisma/client/runtime/library';
import { CatalogArchiveService } from './catalog-archive.service';

describe('CatalogArchiveService', () => {
  const matchId = BigInt(10);
  const leagueId = BigInt(1);

  let prisma: {
    match: { findFirst: jest.Mock; update: jest.Mock; findMany: jest.Mock; updateMany: jest.Mock };
    league: { findFirst: jest.Mock; update: jest.Mock };
    bet: { findMany: jest.Mock };
    settlementBatch: { findFirst: jest.Mock; findMany: jest.Mock; deleteMany: jest.Mock };
    market: { updateMany: jest.Mock };
    marketSelection: { updateMany: jest.Mock };
    entityTranslation: { findFirst: jest.Mock };
    $transaction: jest.Mock;
  };
  let matchBetStats: { betStatsForMatches: jest.Mock };
  let service: CatalogArchiveService;

  beforeEach(() => {
    prisma = {
      match: {
        findFirst: jest.fn(),
        update: jest.fn(),
        findMany: jest.fn(),
        updateMany: jest.fn(),
      },
      league: { findFirst: jest.fn(), update: jest.fn() },
      bet: { findMany: jest.fn().mockResolvedValue([]) },
      settlementBatch: {
        findFirst: jest.fn().mockResolvedValue(null),
        findMany: jest.fn().mockResolvedValue([]),
        deleteMany: jest.fn().mockResolvedValue({ count: 0 }),
      },
      market: { updateMany: jest.fn() },
      marketSelection: { updateMany: jest.fn() },
      entityTranslation: { findFirst: jest.fn().mockResolvedValue(null) },
      $transaction: jest.fn(async (fn: (tx: typeof prisma) => Promise<void>) => fn(prisma)),
    };
    matchBetStats = { betStatsForMatches: jest.fn().mockResolvedValue(new Map()) };
    service = new CatalogArchiveService(prisma as never, matchBetStats as never);
  });

  const baseMatch = {
    id: matchId,
    status: 'PUBLISHED',
    isOutright: false,
    matchName: null,
    homeTeamId: BigInt(2),
    awayTeamId: BigInt(3),
    homeTeam: { code: 'A' },
    awayTeam: { code: 'B' },
    league: { id: leagueId },
  };

  it('preview flags pending bets and unsettled match', async () => {
    prisma.match.findFirst.mockResolvedValue(baseMatch);
    prisma.bet.findMany.mockResolvedValue([{ stake: new Decimal(50) }, { stake: new Decimal(25) }]);

    const preview = await service.getMatchArchivePreview(matchId);

    expect(preview.pendingBetCount).toBe(2);
    expect(preview.pendingStake).toBe('75');
    expect(preview.requiresForce).toBe(true);
    expect(preview.warnings).toEqual(expect.arrayContaining(['PENDING_BETS', 'UNSETTLED_MATCH']));
  });

  it('archive without force throws ARCHIVE_BLOCKED when warnings exist', async () => {
    prisma.match.findFirst.mockResolvedValue(baseMatch);
    prisma.bet.findMany.mockResolvedValue([{ stake: new Decimal(10) }]);

    await expect(service.archiveMatch(matchId, { force: false })).rejects.toMatchObject({
      response: expect.objectContaining({ code: 'ARCHIVE_BLOCKED' }),
    });
  });

  it('archives settled match with stale preview batch when force is true', async () => {
    prisma.match.findFirst.mockResolvedValue({ ...baseMatch, status: 'SETTLED' });
    prisma.bet.findMany.mockResolvedValue([]);
    prisma.settlementBatch.findFirst.mockResolvedValue({ id: BigInt(99) });

    const result = await service.archiveMatch(matchId, { force: true });

    expect(result.matchId).toBe(matchId.toString());
    expect(prisma.settlementBatch.deleteMany).toHaveBeenCalledWith({
      where: { matchId, status: 'PREVIEW' },
    });
    expect(prisma.match.update).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          deletedAt: expect.any(Date),
          status: 'SETTLED',
        }),
      }),
    );
  });

  it('archives settled match without force when no warnings', async () => {
    prisma.match.findFirst.mockResolvedValue({ ...baseMatch, status: 'SETTLED' });
    prisma.bet.findMany.mockResolvedValue([]);

    await service.archiveMatch(matchId, { force: false });

    expect(prisma.match.update).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({ status: 'SETTLED' }),
      }),
    );
  });

  it('archive with force rejects draft matches', async () => {
    prisma.match.findFirst.mockResolvedValue({ ...baseMatch, status: 'DRAFT' });

    await expect(service.archiveMatch(matchId, { force: true })).rejects.toMatchObject({
      response: expect.objectContaining({ code: 'MATCH_DELETE_DRAFT_ONLY' }),
    });
    expect(prisma.match.update).not.toHaveBeenCalled();
  });

  it('league preview blocks when child match is not terminal', async () => {
    prisma.league.findFirst.mockResolvedValue({ id: leagueId });
    prisma.match.findMany.mockResolvedValue([
      {
        id: matchId,
        status: 'CLOSED',
        isOutright: false,
        matchName: null,
        homeTeamId: BigInt(2),
        awayTeamId: BigInt(3),
        homeTeam: { code: 'A' },
        awayTeam: { code: 'B' },
      },
    ]);
    matchBetStats.betStatsForMatches.mockResolvedValue(
      new Map([[matchId.toString(), { betCount: 0, totalStake: '0', pendingCount: 0 }]]),
    );

    const preview = await service.getLeagueArchivePreview(leagueId);

    expect(preview.canArchive).toBe(false);
    expect(preview.blockingMatches).toHaveLength(1);
    expect(preview.blockingMatches[0].status).toBe('CLOSED');
  });

  it('league archive cascades when all children are settled', async () => {
    prisma.league.findFirst.mockResolvedValue({ id: leagueId });
    prisma.match.findMany
      .mockResolvedValueOnce([
        {
          id: matchId,
          status: 'SETTLED',
          isOutright: false,
          matchName: null,
          homeTeamId: BigInt(2),
          awayTeamId: BigInt(3),
          homeTeam: { code: 'A' },
          awayTeam: { code: 'B' },
        },
      ])
      .mockResolvedValueOnce([{ id: matchId, status: 'SETTLED' }]);
    matchBetStats.betStatsForMatches.mockResolvedValue(
      new Map([[matchId.toString(), { betCount: 1, totalStake: '100', pendingCount: 0 }]]),
    );

    const result = await service.archiveLeague(leagueId);

    expect(result.leagueId).toBe(leagueId.toString());
    expect(prisma.match.updateMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { id: { in: [matchId] } },
        data: { deletedAt: expect.any(Date) },
      }),
    );
    expect(prisma.league.update).toHaveBeenCalledWith(
      expect.objectContaining({
        data: { deletedAt: expect.any(Date), isActive: false },
      }),
    );
  });

  it('league archive throws LEAGUE_ARCHIVE_NOT_READY when blocked', async () => {
    prisma.league.findFirst.mockResolvedValue({ id: leagueId });
    prisma.match.findMany.mockResolvedValue([
      {
        id: matchId,
        status: 'PUBLISHED',
        isOutright: false,
        matchName: null,
        homeTeamId: BigInt(2),
        awayTeamId: BigInt(3),
        homeTeam: { code: 'A' },
        awayTeam: { code: 'B' },
      },
    ]);
    matchBetStats.betStatsForMatches.mockResolvedValue(
      new Map([[matchId.toString(), { betCount: 0, totalStake: '0', pendingCount: 0 }]]),
    );

    await expect(service.archiveLeague(leagueId)).rejects.toMatchObject({
      response: expect.objectContaining({ code: 'LEAGUE_ARCHIVE_NOT_READY' }),
    });
  });
});
