import type { TestingModule } from '@nestjs/testing';
import { Test } from '@nestjs/testing';
import { NotFoundException } from '@nestjs/common';
import { ReportsService } from './reports.service';
import { PrismaService } from '../../prisma/prisma.service';
import { StatsService } from '../stats/stats.service';

const ReportStatus = {
  PENDING: 'PENDING',
  GENERATING: 'GENERATING',
  COMPLETED: 'COMPLETED',
  FAILED: 'FAILED',
};

jest.mock('@prisma/client', () => ({
  PrismaClient: class PrismaClient {},
  ReportStatus: {
    PENDING: 'PENDING',
    GENERATING: 'GENERATING',
    COMPLETED: 'COMPLETED',
    FAILED: 'FAILED',
  },
  Prisma: {},
}));

describe('ReportsService', () => {
  let service: ReportsService;

  const mockPrisma = {
    report: {
      findMany: jest.fn(),
      findFirst: jest.fn(),
      create: jest.fn(),
      update: jest.fn(),
    },
  };

  const CLUB_ID = 'club-uuid-1';
  const USER_ID = 'user-uuid-1';
  const REPORT_ID = 'report-uuid-1';

  const mockReport = {
    id: REPORT_ID,
    clubId: CLUB_ID,
    generatedById: USER_ID,
    type: 'clients_summary',
    title: 'Client Summary Report',
    parametersJson: {},
    status: ReportStatus.COMPLETED,
    fileUrl: 'https://s3.example.com/reports/report.pdf',
    createdAt: new Date('2026-01-01T10:00:00Z'),
    updatedAt: new Date('2026-01-01T10:05:00Z'),
    generatedBy: {
      id: USER_ID,
      firstName: 'Ivan',
      lastName: 'Petrov',
    },
  };

  beforeEach(async () => {
    jest.clearAllMocks();

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        ReportsService,
        { provide: PrismaService, useValue: mockPrisma },
        { provide: StatsService, useValue: {} },
      ],
    }).compile();

    service = module.get<ReportsService>(ReportsService);
  });

  // ─── findAll ──────────────────────────────────────────────────────────────

  describe('findAll', () => {
    it('returns paginated reports without filters', async () => {
      mockPrisma.report.findMany.mockResolvedValue([mockReport]);

      const result = await service.findAll(CLUB_ID, { limit: 20 });

      expect(mockPrisma.report.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { clubId: CLUB_ID },
          take: 21,
          orderBy: { createdAt: 'desc' },
          include: expect.objectContaining({ generatedBy: expect.any(Object) }),
        }),
      );
      expect(result.data).toHaveLength(1);
      expect(result.hasMore).toBe(false);
      expect(result.nextCursor).toBeNull();
    });

    it('sets hasMore and nextCursor when there are more items than limit', async () => {
      const reports = Array.from({ length: 3 }, (_, i) => ({
        ...mockReport,
        id: `report-${i}`,
      }));
      mockPrisma.report.findMany.mockResolvedValue(reports);

      const result = await service.findAll(CLUB_ID, { limit: 2 });

      expect(result.hasMore).toBe(true);
      expect(result.data).toHaveLength(2);
      expect(result.nextCursor).toBe('report-1');
    });

    it('applies cursor-based pagination when cursor is provided', async () => {
      mockPrisma.report.findMany.mockResolvedValue([mockReport]);

      await service.findAll(CLUB_ID, { cursor: 'prev-cursor-id', limit: 20 });

      expect(mockPrisma.report.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          cursor: { id: 'prev-cursor-id' },
          skip: 1,
        }),
      );
    });

    it('filters by status and type when provided', async () => {
      mockPrisma.report.findMany.mockResolvedValue([]);

      await service.findAll(CLUB_ID, {
        limit: 20,
        status: ReportStatus.COMPLETED as any,
        type: 'clients_summary',
      });

      expect(mockPrisma.report.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: {
            clubId: CLUB_ID,
            status: ReportStatus.COMPLETED,
            type: 'clients_summary',
          },
        }),
      );
    });
  });

  // ─── findById ─────────────────────────────────────────────────────────────

  describe('findById', () => {
    it('returns a report when found', async () => {
      mockPrisma.report.findFirst.mockResolvedValue(mockReport);

      const result = await service.findById(CLUB_ID, REPORT_ID);

      expect(mockPrisma.report.findFirst).toHaveBeenCalledWith({
        where: { id: REPORT_ID, clubId: CLUB_ID },
        include: expect.objectContaining({ generatedBy: expect.any(Object) }),
      });
      expect(result).toEqual(mockReport);
    });

    it('throws NotFoundException when report is not found', async () => {
      mockPrisma.report.findFirst.mockResolvedValue(null);

      await expect(service.findById(CLUB_ID, 'nonexistent-id')).rejects.toThrow(
        new NotFoundException('Report with id nonexistent-id not found'),
      );
    });
  });

  // ─── generate ─────────────────────────────────────────────────────────────

  describe('generate', () => {
    const generateDto = {
      type: 'clients_summary' as any,
      title: 'My Report',
      parameters: { dateFrom: '2026-01-01', dateTo: '2026-01-31' },
    };

    it('creates a report with PENDING status and then updates to GENERATING', async () => {
      const createdReport = {
        ...mockReport,
        status: ReportStatus.PENDING,
      };
      mockPrisma.report.create.mockResolvedValue(createdReport);
      mockPrisma.report.update.mockResolvedValue({
        ...createdReport,
        status: ReportStatus.GENERATING,
      });

      const result = await service.generate(CLUB_ID, USER_ID, generateDto);

      expect(mockPrisma.report.create).toHaveBeenCalledWith({
        data: {
          clubId: CLUB_ID,
          generatedById: USER_ID,
          type: generateDto.type,
          title: generateDto.title,
          parametersJson: generateDto.parameters,
          status: ReportStatus.PENDING,
        },
        include: expect.objectContaining({ generatedBy: expect.any(Object) }),
      });
      expect(mockPrisma.report.update).toHaveBeenCalledWith({
        where: { id: createdReport.id },
        data: { status: ReportStatus.GENERATING },
      });
      expect(result.status).toBe(ReportStatus.GENERATING);
    });

    it('returns report spread with GENERATING status regardless of DB update response', async () => {
      const pendingReport = {
        ...mockReport,
        id: 'new-report-id',
        status: ReportStatus.PENDING,
      };
      mockPrisma.report.create.mockResolvedValue(pendingReport);
      mockPrisma.report.update.mockResolvedValue({
        ...pendingReport,
        status: ReportStatus.GENERATING,
      });

      const result = await service.generate(CLUB_ID, USER_ID, generateDto);

      expect(result.status).toBe(ReportStatus.GENERATING);
      expect(result.id).toBe('new-report-id');
    });

    it('uses empty object for parameters when none are provided', async () => {
      const dtoWithoutParams = { type: 'sales_summary' as any, title: 'Sales' };
      const pendingReport = { ...mockReport, status: ReportStatus.PENDING };
      mockPrisma.report.create.mockResolvedValue(pendingReport);
      mockPrisma.report.update.mockResolvedValue(pendingReport);

      await service.generate(CLUB_ID, USER_ID, dtoWithoutParams);

      expect(mockPrisma.report.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({ parametersJson: {} }),
        }),
      );
    });
  });

  // ─── getDownloadUrl ───────────────────────────────────────────────────────

  describe('getDownloadUrl', () => {
    it('returns fileUrl when report is COMPLETED and has a file', async () => {
      mockPrisma.report.findFirst.mockResolvedValue(mockReport);

      const result = await service.getDownloadUrl(CLUB_ID, REPORT_ID);

      expect(result).toEqual({ fileUrl: mockReport.fileUrl });
    });

    it('throws NotFoundException when report does not exist', async () => {
      mockPrisma.report.findFirst.mockResolvedValue(null);

      await expect(service.getDownloadUrl(CLUB_ID, 'nonexistent-id')).rejects.toThrow(
        new NotFoundException('Report with id nonexistent-id not found'),
      );
    });

    it('throws NotFoundException when report is not yet COMPLETED', async () => {
      mockPrisma.report.findFirst.mockResolvedValue({
        ...mockReport,
        status: ReportStatus.GENERATING,
        fileUrl: null,
      });

      await expect(service.getDownloadUrl(CLUB_ID, REPORT_ID)).rejects.toThrow(
        new NotFoundException('Report file is not available yet'),
      );
    });

    it('throws NotFoundException when report is COMPLETED but fileUrl is missing', async () => {
      mockPrisma.report.findFirst.mockResolvedValue({
        ...mockReport,
        status: ReportStatus.COMPLETED,
        fileUrl: null,
      });

      await expect(service.getDownloadUrl(CLUB_ID, REPORT_ID)).rejects.toThrow(
        new NotFoundException('Report file is not available yet'),
      );
    });
  });
});
