import { documentProcessingService } from '../documentProcessingService';
import { DocumentModel } from '../../models/DocumentModel';
import { ProcessingJobModel } from '../../models/ProcessingJobModel';
import { fileStorageService } from '../fileStorageService';
import { llmService } from '../llmService';
import { pdfGenerationService } from '../pdfGenerationService';
import { config } from '../../config/env';
import fs from 'fs';
import path from 'path';

// Mock dependencies
jest.mock('../../models/DocumentModel');
jest.mock('../../models/ProcessingJobModel');
jest.mock('../fileStorageService');
jest.mock('../llmService');
jest.mock('../pdfGenerationService');
jest.mock('../../config/env');
jest.mock('fs');
jest.mock('path');

const mockDocumentModel = DocumentModel as jest.Mocked<typeof DocumentModel>;
const mockProcessingJobModel = ProcessingJobModel as jest.Mocked<typeof ProcessingJobModel>;
const mockFileStorageService = fileStorageService as jest.Mocked<typeof fileStorageService>;
const mockLlmService = llmService as jest.Mocked<typeof llmService>;
const mockPdfGenerationService = pdfGenerationService as jest.Mocked<typeof pdfGenerationService>;

// Mock CIM review data that matches the schema
const mockCIMReviewData = {
  dealOverview: {
    targetCompanyName: 'Test Company',
    industrySector: 'Technology',
    geography: 'US',
    dealSource: 'Investment Bank',
    transactionType: 'Buyout',
    dateCIMReceived: '2024-01-01',
    dateReviewed: '2024-01-02',
    reviewers: 'Test Reviewer',
    cimPageCount: '50',
    statedReasonForSale: 'Strategic exit'
  },
  businessDescription: {
    coreOperationsSummary: 'Test operations',
    keyProductsServices: 'Software solutions',
    uniqueValueProposition: 'Market leader',
    customerBaseOverview: {
      keyCustomerSegments: 'Enterprise clients',
      customerConcentrationRisk: 'Low',
      typicalContractLength: '3 years'
    },
    keySupplierOverview: {
      dependenceConcentrationRisk: 'Moderate'
    }
  },
  marketIndustryAnalysis: {
    estimatedMarketSize: '$1B',
    estimatedMarketGrowthRate: '10%',
    keyIndustryTrends: 'Digital transformation',
    competitiveLandscape: {
      keyCompetitors: 'Competitor A, B',
      targetMarketPosition: '#2',
      basisOfCompetition: 'Innovation'
    },
    barriersToEntry: 'High switching costs'
  },
  financialSummary: {
    financials: {
      fy3: {
        revenue: '$10M',
        revenueGrowth: '15%',
        grossProfit: '$7M',
        grossMargin: '70%',
        ebitda: '$2M',
        ebitdaMargin: '20%'
      },
      fy2: {
        revenue: '$12M',
        revenueGrowth: '20%',
        grossProfit: '$8.4M',
        grossMargin: '70%',
        ebitda: '$2.4M',
        ebitdaMargin: '20%'
      },
      fy1: {
        revenue: '$15M',
        revenueGrowth: '25%',
        grossProfit: '$10.5M',
        grossMargin: '70%',
        ebitda: '$3M',
        ebitdaMargin: '20%'
      },
      ltm: {
        revenue: '$18M',
        revenueGrowth: '20%',
        grossProfit: '$12.6M',
        grossMargin: '70%',
        ebitda: '$3.6M',
        ebitdaMargin: '20%'
      }
    },
    qualityOfEarnings: 'High quality',
    revenueGrowthDrivers: 'Market expansion',
    marginStabilityAnalysis: 'Stable',
    capitalExpenditures: '5%',
    workingCapitalIntensity: 'Low',
    freeCashFlowQuality: 'Strong'
  },
  managementTeamOverview: {
    keyLeaders: 'CEO, CFO, CTO',
    managementQualityAssessment: 'Experienced team',
    postTransactionIntentions: 'Stay on board',
    organizationalStructure: 'Flat structure'
  },
  preliminaryInvestmentThesis: {
    keyAttractions: 'Market leader with strong growth',
    potentialRisks: 'Market competition',
    valueCreationLevers: 'Operational improvements',
    alignmentWithFundStrategy: 'Strong fit'
  },
  keyQuestionsNextSteps: {
    criticalQuestions: 'Market sustainability',
    missingInformation: 'Customer references',
    preliminaryRecommendation: 'Proceed',
    rationaleForRecommendation: 'Strong fundamentals',
    proposedNextSteps: 'Management presentation'
  }
};

