"""Tests for ALL PDF tools routes — Merge, Split, Rotate, Page Numbers, PDF↔Images, Watermark, Protect, Unlock."""
import io
import os
import tempfile
from unittest.mock import patch, MagicMock


# =========================================================================
# Helper: create mock for validate_file + celery task
# =========================================================================
def _mock_validate_and_task(monkeypatch, task_module_path, task_name):
    """Shared helper: mock validate_file to pass, mock the celery task,
    and ensure file.save() works by using a real temp directory."""
    mock_task = MagicMock()
    mock_task.id = 'mock-task-id'

    # Create a real temp dir so file.save() works
    tmp_dir = tempfile.mkdtemp()
    save_path = os.path.join(tmp_dir, 'mock.pdf')

    # Mock file validator to accept any file
    monkeypatch.setattr(
        'app.routes.pdf_tools.validate_actor_file',
        lambda f, allowed_types, actor: ('test.pdf', 'pdf'),
    )
    monkeypatch.setattr(
        'app.routes.pdf_tools.generate_safe_path',
        lambda ext, folder_type: ('mock-task-id', save_path),
    )

    # Mock the celery task delay
    mock_delay = MagicMock(return_value=mock_task)
    monkeypatch.setattr(f'app.routes.pdf_tools.{task_name}.delay', mock_delay)

    return mock_task, mock_delay


