"""Unit tests for file validator service."""

import io
from pathlib import Path

import pytest
from openpyxl import Workbook

from src.services.file_validator import (
    FileValidationError,
    validate_file_extension,
    validate_file_size,
    validate_magic_bytes,
    validate_file_with_openpyxl,
    validate_uploaded_file,
    XLSX_MAGIC_BYTES,
    XLS_MAGIC_BYTES,
    MAX_FILE_SIZE_BYTES,
)


class TestValidateFileExtension:
    """Test file extension validation."""

    def test_valid_xlsx_extension(self):
        """Should pass for .xlsx extension."""
        validate_file_extension("document.xlsx")  # Should not raise

    def test_valid_xls_extension(self):
        """Should pass for .xls extension."""
        validate_file_extension("document.xls")  # Should not raise

    def test_uppercase_extension(self):
        """Should pass for uppercase .XLSX extension."""
        validate_file_extension("DOCUMENT.XLSX")  # Should not raise

    def test_invalid_extension_txt(self):
        """Should raise INVALID_EXTENSION for .txt file."""
        with pytest.raises(FileValidationError) as exc_info:
            validate_file_extension("document.txt")
        assert exc_info.value.code == "INVALID_EXTENSION"
        assert ".txt" in exc_info.value.message

    def test_invalid_extension_pdf(self):
        """Should raise INVALID_EXTENSION for .pdf file."""
        with pytest.raises(FileValidationError) as exc_info:
            validate_file_extension("document.pdf")
        assert exc_info.value.code == "INVALID_EXTENSION"

    def test_no_extension(self):
        """Should raise INVALID_EXTENSION for file with no extension."""
        with pytest.raises(FileValidationError) as exc_info:
            validate_file_extension("document")
        assert exc_info.value.code == "INVALID_EXTENSION"


class TestValidateMagicBytes:
    """Test magic byte signature validation."""

    def test_valid_xlsx_magic_bytes(self):
        """Should pass for valid .xlsx magic bytes (PK)."""
        content = b"PK\x03\x04" + b"\x00" * 100  # ZIP file signature
        validate_magic_bytes(content, "test.xlsx")  # Should not raise

    def test_valid_xls_magic_bytes(self):
        """Should pass for valid .xls magic bytes (OLE2)."""
        content = b"\xd0\xcf\x11\xe0" + b"\x00" * 100  # OLE2 signature
        validate_magic_bytes(content, "test.xls")  # Should not raise

    def test_invalid_magic_bytes_pdf(self):
        """Should raise INVALID_FORMAT for PDF file."""
        content = b"%PDF-1.4" + b"\x00" * 100
        with pytest.raises(FileValidationError) as exc_info:
            validate_magic_bytes(content, "renamed.xlsx")
        assert exc_info.value.code == "INVALID_FORMAT"
        assert "signature" in exc_info.value.message.lower()

    def test_invalid_magic_bytes_text(self):
        """Should raise INVALID_FORMAT for text file."""
        content = b"This is a text file"
        with pytest.raises(FileValidationError) as exc_info:
            validate_magic_bytes(content, "renamed.xlsx")
        assert exc_info.value.code == "INVALID_FORMAT"

    def test_file_too_small(self):
        """Should raise INVALID_FORMAT for file smaller than 4 bytes."""
        content = b"PK"  # Only 2 bytes
        with pytest.raises(FileValidationError) as exc_info:
            validate_magic_bytes(content, "tiny.xlsx")
        assert exc_info.value.code == "INVALID_FORMAT"
        assert "too small" in exc_info.value.message.lower()


class TestValidateFileSize:
    """Test file size validation."""

    def test_valid_small_file(self):
        """Should pass for small files."""
        validate_file_size(1024)  # 1KB - Should not raise

    def test_valid_max_size_file(self):
        """Should pass for file exactly at limit."""
        validate_file_size(MAX_FILE_SIZE_BYTES)  # Should not raise

    def test_invalid_oversized_file(self):
        """Should raise FILE_TOO_LARGE for oversized file."""
        oversized = MAX_FILE_SIZE_BYTES + 1
        with pytest.raises(FileValidationError) as exc_info:
            validate_file_size(oversized)
        assert exc_info.value.code == "FILE_TOO_LARGE"
        assert "50" in exc_info.value.message  # 50MB limit mentioned

    def test_custom_max_size(self):
        """Should respect custom max_size parameter."""
        custom_max = 10 * 1024 * 1024  # 10MB
        with pytest.raises(FileValidationError) as exc_info:
            validate_file_size(custom_max + 1, max_size=custom_max)
        assert exc_info.value.code == "FILE_TOO_LARGE"


