import { ForbiddenException, NotFoundException } from '@nestjs/common';
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 { GenerateReportCommand } from '../../application/commands/generate-report/generate-report.command';
import { GenerateReportHandler } from '../../application/commands/generate-report/generate-report.handler';
import { DeleteReportCommand } from '../../application/commands/delete-report/delete-report.command';
import { DeleteReportHandler } from '../../application/commands/delete-report/delete-report.handler';
import { GetReportQuery } from '../../application/queries/get-report/get-report.query';
import { GetReportHandler } from '../../application/queries/get-report/get-report.handler';
import { ListReportsQuery } from '../../application/queries/list-reports/list-reports.query';
import { ListReportsHandler } from '../../application/queries/list-reports/list-reports.handler';
import { ReportGenerationProcessor } from '../services/report-generation.processor';

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

/**
 * E2E-style integration test for the report generation pipeline.
 *
 * Tests the full flow: command → repository → BullMQ job → processor →
 * data fetch → AI narrative → PDF generation → storage → status transitions.
 *
 * All external services are mocked at the boundary (repository, queue, AI, PDF, storage)
 * but the pipeline logic is tested end-to-end across handler → processor → entity.
 */
describe('Report Generation Pipeline (Integration)', () => {
  // ── Shared mocks ──────────────────────────────────────────────────
  type MockRepo = { [K in keyof IReportRepository]: ReturnType<typeof vi.fn> };
  type MockQueue = { add: ReturnType<typeof vi.fn> };

  let mockRepo: MockRepo;
  let mockQueue: MockQueue;
  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> };

  // ── Handlers ──────────────────────────────────────────────────────
  let generateHandler: GenerateReportHandler;
  let getHandler: GetReportHandler;
  let listHandler: ListReportsHandler;
  let deleteHandler: DeleteReportHandler;
  let processor: ReportGenerationProcessor;

  // In-memory report store to simulate persistence across handlers
  const reportStore = new Map<string, ReportEntity>();

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

    // ── Repository mock backed by in-memory store ──
    mockRepo = {
      findById: vi.fn(async (id: string) => reportStore.get(id) ?? null),
      findByUserId: vi.fn(async (filter: { userId: string; type?: ReportType; limit?: number; offset?: number }) => {
        const all = [...reportStore.values()].filter((r) => r.userId === filter.userId);
        const filtered = filter.type ? all.filter((r) => r.type === filter.type) : all;
        const offset = filter.offset ?? 0;
        const limit = filter.limit ?? 20;
        return {
          reports: filtered.slice(offset, offset + limit),
          total: filtered.length,
        };
      }),
      save: vi.fn(async (entity: ReportEntity) => {
        reportStore.set(entity.id, entity);
      }),
      update: vi.fn(async (entity: ReportEntity) => {
        reportStore.set(entity.id, entity);
      }),
      delete: vi.fn(async (id: string) => {
        reportStore.delete(id);
      }),
      countByUserInPeriod: vi.fn().mockResolvedValue(0),
    };

    mockQueue = {
      add: vi.fn().mockResolvedValue(undefined),
    };

    mockMacroData = {
      getByProvince: vi.fn().mockResolvedValue([
        { indicator: 'gdp', period: '2025', value: 150000, unit: 'tỷ VND' },
        { indicator: 'fdi', period: '2025', value: 5000, unit: 'triệu USD' },
        { indicator: 'population', period: '2025', value: 2100000, unit: 'người' },
        { indicator: 'urbanization', period: '2025', value: 78, unit: '%' },
        { indicator: 'labor', period: '2025', value: 1400000, unit: 'người' },
        { indicator: 'wage', period: '2025', value: 8500000, unit: 'VND/tháng' },
        { indicator: 'industrial_output', period: '2025', value: 95000, unit: 'tỷ VND' },
      ]),
    };

    mockInfraData = {
      getByProvince: vi.fn().mockResolvedValue([
        {
          name: 'KCN VSIP II-A',
          category: 'industrial_park',
          status: 'active',
          investmentVND: BigInt(5000000000000),
          completionDate: new Date('2024-06-01'),
        },
        {
          name: 'Cầu Mỹ Phước - Tân Vạn',
          category: 'road',
          status: 'completed',
          investmentVND: BigInt(3000000000000),
          completionDate: new Date('2023-12-01'),
        },
      ]),
    };

    mockAINarrative = {
      generateNarrative: vi.fn().mockResolvedValue('Phân tích chuyên sâu do AI tạo.'),
    };

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

    mockPdfStorage = {
      uploadPdf: vi.fn().mockResolvedValue('https://cdn.goodgo.vn/reports/test-report.pdf'),
    };

    // Wire handlers
    generateHandler = new GenerateReportHandler(mockRepo as any, mockQueue as any);
    getHandler = new GetReportHandler(mockRepo as any);
    listHandler = new ListReportsHandler(mockRepo as any);
    deleteHandler = new DeleteReportHandler(mockRepo as any);
    processor = new ReportGenerationProcessor(
      mockRepo as any,
      mockMacroData as any,
      mockInfraData as any,
      mockAINarrative as any,
      mockPdfGenerator as any,
      mockPdfStorage as any,
    );
  });

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

  // ================================================================
  //  1. Full pipeline: generate → queue → process → READY
  // ================================================================
  describe('Full pipeline flow', () => {
    it('INDUSTRIAL_LOCATION: generate → enqueue → process → READY with PDF', async () => {
      // Step 1: Generate (creates entity + enqueues job)
      const result = await generateHandler.execute(
        new GenerateReportCommand(
          'user-1',
          ReportType.INDUSTRIAL_LOCATION,
          'Báo cáo KCN Bình Dương Q2-2026',
          { province: 'Bình Dương' },
        ),
      );

      expect(result.reportId).toBeDefined();
      expect(mockQueue.add).toHaveBeenCalledWith(
        'generate',
        { reportId: result.reportId },
        expect.objectContaining({ attempts: 2, backoff: expect.any(Object) }),
      );

      // Verify initial state
      const pending = reportStore.get(result.reportId)!;
      expect(pending.status).toBe(ReportStatus.GENERATING);
      expect(pending.content).toBeNull();
      expect(pending.pdfUrl).toBeNull();

      // Step 2: Process (simulates BullMQ worker)
      await processor.process(makeJob(result.reportId));

      // Step 3: Verify final state
      const completed = reportStore.get(result.reportId)!;
      expect(completed.status).toBe(ReportStatus.READY);
      expect(completed.content).toBeTruthy();
      expect(completed.pdfUrl).toBe('https://cdn.goodgo.vn/reports/test-report.pdf');

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

      // Verify AI narratives (4 sections for industrial)
      expect(mockAINarrative.generateNarrative).toHaveBeenCalledTimes(4);

      // Verify PDF generation + upload
      expect(mockPdfGenerator.generatePdf).toHaveBeenCalledOnce();
      expect(mockPdfStorage.uploadPdf).toHaveBeenCalledOnce();
    });

    it('RESIDENTIAL_MARKET: generate → process → READY with 6 narratives', async () => {
      const result = await generateHandler.execute(
        new GenerateReportCommand(
          'user-1',
          ReportType.RESIDENTIAL_MARKET,
          'Thị trường nhà ở TP.HCM',
          { city: 'TP.HCM', period: 'Q2-2026' },
        ),
      );

      await processor.process(makeJob(result.reportId));

      const report = reportStore.get(result.reportId)!;
      expect(report.status).toBe(ReportStatus.READY);
      expect(mockAINarrative.generateNarrative).toHaveBeenCalledTimes(6);
      expect(mockMacroData.getByProvince).toHaveBeenCalledWith(
        'TP.HCM',
        expect.arrayContaining(['gdp', 'cpi', 'mortgage_rate']),
      );
    });

    it('DISTRICT_ANALYSIS: generate → process → READY with 5 narratives', async () => {
      const result = await generateHandler.execute(
        new GenerateReportCommand(
          'user-1',
          ReportType.DISTRICT_ANALYSIS,
          'Phân tích Quận 2',
          { city: 'TP.HCM', district: 'Quận 2' },
        ),
      );

      await processor.process(makeJob(result.reportId));

      const report = reportStore.get(result.reportId)!;
      expect(report.status).toBe(ReportStatus.READY);
      expect(mockAINarrative.generateNarrative).toHaveBeenCalledTimes(5);
    });
  });

  // ================================================================
  //  2. Status polling flow: GET /reports/:id/status
  // ================================================================
  describe('Status polling flow', () => {
    it('returns GENERATING status before processing, READY after', async () => {
      const result = await generateHandler.execute(
        new GenerateReportCommand(
          'user-1',
          ReportType.INDUSTRIAL_LOCATION,
          'Status Test Report',
          { province: 'Đồng Nai' },
        ),
      );

      // Poll 1: status should be GENERATING
      const before = await getHandler.execute(new GetReportQuery(result.reportId, 'user-1'));
      expect(before.status).toBe(ReportStatus.GENERATING);
      expect(before.pdfUrl).toBeNull();
      expect(before.errorMsg).toBeNull();

      // Process the job
      await processor.process(makeJob(result.reportId));

      // Poll 2: status should be READY
      const after = await getHandler.execute(new GetReportQuery(result.reportId, 'user-1'));
      expect(after.status).toBe(ReportStatus.READY);
      expect(after.pdfUrl).toBe('https://cdn.goodgo.vn/reports/test-report.pdf');
    });

    it('returns FAILED status when processing fails', async () => {
      mockAINarrative.generateNarrative.mockRejectedValue(new Error('Claude API timeout'));

      const result = await generateHandler.execute(
        new GenerateReportCommand(
          'user-1',
          ReportType.INDUSTRIAL_LOCATION,
          'Failing Report',
          { province: 'Hà Nội' },
        ),
      );

      // Process fails
      await expect(processor.process(makeJob(result.reportId))).rejects.toThrow('Claude API timeout');

      // Poll: status should be FAILED
      const report = await getHandler.execute(new GetReportQuery(result.reportId, 'user-1'));
      expect(report.status).toBe(ReportStatus.FAILED);
      expect(report.errorMsg).toBe('Claude API timeout');
      expect(report.pdfUrl).toBeNull();
    });
  });

  // ================================================================
  //  3. Quota enforcement
  // ================================================================
  describe('Quota enforcement', () => {
    it('countByUserInPeriod returns correct count for usage tracking', async () => {
      // Generate multiple reports
      await generateHandler.execute(
        new GenerateReportCommand('user-1', ReportType.RESIDENTIAL_MARKET, 'Report 1', {}),
      );
      await generateHandler.execute(
        new GenerateReportCommand('user-1', ReportType.DISTRICT_ANALYSIS, 'Report 2', {}),
      );
      await generateHandler.execute(
        new GenerateReportCommand('user-2', ReportType.INDUSTRIAL_LOCATION, 'Report 3', {}),
      );

      // The repo save was called 3 times
      expect(mockRepo.save).toHaveBeenCalledTimes(3);

      // Verify queue was called for each
      expect(mockQueue.add).toHaveBeenCalledTimes(3);
    });

    it('QuotaGuard blocks generation when quota is exceeded', async () => {
      // Simulate quota exceeded scenario via countByUserInPeriod
      mockRepo.countByUserInPeriod.mockResolvedValue(10);

      const count = await mockRepo.countByUserInPeriod(
        'user-1',
        new Date('2026-04-01'),
        new Date('2026-04-30'),
      );

      expect(count).toBe(10);
      // When maxReports is 5, QuotaGuard would throw ForbiddenException
      // before the handler is even called
    });

    it('list reports scoped to user — no cross-user leakage', async () => {
      await generateHandler.execute(
        new GenerateReportCommand('user-1', ReportType.RESIDENTIAL_MARKET, 'User1 Report', {}),
      );
      await generateHandler.execute(
        new GenerateReportCommand('user-2', ReportType.INDUSTRIAL_LOCATION, 'User2 Report', {}),
      );

      const user1Reports = await listHandler.execute(new ListReportsQuery('user-1'));
      const user2Reports = await listHandler.execute(new ListReportsQuery('user-2'));

      expect(user1Reports.reports).toHaveLength(1);
      expect(user1Reports.total).toBe(1);
      expect(user1Reports.reports[0]!.title).toBe('User1 Report');

      expect(user2Reports.reports).toHaveLength(1);
      expect(user2Reports.total).toBe(1);
      expect(user2Reports.reports[0]!.title).toBe('User2 Report');
    });

    it('list reports filters by type', async () => {
      await generateHandler.execute(
        new GenerateReportCommand('user-1', ReportType.RESIDENTIAL_MARKET, 'Residential', {}),
      );
      await generateHandler.execute(
        new GenerateReportCommand('user-1', ReportType.INDUSTRIAL_LOCATION, 'Industrial', {}),
      );

      const filtered = await listHandler.execute(
        new ListReportsQuery('user-1', ReportType.INDUSTRIAL_LOCATION),
      );

      expect(filtered.reports).toHaveLength(1);
      expect(filtered.reports[0]!.type).toBe(ReportType.INDUSTRIAL_LOCATION);
    });
  });

  // ================================================================
  //  4. Error handling and edge cases
  // ================================================================
  describe('Error handling', () => {
    it('processor skips when report not found in DB', async () => {
      await processor.process(makeJob('nonexistent-id'));

      expect(mockAINarrative.generateNarrative).not.toHaveBeenCalled();
      expect(mockPdfGenerator.generatePdf).not.toHaveBeenCalled();
      expect(mockRepo.update).not.toHaveBeenCalled();
    });

    it('marks READY without PDF when PDF generation fails', async () => {
      mockPdfGenerator.generatePdf.mockRejectedValue(new Error('Puppeteer crashed'));

      const result = await generateHandler.execute(
        new GenerateReportCommand(
          'user-1',
          ReportType.DISTRICT_ANALYSIS,
          'PDF Fail Test',
          { city: 'Hà Nội', district: 'Hoàn Kiếm' },
        ),
      );

      await processor.process(makeJob(result.reportId));

      const report = reportStore.get(result.reportId)!;
      expect(report.status).toBe(ReportStatus.READY);
      expect(report.content).toBeTruthy();
      expect(report.pdfUrl).toBeNull();
    });

    it('marks FAILED and throws when AI narrative generation fails completely', async () => {
      mockAINarrative.generateNarrative.mockRejectedValue(new Error('Rate limit exceeded'));

      const result = await generateHandler.execute(
        new GenerateReportCommand(
          'user-1',
          ReportType.INDUSTRIAL_LOCATION,
          'AI Fail Test',
          { province: 'Long An' },
        ),
      );

      await expect(processor.process(makeJob(result.reportId))).rejects.toThrow('Rate limit exceeded');

      const report = reportStore.get(result.reportId)!;
      expect(report.status).toBe(ReportStatus.FAILED);
      expect(report.errorMsg).toBe('Rate limit exceeded');
    });

    it('get report throws NotFoundException for missing report', async () => {
      await expect(
        getHandler.execute(new GetReportQuery('nonexistent', 'user-1')),
      ).rejects.toThrow(NotFoundException);
    });

    it('get report throws ForbiddenException for wrong user', async () => {
      const result = await generateHandler.execute(
        new GenerateReportCommand('user-1', ReportType.PORTFOLIO, 'Private', {}),
      );

      await expect(
        getHandler.execute(new GetReportQuery(result.reportId, 'user-other')),
      ).rejects.toThrow(ForbiddenException);
    });
  });

  // ================================================================
  //  5. DELETE /reports/:id cleanup
  // ================================================================
  describe('Delete report cleanup', () => {
    it('deletes a completed report for the owner', async () => {
      const result = await generateHandler.execute(
        new GenerateReportCommand('user-1', ReportType.RESIDENTIAL_MARKET, 'To Delete', {}),
      );

      await processor.process(makeJob(result.reportId));
      expect(reportStore.has(result.reportId)).toBe(true);

      await deleteHandler.execute(new DeleteReportCommand(result.reportId, 'user-1'));
      expect(reportStore.has(result.reportId)).toBe(false);
    });

    it('deletes a GENERATING report before processing', async () => {
      const result = await generateHandler.execute(
        new GenerateReportCommand('user-1', ReportType.PORTFOLIO, 'Cancelled', {}),
      );

      expect(reportStore.get(result.reportId)!.status).toBe(ReportStatus.GENERATING);

      await deleteHandler.execute(new DeleteReportCommand(result.reportId, 'user-1'));
      expect(reportStore.has(result.reportId)).toBe(false);
    });

    it('delete throws NotFoundException for missing report', async () => {
      await expect(
        deleteHandler.execute(new DeleteReportCommand('nonexistent', 'user-1')),
      ).rejects.toThrow(NotFoundException);
    });

    it('delete throws ForbiddenException for wrong user', async () => {
      const result = await generateHandler.execute(
        new GenerateReportCommand('user-1', ReportType.RESIDENTIAL_MARKET, 'Not Yours', {}),
      );

      await expect(
        deleteHandler.execute(new DeleteReportCommand(result.reportId, 'user-other')),
      ).rejects.toThrow(ForbiddenException);
    });

    it('report no longer retrievable after deletion', async () => {
      const result = await generateHandler.execute(
        new GenerateReportCommand('user-1', ReportType.DISTRICT_ANALYSIS, 'Gone', {}),
      );

      await deleteHandler.execute(new DeleteReportCommand(result.reportId, 'user-1'));

      await expect(
        getHandler.execute(new GetReportQuery(result.reportId, 'user-1')),
      ).rejects.toThrow(NotFoundException);

      const listResult = await listHandler.execute(new ListReportsQuery('user-1'));
      expect(listResult.reports).toHaveLength(0);
    });
  });

  // ================================================================
  //  6. Report type coverage
  // ================================================================
  describe('All report types complete successfully', () => {
    const reportTypes: Array<{ type: ReportType; params: Record<string, unknown>; expectedNarratives: number }> = [
      { type: ReportType.INDUSTRIAL_LOCATION, params: { province: 'Bình Dương' }, expectedNarratives: 4 },
      { type: ReportType.RESIDENTIAL_MARKET, params: { city: 'TP.HCM', period: 'Q2-2026' }, expectedNarratives: 6 },
      { type: ReportType.DISTRICT_ANALYSIS, params: { city: 'TP.HCM', district: 'Quận 7' }, expectedNarratives: 5 },
      { type: ReportType.PORTFOLIO, params: { assets: ['prop-1'] }, expectedNarratives: 1 },
      { type: ReportType.INVESTMENT_FEASIBILITY, params: { scenario: 'test' }, expectedNarratives: 1 },
      { type: ReportType.PROPERTY_VALUATION, params: { propertyId: 'p-1' }, expectedNarratives: 1 },
    ];

    it.each(reportTypes)(
      '$type → generates $expectedNarratives narratives and completes READY',
      async ({ type, params, expectedNarratives }) => {
        const result = await generateHandler.execute(
          new GenerateReportCommand('user-1', type, `Report: ${type}`, params),
        );

        await processor.process(makeJob(result.reportId));

        const report = reportStore.get(result.reportId)!;
        expect(report.status).toBe(ReportStatus.READY);
        expect(report.content).toBeTruthy();
        expect(mockAINarrative.generateNarrative).toHaveBeenCalledTimes(expectedNarratives);

        // Reset mock for next iteration
        mockAINarrative.generateNarrative.mockClear();
      },
    );
  });

  // ================================================================
  //  7. Temp file cleanup
  // ================================================================
  describe('Temp file lifecycle', () => {
    it('cleans up temp PDF file after successful upload', async () => {
      const fs = await import('fs');

      const result = await generateHandler.execute(
        new GenerateReportCommand('user-1', ReportType.DISTRICT_ANALYSIS, 'Cleanup Test', { city: 'Hà Nội', district: 'Ba Đình' }),
      );

      await processor.process(makeJob(result.reportId));

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