# =========================================================================
# 1. Merge PDFs — POST /api/pdf-tools/merge
# =========================================================================
class TestMergePdfs:
    def test_merge_no_files(self, client):
        """Should return 400 when no files provided."""
        response = client.post('/api/pdf-tools/merge')
        assert response.status_code == 400
        data = response.get_json()
        assert 'error' in data

    def test_merge_single_file(self, client):
        """Should return 400 when only one file provided."""
        data = {'files': (io.BytesIO(b'%PDF-1.4 test'), 'test.pdf')}
        response = client.post(
            '/api/pdf-tools/merge',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 400
        assert 'at least 2' in response.get_json()['error'].lower()

    def test_merge_success(self, client, monkeypatch):
        """Should return 202 with task_id when valid PDFs provided."""
        mock_task = MagicMock()
        mock_task.id = 'merge-task-id'
        monkeypatch.setattr(
            'app.routes.pdf_tools.validate_actor_file',
            lambda f, allowed_types, actor: ('test.pdf', 'pdf'),
        )
        monkeypatch.setattr(
            'app.routes.pdf_tools.merge_pdfs_task.delay',
            MagicMock(return_value=mock_task),
        )
        # Mock os.makedirs and FileStorage.save so nothing touches disk
        monkeypatch.setattr('app.routes.pdf_tools.os.makedirs', lambda *a, **kw: None)
        monkeypatch.setattr(
            'werkzeug.datastructures.file_storage.FileStorage.save',
            lambda self, dst, buffer_size=16384: None,
        )

        data = {
            'files': [
                (io.BytesIO(b'%PDF-1.4 file1'), 'a.pdf'),
                (io.BytesIO(b'%PDF-1.4 file2'), 'b.pdf'),
            ]
        }
        response = client.post(
            '/api/pdf-tools/merge',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 202
        body = response.get_json()
        assert body['task_id'] == 'merge-task-id'
        assert 'message' in body

    def test_merge_too_many_files(self, client, monkeypatch):
        """Should return 400 when more than 20 files provided."""
        monkeypatch.setattr(
            'app.routes.pdf_tools.validate_actor_file',
            lambda f, allowed_types, actor: ('test.pdf', 'pdf'),
        )
        data = {
            'files': [
                (io.BytesIO(b'%PDF-1.4'), f'file{i}.pdf')
                for i in range(21)
            ]
        }
        response = client.post(
            '/api/pdf-tools/merge',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 400
        assert '20' in response.get_json()['error']


# =========================================================================
# 2. Split PDF — POST /api/pdf-tools/split
# =========================================================================
class TestSplitPdf:
    def test_split_no_file(self, client):
        """Should return 400 when no file provided."""
        response = client.post('/api/pdf-tools/split')
        assert response.status_code == 400
        data = response.get_json()
        assert data['error'] == 'No file provided.'

    def test_split_success_all_mode(self, client, monkeypatch):
        """Should accept file and return 202 with mode=all."""
        mock_task, mock_delay = _mock_validate_and_task(
            monkeypatch, 'app.routes.pdf_tools', 'split_pdf_task'
        )
        data = {
            'file': (io.BytesIO(b'%PDF-1.4 test'), 'test.pdf'),
            'mode': 'all',
        }
        response = client.post(
            '/api/pdf-tools/split',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 202
        body = response.get_json()
        assert body['task_id'] == 'mock-task-id'

    def test_split_success_range_mode(self, client, monkeypatch):
        """Should accept file with mode=range and pages."""
        mock_task, mock_delay = _mock_validate_and_task(
            monkeypatch, 'app.routes.pdf_tools', 'split_pdf_task'
        )
        data = {
            'file': (io.BytesIO(b'%PDF-1.4 test'), 'test.pdf'),
            'mode': 'range',
            'pages': '1,3,5-8',
        }
        response = client.post(
            '/api/pdf-tools/split',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 202
        mock_delay.assert_called_once()
        # Verify pages parameter was passed
        call_args = mock_delay.call_args
        assert call_args[0][4] == '1,3,5-8'  # pages arg

    def test_split_range_mode_requires_pages(self, client, monkeypatch):
        """Should return 400 when range mode is selected without pages."""
        monkeypatch.setattr(
            'app.routes.pdf_tools.validate_actor_file',
            lambda f, allowed_types, actor: ('test.pdf', 'pdf'),
        )

        data = {
            'file': (io.BytesIO(b'%PDF-1.4 test'), 'test.pdf'),
            'mode': 'range',
        }
        response = client.post(
            '/api/pdf-tools/split',
            data=data,
            content_type='multipart/form-data',
        )

        assert response.status_code == 400
        assert 'specify which pages to extract' in response.get_json()['error'].lower()


# =========================================================================
# 3. Rotate PDF — POST /api/pdf-tools/rotate
# =========================================================================
class TestRotatePdf:
    def test_rotate_no_file(self, client):
        response = client.post('/api/pdf-tools/rotate')
        assert response.status_code == 400

    def test_rotate_invalid_degrees(self, client, monkeypatch):
        """Should reject invalid rotation angles."""
        monkeypatch.setattr(
            'app.routes.pdf_tools.validate_actor_file',
            lambda f, allowed_types, actor: ('test.pdf', 'pdf'),
        )
        data = {
            'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
            'rotation': '45',
        }
        response = client.post(
            '/api/pdf-tools/rotate',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 400
        assert '90, 180, or 270' in response.get_json()['error']

    def test_rotate_success(self, client, monkeypatch):
        mock_task, mock_delay = _mock_validate_and_task(
            monkeypatch, 'app.routes.pdf_tools', 'rotate_pdf_task'
        )
        data = {
            'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
            'rotation': '90',
            'pages': 'all',
        }
        response = client.post(
            '/api/pdf-tools/rotate',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 202
        assert response.get_json()['task_id'] == 'mock-task-id'


# =========================================================================
# 4. Page Numbers — POST /api/pdf-tools/page-numbers
# =========================================================================
class TestAddPageNumbers:
    def test_page_numbers_no_file(self, client):
        response = client.post('/api/pdf-tools/page-numbers')
        assert response.status_code == 400

    def test_page_numbers_success(self, client, monkeypatch):
        mock_task, mock_delay = _mock_validate_and_task(
            monkeypatch, 'app.routes.pdf_tools', 'add_page_numbers_task'
        )
        data = {
            'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
            'position': 'bottom-center',
            'start_number': '1',
        }
        response = client.post(
            '/api/pdf-tools/page-numbers',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 202

    def test_page_numbers_invalid_position_defaults(self, client, monkeypatch):
        """Invalid position should default to bottom-center."""
        mock_task, mock_delay = _mock_validate_and_task(
            monkeypatch, 'app.routes.pdf_tools', 'add_page_numbers_task'
        )
        data = {
            'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
            'position': 'invalid-position',
        }
        response = client.post(
            '/api/pdf-tools/page-numbers',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 202
        # Should have used default 'bottom-center'
        call_args = mock_delay.call_args[0]
        assert call_args[3] == 'bottom-center'


# =========================================================================
# 5. PDF to Images — POST /api/pdf-tools/pdf-to-images
# =========================================================================
class TestPdfToImages:
    def test_pdf_to_images_no_file(self, client):
        response = client.post('/api/pdf-tools/pdf-to-images')
        assert response.status_code == 400

    def test_pdf_to_images_success(self, client, monkeypatch):
        mock_task, mock_delay = _mock_validate_and_task(
            monkeypatch, 'app.routes.pdf_tools', 'pdf_to_images_task'
        )
        data = {
            'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
            'format': 'png',
            'dpi': '200',
        }
        response = client.post(
            '/api/pdf-tools/pdf-to-images',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 202

    def test_pdf_to_images_invalid_format_defaults(self, client, monkeypatch):
        """Invalid format should default to png."""
        mock_task, mock_delay = _mock_validate_and_task(
            monkeypatch, 'app.routes.pdf_tools', 'pdf_to_images_task'
        )
        data = {
            'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
            'format': 'bmp',
        }
        response = client.post(
            '/api/pdf-tools/pdf-to-images',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 202
        call_args = mock_delay.call_args[0]
        assert call_args[3] == 'png'  # default format


# =========================================================================
# 6. Images to PDF — POST /api/pdf-tools/images-to-pdf
# =========================================================================
class TestImagesToPdf:
    def test_images_to_pdf_no_files(self, client):
        response = client.post('/api/pdf-tools/images-to-pdf')
        assert response.status_code == 400

    def test_images_to_pdf_success(self, client, monkeypatch):
        mock_task = MagicMock()
        mock_task.id = 'images-task-id'
        monkeypatch.setattr(
            'app.routes.pdf_tools.validate_actor_file',
            lambda f, allowed_types, actor: ('test.png', 'png'),
        )
        monkeypatch.setattr(
            'app.routes.pdf_tools.images_to_pdf_task.delay',
            MagicMock(return_value=mock_task),
        )
        # Mock os.makedirs and FileStorage.save so nothing touches disk
        monkeypatch.setattr('app.routes.pdf_tools.os.makedirs', lambda *a, **kw: None)
        monkeypatch.setattr(
            'werkzeug.datastructures.file_storage.FileStorage.save',
            lambda self, dst, buffer_size=16384: None,
        )
        data = {
            'files': [
                (io.BytesIO(b'\x89PNG\r\n'), 'img1.png'),
                (io.BytesIO(b'\x89PNG\r\n'), 'img2.png'),
            ]
        }
        response = client.post(
            '/api/pdf-tools/images-to-pdf',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 202
        assert response.get_json()['task_id'] == 'images-task-id'

    def test_images_to_pdf_too_many(self, client, monkeypatch):
        monkeypatch.setattr(
            'app.routes.pdf_tools.validate_actor_file',
            lambda f, allowed_types, actor: ('test.png', 'png'),
        )
        data = {
            'files': [
                (io.BytesIO(b'\x89PNG\r\n'), f'img{i}.png')
                for i in range(51)
            ]
        }
        response = client.post(
            '/api/pdf-tools/images-to-pdf',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 400
        assert '50' in response.get_json()['error']


# =========================================================================
# 7. Watermark PDF — POST /api/pdf-tools/watermark
# =========================================================================
class TestWatermarkPdf:
    def test_watermark_no_file(self, client):
        response = client.post('/api/pdf-tools/watermark')
        assert response.status_code == 400

    def test_watermark_no_text(self, client, monkeypatch):
        monkeypatch.setattr(
            'app.routes.pdf_tools.validate_actor_file',
            lambda f, allowed_types, actor: ('test.pdf', 'pdf'),
        )
        data = {
            'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
            'text': '',
        }
        response = client.post(
            '/api/pdf-tools/watermark',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 400
        assert 'required' in response.get_json()['error'].lower()

    def test_watermark_text_too_long(self, client, monkeypatch):
        monkeypatch.setattr(
            'app.routes.pdf_tools.validate_actor_file',
            lambda f, allowed_types, actor: ('test.pdf', 'pdf'),
        )
        data = {
            'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
            'text': 'x' * 101,
        }
        response = client.post(
            '/api/pdf-tools/watermark',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 400
        assert '100' in response.get_json()['error']

    def test_watermark_success(self, client, monkeypatch):
        mock_task, mock_delay = _mock_validate_and_task(
            monkeypatch, 'app.routes.pdf_tools', 'watermark_pdf_task'
        )
        data = {
            'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
            'text': 'CONFIDENTIAL',
            'opacity': '0.5',
        }
        response = client.post(
            '/api/pdf-tools/watermark',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 202


# =========================================================================
# 8. Protect PDF — POST /api/pdf-tools/protect
# =========================================================================
class TestProtectPdf:
    def test_protect_no_file(self, client):
        response = client.post('/api/pdf-tools/protect')
        assert response.status_code == 400

    def test_protect_no_password(self, client, monkeypatch):
        monkeypatch.setattr(
            'app.routes.pdf_tools.validate_actor_file',
            lambda f, allowed_types, actor: ('test.pdf', 'pdf'),
        )
        data = {
            'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
            'password': '',
        }
        response = client.post(
            '/api/pdf-tools/protect',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 400
        assert 'required' in response.get_json()['error'].lower()

    def test_protect_short_password(self, client, monkeypatch):
        monkeypatch.setattr(
            'app.routes.pdf_tools.validate_actor_file',
            lambda f, allowed_types, actor: ('test.pdf', 'pdf'),
        )
        data = {
            'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
            'password': 'abc',
        }
        response = client.post(
            '/api/pdf-tools/protect',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 400
        assert '4 characters' in response.get_json()['error']

    def test_protect_success(self, client, monkeypatch):
        mock_task, mock_delay = _mock_validate_and_task(
            monkeypatch, 'app.routes.pdf_tools', 'protect_pdf_task'
        )
        data = {
            'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
            'password': 'secret1234',
        }
        response = client.post(
            '/api/pdf-tools/protect',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 202


# =========================================================================
# 9. Unlock PDF — POST /api/pdf-tools/unlock
# =========================================================================
class TestUnlockPdf:
    def test_unlock_no_file(self, client):
        response = client.post('/api/pdf-tools/unlock')
        assert response.status_code == 400

    def test_unlock_no_password(self, client, monkeypatch):
        monkeypatch.setattr(
            'app.routes.pdf_tools.validate_actor_file',
            lambda f, allowed_types, actor: ('test.pdf', 'pdf'),
        )
        data = {
            'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
            'password': '',
        }
        response = client.post(
            '/api/pdf-tools/unlock',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 400

    def test_unlock_success(self, client, monkeypatch):
        mock_task, mock_delay = _mock_validate_and_task(
            monkeypatch, 'app.routes.pdf_tools', 'unlock_pdf_task'
        )
        data = {
            'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
            'password': 'mypassword',
        }
        response = client.post(
            '/api/pdf-tools/unlock',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 202


# =========================================================================
# 9. Remove Watermark — POST /api/pdf-tools/remove-watermark
# =========================================================================
class TestRemoveWatermark:
    def test_no_file(self, client):
        """Should return 400 when no file provided."""
        response = client.post('/api/pdf-tools/remove-watermark')
        assert response.status_code == 400

    def test_success(self, client, monkeypatch):
        """Should return 202 with task_id on valid PDF."""
        _mock_validate_and_task(
            monkeypatch, 'app.routes.pdf_tools', 'remove_watermark_task'
        )
        data = {'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf')}
        response = client.post(
            '/api/pdf-tools/remove-watermark',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 202


# =========================================================================
# 10. Reorder PDF — POST /api/pdf-tools/reorder
# =========================================================================
class TestReorderPdf:
    def test_no_file(self, client):
        """Should return 400 when no file provided."""
        response = client.post('/api/pdf-tools/reorder')
        assert response.status_code == 400

    def test_no_page_order(self, client, monkeypatch):
        """Should return 400 when no page_order provided."""
        monkeypatch.setattr(
            'app.routes.pdf_tools.validate_actor_file',
            lambda f, allowed_types, actor: ('test.pdf', 'pdf'),
        )
        data = {'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf')}
        response = client.post(
            '/api/pdf-tools/reorder',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 400

    def test_success(self, client, monkeypatch):
        """Should return 202 with task_id on valid request."""
        _mock_validate_and_task(
            monkeypatch, 'app.routes.pdf_tools', 'reorder_pdf_task'
        )
        data = {
            'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
            'page_order': '3,1,2',
        }
        response = client.post(
            '/api/pdf-tools/reorder',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 202


# =========================================================================
# 11. Extract Pages — POST /api/pdf-tools/extract-pages
# =========================================================================
class TestExtractPages:
    def test_no_file(self, client):
        """Should return 400 when no file provided."""
        response = client.post('/api/pdf-tools/extract-pages')
        assert response.status_code == 400

    def test_no_pages(self, client, monkeypatch):
        """Should return 400 when no pages param provided."""
        monkeypatch.setattr(
            'app.routes.pdf_tools.validate_actor_file',
            lambda f, allowed_types, actor: ('test.pdf', 'pdf'),
        )
        data = {'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf')}
        response = client.post(
            '/api/pdf-tools/extract-pages',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 400

    def test_success(self, client, monkeypatch):
        """Should return 202 with task_id on valid request."""
        _mock_validate_and_task(
            monkeypatch, 'app.routes.pdf_tools', 'extract_pages_task'
        )
        data = {
            'file': (io.BytesIO(b'%PDF-1.4'), 'test.pdf'),
            'pages': '1,3,5-8',
        }
        response = client.post(
            '/api/pdf-tools/extract-pages',
            data=data,
            content_type='multipart/form-data',
        )
        assert response.status_code == 202