describe('DocumentProcessingService', () => {
  const mockDocument = {
    id: 'doc-123',
    user_id: 'user-123',
    original_file_name: 'test-document.pdf',
    file_path: '/uploads/test-document.pdf',
    file_size: 1024,
    status: 'uploaded' as const,
    uploaded_at: new Date(),
    created_at: new Date(),
    updated_at: new Date(),
  };



  beforeEach(() => {
    jest.clearAllMocks();
    
    // Mock config
    (config as any).upload = {
      uploadDir: '/test/uploads',
    };
    (config as any).llm = {
      maxTokens: 4000,
    };

    // Mock fs
    (fs.existsSync as jest.Mock).mockReturnValue(true);
    (fs.mkdirSync as jest.Mock).mockImplementation(() => {});
    (fs.writeFileSync as jest.Mock).mockImplementation(() => {});

    // Mock path
    (path.join as jest.Mock).mockImplementation((...args) => args.join('/'));
    (path.dirname as jest.Mock).mockReturnValue('/test/uploads/summaries');
  });

  describe('processDocument', () => {
    it('should process a document successfully', async () => {
      // Mock document model
      mockDocumentModel.findById.mockResolvedValue(mockDocument);
      mockDocumentModel.updateStatus.mockResolvedValue(mockDocument);

      // Mock file storage service
      mockFileStorageService.getFile.mockResolvedValue(Buffer.from('mock pdf content'));
      mockFileStorageService.fileExists.mockResolvedValue(true);

      // Mock processing job model
      mockProcessingJobModel.create.mockResolvedValue({} as any);
      mockProcessingJobModel.updateStatus.mockResolvedValue({} as any);

      // Mock LLM service
      // Remove estimateTokenCount mock - it's a private method
      mockLlmService.processCIMDocument.mockResolvedValue({
        success: true,
        jsonOutput: mockCIMReviewData,
        model: 'test-model',
        cost: 0.01,
        inputTokens: 1000,
        outputTokens: 500
      });

      // Mock PDF generation service
      mockPdfGenerationService.generatePDFFromMarkdown.mockResolvedValue(true);

      const result = await documentProcessingService.processDocument(
        'doc-123',
        'user-123'
      );

      expect(result.success).toBe(true);
      expect(result.documentId).toBe('doc-123');
      expect(result.jobId).toBeDefined();
      expect(result.steps).toHaveLength(5);
      expect(result.steps.every(step => step.status === 'completed')).toBe(true);
    });

    it('should handle document validation failure', async () => {
      mockDocumentModel.findById.mockResolvedValue(null);

      const result = await documentProcessingService.processDocument(
        'doc-123',
        'user-123'
      );

      expect(result.success).toBe(false);
      expect(result.error).toContain('Document not found');
    });

    it('should handle access denied', async () => {
      const wrongUserDocument = { ...mockDocument, user_id: 'wrong-user' as any };
      mockDocumentModel.findById.mockResolvedValue(wrongUserDocument);

      const result = await documentProcessingService.processDocument(
        'doc-123',
        'user-123'
      );

      expect(result.success).toBe(false);
      expect(result.error).toContain('Access denied');
    });

    it('should handle file not found', async () => {
      mockDocumentModel.findById.mockResolvedValue(mockDocument);
      mockFileStorageService.fileExists.mockResolvedValue(false);

      const result = await documentProcessingService.processDocument(
        'doc-123',
        'user-123'
      );

      expect(result.success).toBe(false);
      expect(result.error).toContain('Document file not accessible');
    });

    it('should handle text extraction failure', async () => {
      mockDocumentModel.findById.mockResolvedValue(mockDocument);
      mockFileStorageService.fileExists.mockResolvedValue(true);
      mockFileStorageService.getFile.mockResolvedValue(null);

      const result = await documentProcessingService.processDocument(
        'doc-123',
        'user-123'
      );

      expect(result.success).toBe(false);
      expect(result.error).toContain('Could not read document file');
    });

    it('should handle LLM processing failure', async () => {
      mockDocumentModel.findById.mockResolvedValue(mockDocument);
      mockFileStorageService.fileExists.mockResolvedValue(true);
      mockFileStorageService.getFile.mockResolvedValue(Buffer.from('mock pdf content'));
      mockProcessingJobModel.create.mockResolvedValue({} as any);
      // Remove estimateTokenCount mock - it's a private method
      mockLlmService.processCIMDocument.mockRejectedValue(new Error('LLM API error'));

      const result = await documentProcessingService.processDocument(
        'doc-123',
        'user-123'
      );

      expect(result.success).toBe(false);
      expect(result.error).toContain('LLM processing failed');
    });

    it('should handle PDF generation failure', async () => {
      mockDocumentModel.findById.mockResolvedValue(mockDocument);
      mockFileStorageService.fileExists.mockResolvedValue(true);
      mockFileStorageService.getFile.mockResolvedValue(Buffer.from('mock pdf content'));
      mockProcessingJobModel.create.mockResolvedValue({} as any);
      // Remove estimateTokenCount mock - it's a private method
      mockLlmService.processCIMDocument.mockResolvedValue({
        success: true,
        jsonOutput: mockCIMReviewData,
        model: 'test-model',
        cost: 0.01,
        inputTokens: 1000,
        outputTokens: 500
      });
      mockPdfGenerationService.generatePDFFromMarkdown.mockResolvedValue(false);

      const result = await documentProcessingService.processDocument(
        'doc-123',
        'user-123'
      );

      expect(result.success).toBe(false);
      expect(result.error).toContain('Failed to generate PDF');
    });

    it('should process large documents in chunks', async () => {
      mockDocumentModel.findById.mockResolvedValue(mockDocument);
      mockFileStorageService.fileExists.mockResolvedValue(true);
      mockFileStorageService.getFile.mockResolvedValue(Buffer.from('mock pdf content'));
      mockProcessingJobModel.create.mockResolvedValue({} as any);
      mockProcessingJobModel.updateStatus.mockResolvedValue({} as any);

      // Mock large document
      mockLlmService.processCIMDocument.mockResolvedValue({
        success: true,
        jsonOutput: mockCIMReviewData,
        model: 'test-model',
        cost: 0.01,
        inputTokens: 1000,
        outputTokens: 500
      });
      mockPdfGenerationService.generatePDFFromMarkdown.mockResolvedValue(true);

      const result = await documentProcessingService.processDocument(
        'doc-123',
        'user-123'
      );

      expect(result.success).toBe(true);
      expect(mockLlmService.processCIMDocument).toHaveBeenCalled();
    });
  });

  describe('getProcessingJobStatus', () => {
    it('should return job status', async () => {
      const mockJob = {
        id: 'job-123',
        status: 'completed',
        created_at: new Date(),
      };

      mockProcessingJobModel.findById.mockResolvedValue(mockJob as any);

      const result = await documentProcessingService.getProcessingJobStatus('job-123');

      expect(result).toEqual(mockJob);
      expect(mockProcessingJobModel.findById).toHaveBeenCalledWith('job-123');
    });

    it('should handle job not found', async () => {
      mockProcessingJobModel.findById.mockResolvedValue(null);

      const result = await documentProcessingService.getProcessingJobStatus('job-123');

      expect(result).toBeNull();
    });
  });

  describe('getDocumentProcessingHistory', () => {
    it('should return processing history', async () => {
      const mockJobs = [
        { id: 'job-1', status: 'completed' },
        { id: 'job-2', status: 'failed' },
      ];

      mockProcessingJobModel.findByDocumentId.mockResolvedValue(mockJobs as any);

      const result = await documentProcessingService.getDocumentProcessingHistory('doc-123');

      expect(result).toEqual(mockJobs);
      expect(mockProcessingJobModel.findByDocumentId).toHaveBeenCalledWith('doc-123');
    });

    it('should return empty array for no history', async () => {
      mockProcessingJobModel.findByDocumentId.mockResolvedValue([]);

      const result = await documentProcessingService.getDocumentProcessingHistory('doc-123');

      expect(result).toEqual([]);
    });
  });

  describe('document analysis', () => {
    it('should detect financial content', () => {
      const financialText = 'Revenue increased by 25% and EBITDA margins improved.';
      const result = (documentProcessingService as any).detectFinancialContent(financialText);
      expect(result).toBe(true);
    });

    it('should detect technical content', () => {
      const technicalText = 'The system architecture includes multiple components.';
      const result = (documentProcessingService as any).detectTechnicalContent(technicalText);
      expect(result).toBe(true);
    });

    it('should extract key topics', () => {
      const text = 'Financial analysis shows strong market growth and competitive advantages.';
      const result = (documentProcessingService as any).extractKeyTopics(text);
      expect(result).toContain('Financial Analysis');
      expect(result).toContain('Market Analysis');
    });

    it('should analyze sentiment', () => {
      const positiveText = 'Strong growth and excellent opportunities.';
      const result = (documentProcessingService as any).analyzeSentiment(positiveText);
      expect(result).toBe('positive');
    });

    it('should assess complexity', () => {
      const simpleText = 'This is a simple document.';
      const result = (documentProcessingService as any).assessComplexity(simpleText);
      expect(result).toBe('low');
    });
  });

  describe('error handling', () => {
    it('should handle database errors gracefully', async () => {
      mockDocumentModel.findById.mockRejectedValue(new Error('Database connection failed'));

      const result = await documentProcessingService.processDocument(
        'doc-123',
        'user-123'
      );

      expect(result.success).toBe(false);
      expect(result.error).toContain('Database connection failed');
    });

    it('should handle file system errors', async () => {
      mockDocumentModel.findById.mockResolvedValue(mockDocument);
      mockFileStorageService.fileExists.mockResolvedValue(true);
      mockFileStorageService.getFile.mockRejectedValue(new Error('File system error'));

      const result = await documentProcessingService.processDocument(
        'doc-123',
        'user-123'
      );

      expect(result.success).toBe(false);
      expect(result.error).toContain('File system error');
    });
  });
}); 