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

import io
from unittest.mock import AsyncMock

import pytest
from fastapi import UploadFile

from src.services.batch_validator import (
    MAX_BATCH_SIZE_BYTES,
    MAX_FILES_PER_BATCH,
    BatchValidationError,
    validate_batch,
    validate_batch_file_count,
    validate_batch_total_size,
)


def create_mock_upload_file(filename: str, content: bytes) -> UploadFile:
    """Helper to create a mock UploadFile with specified content.

    Args:
        filename: Name of the file.
        content: File content as bytes.

    Returns:
        UploadFile instance with mock read/seek methods.
    """
    file_obj = io.BytesIO(content)
    upload_file = UploadFile(filename=filename, file=file_obj)

    # Override read and seek to work with BytesIO
    async def async_read():
        return file_obj.read()

    async def async_seek(position: int):
        return file_obj.seek(position)

    upload_file.read = async_read
    upload_file.seek = async_seek

    return upload_file


class TestValidateBatchFileCount:
    """Test file count validation."""

    @pytest.mark.asyncio
    async def test_valid_single_file(self):
        """Should pass for single file."""
        files = [create_mock_upload_file("file1.xlsx", b"test")]
        await validate_batch_file_count(files)  # Should not raise

    @pytest.mark.asyncio
    async def test_valid_multiple_files(self):
        """Should pass for multiple files within limit."""
        files = [
            create_mock_upload_file(f"file{i}.xlsx", b"test") for i in range(10)
        ]
        await validate_batch_file_count(files)  # Should not raise

    @pytest.mark.asyncio
    async def test_valid_max_files(self):
        """Should pass for exactly 20 files."""
        files = [
            create_mock_upload_file(f"file{i}.xlsx", b"test")
            for i in range(MAX_FILES_PER_BATCH)
        ]
        await validate_batch_file_count(files)  # Should not raise

    @pytest.mark.asyncio
    async def test_too_many_files(self):
        """Should raise TOO_MANY_FILES for 21 files."""
        files = [
            create_mock_upload_file(f"file{i}.xlsx", b"test")
            for i in range(MAX_FILES_PER_BATCH + 1)
        ]
        with pytest.raises(BatchValidationError) as exc_info:
            await validate_batch_file_count(files)
        assert exc_info.value.code == "TOO_MANY_FILES"
        assert "21 files" in exc_info.value.message
        assert "maximum allowed is 20" in exc_info.value.message

    @pytest.mark.asyncio
    async def test_empty_batch(self):
        """Should raise EMPTY_BATCH for zero files."""
        files = []
        with pytest.raises(BatchValidationError) as exc_info:
            await validate_batch_file_count(files)
        assert exc_info.value.code == "EMPTY_BATCH"
        assert "No files provided" in exc_info.value.message


class TestValidateBatchTotalSize:
    """Test total batch size validation."""

    @pytest.mark.asyncio
    async def test_valid_small_batch(self):
        """Should pass for small batch."""
        files = [
            create_mock_upload_file("file1.xlsx", b"x" * 1024),  # 1KB
            create_mock_upload_file("file2.xlsx", b"y" * 2048),  # 2KB
        ]
        total_size = await validate_batch_total_size(files)
        assert total_size == 1024 + 2048

    @pytest.mark.asyncio
    async def test_valid_large_batch_under_limit(self):
        """Should pass for batch just under 500MB limit."""
        # Create 10 files of ~49MB each = ~490MB total
        file_size = 49 * 1024 * 1024
        files = [
            create_mock_upload_file(f"file{i}.xlsx", b"x" * file_size)
            for i in range(10)
        ]
        total_size = await validate_batch_total_size(files)
        assert total_size == file_size * 10
        assert total_size < MAX_BATCH_SIZE_BYTES

    @pytest.mark.asyncio
    async def test_valid_batch_at_limit(self):
        """Should pass for batch exactly at 500MB limit."""
        files = [create_mock_upload_file("file1.xlsx", b"x" * MAX_BATCH_SIZE_BYTES)]
        total_size = await validate_batch_total_size(files)
        assert total_size == MAX_BATCH_SIZE_BYTES

    @pytest.mark.asyncio
    async def test_batch_exceeds_limit(self):
        """Should raise BATCH_TOO_LARGE for batch over 500MB."""
        oversized = MAX_BATCH_SIZE_BYTES + 1024  # 500MB + 1KB
        files = [create_mock_upload_file("huge.xlsx", b"x" * oversized)]
        with pytest.raises(BatchValidationError) as exc_info:
            await validate_batch_total_size(files)
        assert exc_info.value.code == "BATCH_TOO_LARGE"
        assert "500.00MB" in exc_info.value.message
        assert "exceeds maximum allowed size of 500MB" in exc_info.value.message

    @pytest.mark.asyncio
    async def test_multiple_files_exceed_limit(self):
        """Should raise BATCH_TOO_LARGE when multiple files exceed limit."""
        # 5 files of 110MB each = 550MB total (over 500MB limit)
        file_size = 110 * 1024 * 1024
        files = [
            create_mock_upload_file(f"file{i}.xlsx", b"x" * file_size)
            for i in range(5)
        ]
        with pytest.raises(BatchValidationError) as exc_info:
            await validate_batch_total_size(files)
        assert exc_info.value.code == "BATCH_TOO_LARGE"

    @pytest.mark.asyncio
    async def test_file_pointer_reset_after_read(self):
        """Should reset file pointers after reading for size check."""
        content = b"test content"
        files = [create_mock_upload_file("file1.xlsx", content)]

        await validate_batch_total_size(files)

        # File should be readable again after validation
        read_content = await files[0].read()
        assert read_content == content


class TestValidateBatch:
    """Test orchestrated batch validation."""

    @pytest.mark.asyncio
    async def test_valid_batch_all_checks_pass(self):
        """Should pass when all validations succeed."""
        files = [
            create_mock_upload_file("file1.xlsx", b"x" * (10 * 1024 * 1024)),  # 10MB
            create_mock_upload_file("file2.xlsx", b"y" * (20 * 1024 * 1024)),  # 20MB
        ]
        total_size = await validate_batch(files)
        assert total_size == 30 * 1024 * 1024

    @pytest.mark.asyncio
    async def test_fails_on_too_many_files(self):
        """Should fail on file count before checking size."""
        # 21 small files (file count violation)
        files = [
            create_mock_upload_file(f"file{i}.xlsx", b"x" * 1024)
            for i in range(MAX_FILES_PER_BATCH + 1)
        ]
        with pytest.raises(BatchValidationError) as exc_info:
            await validate_batch(files)
        assert exc_info.value.code == "TOO_MANY_FILES"

    @pytest.mark.asyncio
    async def test_fails_on_batch_size(self):
        """Should fail on total size after file count passes."""
        # 10 files of 60MB each = 600MB (size violation)
        file_size = 60 * 1024 * 1024
        files = [
            create_mock_upload_file(f"file{i}.xlsx", b"x" * file_size)
            for i in range(10)
        ]
        with pytest.raises(BatchValidationError) as exc_info:
            await validate_batch(files)
        assert exc_info.value.code == "BATCH_TOO_LARGE"

    @pytest.mark.asyncio
    async def test_empty_batch_fails(self):
        """Should fail for empty batch."""
        files = []
        with pytest.raises(BatchValidationError) as exc_info:
            await validate_batch(files)
        assert exc_info.value.code == "EMPTY_BATCH"
