import { ReportEntity } from '../../domain/entities/report.entity';
import { ReportStatus } from '../../domain/enums/report-status.enum';
import { ReportType } from '../../domain/enums/report-type.enum';
import { type IReportRepository } from '../../domain/repositories/report.repository';
import { type IAINarrativeService } from '../../domain/services/ai-narrative.service';
import { type IInfrastructureDataService } from '../../domain/services/infrastructure-data.service';
import { type IMacroDataService } from '../../domain/services/macro-data.service';
import { type IPdfGeneratorService } from '../../domain/services/pdf-generator.service';
import { type IPdfStorageService } from '../../domain/services/pdf-storage.service';
import { ReportGenerationProcessor } from '../services/report-generation.processor';

// Mock fs
vi.mock('fs', () => ({
  readFileSync: vi.fn().mockReturnValue(Buffer.from('fake-pdf')),
  unlinkSync: vi.fn(),
}));

describe('ReportGenerationProcessor', () => {
  let processor: ReportGenerationProcessor;
  let mockReportRepo: { [K in keyof IReportRepository]: ReturnType<typeof vi.fn> };
  let mockMacroData: { [K in keyof IMacroDataService]: ReturnType<typeof vi.fn> };
  let mockInfraData: { [K in keyof IInfrastructureDataService]: ReturnType<typeof vi.fn> };
  let mockAINarrative: { [K in keyof IAINarrativeService]: ReturnType<typeof vi.fn> };
  let mockPdfGenerator: { [K in keyof IPdfGeneratorService]: ReturnType<typeof vi.fn> };
  let mockPdfStorage: { [K in keyof IPdfStorageService]: ReturnType<typeof vi.fn> };

  const createReport = (type: ReportType, params: Record<string, unknown>) =>
    ReportEntity.createNew('report-1', 'user-1', type, 'Test Report', params);

  beforeEach(() => {
    vi.clearAllMocks();

    mockReportRepo = {
      findById: vi.fn(),
      findByUserId: vi.fn(),
      save: vi.fn(),
      update: vi.fn(),
      delete: vi.fn(),
      countByUserInPeriod: vi.fn(),
    };

    mockMacroData = {
      getByProvince: vi.fn().mockResolvedValue([
        { indicator: 'gdp', period: '2025', value: 150000, unit: 'tỷ VND' },
        { indicator: 'fdi', period: '2025', value: 5000, unit: 'triệu USD' },
      ]),
    };

    mockInfraData = {
      getByProvince: vi.fn().mockResolvedValue([
        { name: 'KCN VSIP', category: 'industrial_park', status: 'active', investmentVND: BigInt(5000000000000), completionDate: new Date('2024-01-01') },
      ]),
    };

    mockAINarrative = {
      generateNarrative: vi.fn().mockResolvedValue('AI-generated analysis text.'),
    };

    mockPdfGenerator = {
      generatePdf: vi.fn().mockResolvedValue('/tmp/report.pdf'),
    };

    mockPdfStorage = {
      uploadPdf: vi.fn().mockResolvedValue('https://storage.example.com/reports/report-1.pdf'),
    };

    processor = new ReportGenerationProcessor(
      mockReportRepo as any,
      mockMacroData as any,
      mockInfraData as any,
      mockAINarrative as any,
      mockPdfGenerator as any,
      mockPdfStorage as any,
    );
  });

  const makeJob = (reportId: string) => ({ data: { reportId } }) as any;

  it('skips processing when report is not found', async () => {
    mockReportRepo.findById.mockResolvedValue(null);

    await processor.process(makeJob('nonexistent'));

    expect(mockPdfGenerator.generatePdf).not.toHaveBeenCalled();
    expect(mockReportRepo.update).not.toHaveBeenCalled();
  });

  describe('INDUSTRIAL_LOCATION report', () => {
    it('fetches macro data and infra projects, generates narratives, creates PDF', async () => {
      const report = createReport(ReportType.INDUSTRIAL_LOCATION, { province: 'Bình Dương' });
      mockReportRepo.findById.mockResolvedValue(report);

      await processor.process(makeJob('report-1'));

      // Fetches data
      expect(mockMacroData.getByProvince).toHaveBeenCalledWith(
        'Bình Dương',
        expect.arrayContaining(['gdp', 'fdi', 'population']),
      );
      expect(mockInfraData.getByProvince).toHaveBeenCalledWith('Bình Dương');

      // Generates AI narratives for 4 sections
      expect(mockAINarrative.generateNarrative).toHaveBeenCalledTimes(4);
      expect(mockAINarrative.generateNarrative).toHaveBeenCalledWith(
        expect.objectContaining({ sectionKey: 'executive_summary' }),
      );

      // Generates and uploads PDF
      expect(mockPdfGenerator.generatePdf).toHaveBeenCalledOnce();
      expect(mockPdfStorage.uploadPdf).toHaveBeenCalledOnce();

      // Marks report as ready with pdfUrl
      expect(mockReportRepo.update).toHaveBeenCalledOnce();
      expect(report.status).toBe(ReportStatus.READY);
      expect(report.pdfUrl).toBe('https://storage.example.com/reports/report-1.pdf');
    });
  });

  describe('RESIDENTIAL_MARKET report', () => {
    it('fetches macro data and generates narratives for market sections', async () => {
      const report = createReport(ReportType.RESIDENTIAL_MARKET, { city: 'TP.HCM', period: 'Q1-2026' });
      mockReportRepo.findById.mockResolvedValue(report);

      await processor.process(makeJob('report-1'));

      expect(mockMacroData.getByProvince).toHaveBeenCalledWith(
        'TP.HCM',
        expect.arrayContaining(['gdp', 'cpi', 'mortgage_rate']),
      );

      // 6 narrative sections for residential market
      expect(mockAINarrative.generateNarrative).toHaveBeenCalledTimes(6);

      expect(report.status).toBe(ReportStatus.READY);
      expect(report.content).toBeTruthy();
    });
  });

  describe('DISTRICT_ANALYSIS report', () => {
    it('generates narratives for district sections', async () => {
      const report = createReport(ReportType.DISTRICT_ANALYSIS, { city: 'TP.HCM', district: 'Quận 2' });
      mockReportRepo.findById.mockResolvedValue(report);

      await processor.process(makeJob('report-1'));

      // 5 narrative sections for district analysis
      expect(mockAINarrative.generateNarrative).toHaveBeenCalledTimes(5);

      expect(report.status).toBe(ReportStatus.READY);
    });
  });

  describe('generic report type', () => {
    it('generates a single executive summary for unknown report types', async () => {
      const report = createReport(ReportType.PORTFOLIO, { assets: ['prop-1'] });
      mockReportRepo.findById.mockResolvedValue(report);

      await processor.process(makeJob('report-1'));

      expect(mockAINarrative.generateNarrative).toHaveBeenCalledOnce();
      expect(mockAINarrative.generateNarrative).toHaveBeenCalledWith(
        expect.objectContaining({ sectionKey: 'executive_summary' }),
      );

      expect(report.status).toBe(ReportStatus.READY);
    });
  });

  describe('PDF generation failure', () => {
    it('completes report without PDF when PDF generation fails', async () => {
      const report = createReport(ReportType.DISTRICT_ANALYSIS, { city: 'Hà Nội', district: 'Hoàn Kiếm' });
      mockReportRepo.findById.mockResolvedValue(report);
      mockPdfGenerator.generatePdf.mockRejectedValue(new Error('Puppeteer crashed'));

      await processor.process(makeJob('report-1'));

      // Report should still be marked ready, but without pdfUrl
      expect(report.status).toBe(ReportStatus.READY);
      expect(report.pdfUrl).toBeNull();
      expect(mockReportRepo.update).toHaveBeenCalledOnce();
    });
  });

  describe('content generation failure', () => {
    it('marks report as failed when narrative generation throws', async () => {
      const report = createReport(ReportType.INDUSTRIAL_LOCATION, { province: 'Đồng Nai' });
      mockReportRepo.findById.mockResolvedValue(report);
      mockAINarrative.generateNarrative.mockRejectedValue(new Error('AI service unavailable'));

      await expect(processor.process(makeJob('report-1'))).rejects.toThrow('AI service unavailable');

      expect(report.status).toBe(ReportStatus.FAILED);
      expect(report.errorMsg).toBe('AI service unavailable');
      expect(mockReportRepo.update).toHaveBeenCalledOnce();
    });
  });

  it('cleans up temp PDF file after upload', async () => {
    const fs = await import('fs');
    const report = createReport(ReportType.DISTRICT_ANALYSIS, { city: 'TP.HCM', district: 'Quận 1' });
    mockReportRepo.findById.mockResolvedValue(report);
    mockPdfGenerator.generatePdf.mockResolvedValue('/tmp/goodgo-report-1.pdf');

    await processor.process(makeJob('report-1'));

    expect(fs.readFileSync).toHaveBeenCalledWith('/tmp/goodgo-report-1.pdf');
    expect(fs.unlinkSync).toHaveBeenCalledWith('/tmp/goodgo-report-1.pdf');
  });
});
