"""
Unit tests for DocumentTypeDetector service.

Tests cover:
- Various file type detection (PDF, image, Office, text)
- PDF editability detection
- Edge cases (file not found, unknown types)
"""

import pytest
from pathlib import Path

from app.services.document_type_detector import (
    DocumentTypeDetector,
    DocumentType,
    ProcessingTrackRecommendation
)


class TestDocumentTypeDetector:
    """Test suite for DocumentTypeDetector."""

    @pytest.fixture
    def detector(self):
        """Create a detector instance with default settings."""
        return DocumentTypeDetector()

    @pytest.fixture
    def strict_detector(self):
        """Create a detector with strict text requirements."""
        return DocumentTypeDetector(
            min_text_length=200,
            text_coverage_threshold=0.95
        )

    # ===== PDF Detection Tests =====

    def test_detect_editable_pdf(self, detector, sample_pdf_editable):
        """Test detection of editable PDF with extractable text."""
        result = detector.detect(sample_pdf_editable)

        assert result.track == "direct"
        assert result.document_type == DocumentType.PDF_EDITABLE
        assert result.confidence >= 0.9
        assert "extractable text" in result.reason.lower()
        assert result.metadata.get("total_pages") == 3

    def test_detect_scanned_pdf(self, detector, sample_pdf_scanned):
        """Test detection of scanned PDF (image-only)."""
        result = detector.detect(sample_pdf_scanned)

        assert result.track == "ocr"
        assert result.document_type == DocumentType.PDF_SCANNED
        assert result.confidence >= 0.9
        assert "scanned" in result.reason.lower()

    def test_detect_mixed_pdf(self, detector, sample_pdf_mixed):
        """Test detection of mixed PDF (text + images)."""
        result = detector.detect(sample_pdf_mixed)

        assert result.track == "ocr"
        assert result.document_type == DocumentType.PDF_MIXED
        assert result.confidence >= 0.5
        assert "mixed" in result.reason.lower()

    def test_pdf_text_coverage_calculation(self, detector, sample_pdf_editable):
        """Test that text coverage is calculated correctly."""
        result = detector.detect(sample_pdf_editable)

        metadata = result.metadata
        assert "text_coverage" in metadata
        assert metadata["text_coverage"] >= 0.9  # All pages have text

    def test_pdf_page_details(self, detector, sample_pdf_editable):
        """Test that page details are included in metadata."""
        result = detector.detect(sample_pdf_editable)

        metadata = result.metadata
        assert "page_details" in metadata
        assert len(metadata["page_details"]) == min(3, detector.sample_pages)

        for page_detail in metadata["page_details"]:
            assert "page" in page_detail
            assert "text_length" in page_detail
            assert "has_text" in page_detail
            assert "image_count" in page_detail

    # ===== Image Detection Tests =====

    def test_detect_png_image(self, detector, sample_image_png):
        """Test detection of PNG image file."""
        result = detector.detect(sample_image_png)

        assert result.track == "ocr"
        assert result.document_type == DocumentType.IMAGE
        assert result.confidence == 1.0
        assert "image" in result.reason.lower()
        assert result.metadata.get("mime_type") == "image/png"

    def test_detect_jpg_image(self, detector, sample_image_jpg):
        """Test detection of JPEG image file."""
        result = detector.detect(sample_image_jpg)

        assert result.track == "ocr"
        assert result.document_type == DocumentType.IMAGE
        assert result.confidence == 1.0
        assert result.metadata.get("file_extension") == ".jpg"

    # ===== Office Document Tests =====

    def test_detect_docx_with_text(self, detector, sample_docx):
        """Test detection of Word document with extractable text.

        Office documents are converted to PDF first, then analyzed.
        If the converted PDF has extractable text, it routes to 'direct' track.

        Note: This test requires LibreOffice to be installed and working.
        If conversion fails, the document falls back to OCR track.
        """
        result = detector.detect(sample_docx)

        # Check if this was detected as Office and converted successfully
        if result.metadata.get("converted_pdf_analyzed"):
            # Conversion happened - track depends on text coverage of converted PDF
            assert result.document_type == DocumentType.OFFICE_WORD
            assert result.metadata.get("original_document_type") == "office_word"
            assert "text_coverage" in result.metadata

            # If converted PDF has enough text, it should use direct track
            # Otherwise it falls back to OCR (for image-based or mixed content)
            if result.track == "direct":
                assert result.confidence >= 0.7
                assert "converted" in result.reason.lower() or "text" in result.reason.lower()
            else:
                # Converted PDF didn't have enough text coverage
                assert result.track == "ocr"
        else:
            # If conversion failed or wasn't attempted, it falls back to OCR
            assert result.track == "ocr"
            assert result.document_type in [DocumentType.OFFICE_WORD, DocumentType.UNKNOWN]

    def test_detect_docx_metadata(self, detector, sample_docx):
        """Test that Office document detection includes correct metadata."""
        result = detector.detect(sample_docx)

        metadata = result.metadata
        assert "mime_type" in metadata
        assert "file_size" in metadata
        assert "file_extension" in metadata
        assert metadata["file_extension"] == ".docx"

    def test_office_conversion_preserves_original_type(self, detector, sample_docx):
        """Test that original Office document type is preserved after conversion."""
        result = detector.detect(sample_docx)

        # Should preserve original Office type even though it was converted to PDF
        assert result.document_type == DocumentType.OFFICE_WORD
        assert result.metadata.get("original_document_type") == "office_word"

    # ===== Text File Tests =====

    def test_detect_text_file(self, detector, sample_text_file):
        """Test detection of plain text file."""
        result = detector.detect(sample_text_file)

        assert result.track == "direct"
        assert result.document_type == DocumentType.TEXT
        assert result.confidence == 1.0
        assert "text" in result.reason.lower()

    # ===== Edge Case Tests =====

    def test_file_not_found(self, detector, temp_dir):
        """Test handling of non-existent file."""
        non_existent = temp_dir / "does_not_exist.pdf"
        result = detector.detect(non_existent)

        assert result.track == "ocr"
        assert result.document_type == DocumentType.UNKNOWN
        assert result.confidence == 0.5
        assert "not found" in result.reason.lower()

    def test_unknown_file_type(self, detector, temp_dir):
        """Test handling of unknown file type."""
        # Create a file with unknown content
        unknown_file = temp_dir / "unknown.xyz"
        unknown_file.write_bytes(b'\x00\x01\x02\x03\x04\x05')

        result = detector.detect(unknown_file)

        assert result.track == "ocr"
        assert result.document_type == DocumentType.UNKNOWN
        assert result.confidence <= 0.5

    def test_empty_pdf(self, detector, temp_dir):
        """Test handling of PDF with blank pages (no content)."""
        import fitz

        empty_pdf = temp_dir / "empty.pdf"
        doc = fitz.open()
        # Create a blank page with no content
        doc.new_page()
        doc.save(str(empty_pdf))
        doc.close()

        result = detector.detect(empty_pdf)

        # Blank PDF should be detected as scanned (no extractable text)
        assert result.track == "ocr"
        assert result.document_type == DocumentType.PDF_SCANNED
        assert result.metadata.get("total_pages") == 1

    # ===== Configuration Tests =====

    def test_custom_min_text_length(self, temp_dir):
        """Test that custom min_text_length affects detection."""
        import fitz

        # Create PDF with minimal text
        pdf_path = temp_dir / "minimal_text.pdf"
        doc = fitz.open()
        page = doc.new_page()
        page.insert_text((50, 50), "Short text")  # Only ~10 chars
        doc.save(str(pdf_path))
        doc.close()

        # Default detector (min_text_length=100)
        default_detector = DocumentTypeDetector()
        result_default = default_detector.detect(pdf_path)

        # Strict detector (min_text_length=200)
        strict_detector = DocumentTypeDetector(min_text_length=5)
        result_strict = strict_detector.detect(pdf_path)

        # With very low threshold, it should find text
        assert result_strict.document_type in [
            DocumentType.PDF_EDITABLE,
            DocumentType.PDF_MIXED
        ]

    def test_sample_pages_setting(self, temp_dir):
        """Test that sample_pages setting is respected."""
        import fitz

        # Create PDF with 10 pages
        pdf_path = temp_dir / "many_pages.pdf"
        doc = fitz.open()
        for i in range(10):
            page = doc.new_page()
            page.insert_text((50, 50), f"Page {i + 1} content\n" * 20)
        doc.save(str(pdf_path))
        doc.close()

        # Detector that samples only 2 pages
        detector = DocumentTypeDetector(sample_pages=2)
        result = detector.detect(pdf_path)

        assert result.metadata["sampled_pages"] == 2
        assert result.metadata["total_pages"] == 10

    # ===== Batch Processing Tests =====

    def test_analyze_batch(self, detector, sample_pdf_editable, sample_image_png, sample_text_file):
        """Test batch analysis of multiple files."""
        files = [sample_pdf_editable, sample_image_png, sample_text_file]
        results = detector.analyze_batch(files)

        assert len(results) == 3
        assert str(sample_pdf_editable) in results
        assert str(sample_image_png) in results
        assert str(sample_text_file) in results

    def test_get_statistics(self, detector, sample_pdf_editable, sample_image_png, sample_text_file):
        """Test statistics calculation from batch results."""
        files = [sample_pdf_editable, sample_image_png, sample_text_file]
        results = detector.analyze_batch(files)
        stats = detector.get_statistics(results)

        assert stats["total"] == 3
        assert "by_track" in stats
        assert stats["by_track"]["ocr"] >= 1  # At least image
        assert stats["by_track"]["direct"] >= 1  # At least text
        assert "confidence" in stats
        assert stats["confidence"]["mean"] > 0

    def test_get_statistics_empty(self, detector):
        """Test statistics with empty results."""
        stats = detector.get_statistics({})
        assert stats == {"total": 0}

    # ===== Recommendation Object Tests =====

    def test_recommendation_to_dict(self, detector, sample_pdf_editable):
        """Test ProcessingTrackRecommendation.to_dict() method."""
        result = detector.detect(sample_pdf_editable)
        result_dict = result.to_dict()

        assert "recommended_track" in result_dict
        assert "confidence" in result_dict
        assert "reason" in result_dict
        assert "document_type" in result_dict
        assert "metadata" in result_dict

        assert result_dict["recommended_track"] == result.track
        assert result_dict["confidence"] == result.confidence

    def test_recommendation_metadata_types(self, detector, sample_pdf_editable):
        """Test that metadata contains correct types."""
        result = detector.detect(sample_pdf_editable)

        assert isinstance(result.track, str)
        assert isinstance(result.confidence, float)
        assert isinstance(result.reason, str)
        assert isinstance(result.document_type, DocumentType)
        assert isinstance(result.metadata, dict)

    # ===== Performance Tests =====

    def test_large_pdf_sampling(self, detector, temp_dir):
        """Test that large PDFs are sampled efficiently."""
        import fitz
        import time

        # Create a large PDF (20 pages)
        large_pdf = temp_dir / "large.pdf"
        doc = fitz.open()
        for i in range(20):
            page = doc.new_page()
            page.insert_text((50, 50), f"Page {i + 1}\n" * 50)
        doc.save(str(large_pdf))
        doc.close()

        # Detection should be fast due to sampling
        start_time = time.time()
        result = detector.detect(large_pdf)
        elapsed = time.time() - start_time

        assert elapsed < 5.0  # Should complete within 5 seconds
        assert result.metadata["sampled_pages"] <= detector.sample_pages
        assert result.metadata["total_pages"] == 20

    # ===== Office Document Direct Extraction Tests =====

    def test_office_document_text_coverage(self, detector, sample_docx):
        """Test that text coverage is calculated for converted Office documents."""
        result = detector.detect(sample_docx)

        # Only check text coverage if conversion was successful
        if result.metadata.get("converted_pdf_analyzed"):
            assert "text_coverage" in result.metadata
            # DOCX with text should have some text coverage (may vary by LibreOffice version)
            assert result.metadata["text_coverage"] >= 0.0
        else:
            # If conversion failed, text_coverage may not be present
            pass  # Test passes either way

    def test_office_conversion_confidence(self, detector, sample_docx):
        """Test that confidence is slightly reduced for converted documents."""
        result = detector.detect(sample_docx)

        # Confidence should be slightly lower than direct PDF analysis
        # (multiplied by 0.95 for converted files)
        assert result.confidence <= 0.95
        assert result.confidence >= 0.7

    def test_office_pptx_detection(self, detector, temp_dir):
        """Test detection of PowerPoint document."""
        from zipfile import ZipFile

        # Create minimal PPTX structure
        pptx_path = temp_dir / "sample.pptx"
        with ZipFile(pptx_path, 'w') as zf:
            # [Content_Types].xml
            content_types = '''<?xml version="1.0" encoding="UTF-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
  <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
  <Default Extension="xml" ContentType="application/xml"/>
  <Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>
  <Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>
</Types>'''
            zf.writestr('[Content_Types].xml', content_types)

            # _rels/.rels
            rels = '''<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/>
</Relationships>'''
            zf.writestr('_rels/.rels', rels)

            # ppt/presentation.xml
            presentation = '''<?xml version="1.0" encoding="UTF-8"?>
<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
  <p:sldIdLst>
    <p:sldId id="256" r:id="rId2" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"/>
  </p:sldIdLst>
</p:presentation>'''
            zf.writestr('ppt/presentation.xml', presentation)

            # ppt/_rels/presentation.xml.rels
            pres_rels = '''<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>
</Relationships>'''
            zf.writestr('ppt/_rels/presentation.xml.rels', pres_rels)

            # ppt/slides/slide1.xml
            slide = '''<?xml version="1.0" encoding="UTF-8"?>
<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
       xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
  <p:cSld>
    <p:spTree>
      <p:sp>
        <p:txBody>
          <a:p><a:r><a:t>Test Slide Content</a:t></a:r></a:p>
        </p:txBody>
      </p:sp>
    </p:spTree>
  </p:cSld>
</p:sld>'''
            zf.writestr('ppt/slides/slide1.xml', slide)

        result = detector.detect(pptx_path)

        # PPTX should be detected as PowerPoint
        # If conversion succeeds, routes to direct track
        # If conversion fails, falls back to OCR
        if result.metadata.get("converted_pdf_analyzed"):
            assert result.document_type == DocumentType.OFFICE_POWERPOINT
            assert result.track == "direct"
            assert result.metadata.get("original_document_type") == "office_ppt"
        else:
            # Conversion failed or file wasn't recognized as Office
            assert result.track == "ocr"
            assert result.document_type in [DocumentType.OFFICE_POWERPOINT, DocumentType.UNKNOWN]

    def test_office_conversion_fallback_on_error(self, detector, temp_dir, monkeypatch):
        """Test that Office conversion failure falls back to OCR track.

        This test directly tests the _analyze_office method to ensure
        proper error handling when conversion fails.
        """
        from pathlib import Path
        from app.services.office_converter import OfficeConverter, OfficeConverterError

        # Mock the converter to raise an error
        original_convert = OfficeConverter.convert_to_pdf

        def mock_convert_to_pdf(self, *args, **kwargs):
            raise OfficeConverterError("Simulated conversion failure")

        monkeypatch.setattr(OfficeConverter, "convert_to_pdf", mock_convert_to_pdf)

        # Create a path that would be recognized (we'll call _analyze_office directly)
        docx_path = temp_dir / "test.docx"
        docx_path.touch()

        # Directly call _analyze_office to test the fallback
        mime_type = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
        result = detector._analyze_office(docx_path, mime_type)

        # Should fall back to OCR on conversion error
        assert result.track == "ocr"
        assert result.confidence <= 0.7
        assert "failed" in result.reason.lower() or "error" in result.reason.lower()
        assert result.document_type == DocumentType.OFFICE_WORD