class TestValidateFileWithOpenpyxl:
    """Test openpyxl validation for corruption and empty files."""

    def test_valid_xlsx_with_data(self):
        """Should pass for valid .xlsx file with data."""
        # Create valid Excel file in memory
        wb = Workbook()
        ws = wb.active
        ws["A1"] = "Header"
        ws["A2"] = "Data"

        buffer = io.BytesIO()
        wb.save(buffer)
        buffer.seek(0)
        content = buffer.read()

        validate_file_with_openpyxl(content, "test.xlsx")  # Should not raise

    def test_corrupted_file(self):
        """Should raise CORRUPTED_FILE for corrupted Excel file."""
        # Create invalid content that looks like Excel but is corrupted
        content = b"PK\x03\x04" + b"\xff" * 100  # ZIP signature but garbage data
        with pytest.raises(FileValidationError) as exc_info:
            validate_file_with_openpyxl(content, "corrupted.xlsx")
        assert exc_info.value.code == "CORRUPTED_FILE"
        assert "cannot be opened" in exc_info.value.message.lower()

    def test_empty_workbook_no_sheets(self):
        """Should raise EMPTY_FILE for workbook with no sheets.

        Note: openpyxl doesn't allow creating workbooks with zero sheets,
        so this scenario is theoretical. We test the validation logic
        with a minimal empty file instead.
        """
        # Test with completely invalid/empty content that fails to load
        # This simulates a file that would have no sheets if it could be opened
        content = b"PK\x03\x04" + b"\x00" * 50  # Minimal ZIP but not valid Excel

        with pytest.raises(FileValidationError) as exc_info:
            validate_file_with_openpyxl(content, "no_sheets.xlsx")
        # Will raise CORRUPTED_FILE since it can't be opened
        assert exc_info.value.code == "CORRUPTED_FILE"

    def test_workbook_with_empty_sheet(self):
        """Should raise EMPTY_FILE for workbook with only empty sheets."""
        wb = Workbook()
        ws = wb.active
        # Don't add any data to the sheet

        buffer = io.BytesIO()
        wb.save(buffer)
        buffer.seek(0)
        content = buffer.read()

        with pytest.raises(FileValidationError) as exc_info:
            validate_file_with_openpyxl(content, "empty_sheet.xlsx")
        assert exc_info.value.code == "EMPTY_FILE"
        assert "empty" in exc_info.value.message.lower()

    def test_workbook_with_multiple_sheets_one_has_data(self):
        """Should pass if at least one sheet has data."""
        wb = Workbook()

        # First sheet - empty
        ws1 = wb.active
        ws1.title = "Empty"

        # Second sheet - has data
        ws2 = wb.create_sheet("Data")
        ws2["A1"] = "Value"

        buffer = io.BytesIO()
        wb.save(buffer)
        buffer.seek(0)
        content = buffer.read()

        validate_file_with_openpyxl(content, "mixed.xlsx")  # Should not raise


class TestValidateUploadedFile:
    """Test orchestrated validation function."""

    def test_full_validation_valid_file(self):
        """Should pass all validations for valid file."""
        # Create valid Excel file
        wb = Workbook()
        ws = wb.active
        ws["A1"] = "Test"

        buffer = io.BytesIO()
        wb.save(buffer)
        buffer.seek(0)
        content = buffer.read()

        validate_uploaded_file("test.xlsx", content)  # Should not raise

    def test_validation_fails_on_extension(self):
        """Should fail fast on invalid extension."""
        content = b"PK\x03\x04" + b"\x00" * 100
        with pytest.raises(FileValidationError) as exc_info:
            validate_uploaded_file("test.pdf", content)
        assert exc_info.value.code == "INVALID_EXTENSION"

    def test_validation_fails_on_size(self):
        """Should fail on oversized file before checking magic bytes."""
        oversized_content = b"\x00" * (MAX_FILE_SIZE_BYTES + 1)
        with pytest.raises(FileValidationError) as exc_info:
            validate_uploaded_file("test.xlsx", oversized_content)
        assert exc_info.value.code == "FILE_TOO_LARGE"

    def test_validation_fails_on_magic_bytes(self):
        """Should fail on invalid magic bytes."""
        content = b"%PDF-1.4" + b"\x00" * 100
        with pytest.raises(FileValidationError) as exc_info:
            validate_uploaded_file("renamed.xlsx", content)
        assert exc_info.value.code == "INVALID_FORMAT"

    def test_validation_fails_on_corruption(self):
        """Should fail on corrupted Excel file."""
        # Valid extension, size, and magic bytes, but corrupted content
        content = b"PK\x03\x04" + b"\xff" * 200
        with pytest.raises(FileValidationError) as exc_info:
            validate_uploaded_file("test.xlsx", content)
        assert exc_info.value.code == "CORRUPTED_FILE"

    def test_validation_order_extension_before_size(self):
        """Should check extension before size (fail fast on cheapest check)."""
        oversized_content = b"\x00" * (MAX_FILE_SIZE_BYTES + 1)
        # Extension check fails first even though size is also invalid
        with pytest.raises(FileValidationError) as exc_info:
            validate_uploaded_file("test.pdf", oversized_content)
        assert exc_info.value.code == "INVALID_EXTENSION"

    def test_custom_max_size_parameter(self):
        """Should respect custom max_size parameter."""
        content = b"PK\x03\x04" + b"\x00" * (10 * 1024 * 1024 + 1)  # 10MB + 1 byte
        custom_max = 10 * 1024 * 1024  # 10MB

        with pytest.raises(FileValidationError) as exc_info:
            validate_uploaded_file("test.xlsx", content, max_size=custom_max)
        assert exc_info.value.code == "FILE_TOO_LARGE"
