"""Integration tests for document upload endpoints."""

import io
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
from uuid import uuid4

import pytest
from fastapi.testclient import TestClient
from openpyxl import Workbook
from sqlalchemy import select

from src.api.main import app
from src.core.config import settings
from src.db.models import ApiKey, Document
from src.db.session import get_async_session, get_sync_session
from src.services.auth_service import hash_api_key


@pytest.fixture
def test_api_key():
    """Create a test API key in the database."""
    test_key = "test_api_key_for_documents_12345"
    key_hash = hash_api_key(test_key)

    with get_sync_session() as session:
        # Clean up any existing test API key
        existing = session.execute(
            select(ApiKey).where(ApiKey.key_hash == key_hash)
        ).scalar_one_or_none()
        if existing:
            session.delete(existing)
            session.commit()

        # Create new test API key
        api_key = ApiKey(
            key_hash=key_hash,
            name="Test Document Upload Key",
            rate_limit_per_minute=60,
            is_active=True,
        )
        session.add(api_key)
        session.commit()
        session.refresh(api_key)

        yield test_key, api_key.id

        # Cleanup: delete test API key and related documents
        session.execute(
            select(ApiKey).where(ApiKey.key_hash == key_hash)
        )
        if api_key_obj := session.execute(
            select(ApiKey).where(ApiKey.key_hash == key_hash)
        ).scalar_one_or_none():
            session.delete(api_key_obj)
            session.commit()


@pytest.fixture
def test_client():
    """Create a FastAPI test client."""
    return TestClient(app)


@pytest.fixture
def sample_excel_file():
    """Create a real valid Excel file for testing."""
    # Create a real Excel file with data
    wb = Workbook()
    ws = wb.active
    ws["A1"] = "Header"
    ws["A2"] = "Data"

    buffer = io.BytesIO()
    wb.save(buffer)
    buffer.seek(0)
    return buffer


@pytest.fixture
def empty_excel_file():
    """Create an Excel file with empty sheets."""
    wb = Workbook()
    ws = wb.active
    # Don't add any data

    buffer = io.BytesIO()
    wb.save(buffer)
    buffer.seek(0)
    return buffer


@pytest.fixture
def text_file():
    """Create a text file with .txt extension."""
    content = b"This is a text file, not an Excel file"
    return io.BytesIO(content)


@pytest.fixture
def pdf_renamed_as_xlsx():
    """Create a PDF file renamed to .xlsx."""
    # PDF magic bytes
    content = b"%PDF-1.4\n%Fake PDF content for testing" * 100
    return io.BytesIO(content)


@pytest.fixture
def corrupted_excel_file():
    """Create a corrupted Excel file with valid magic bytes but invalid structure."""
    # Valid ZIP signature but garbage content
    content = b"PK\x03\x04" + b"\xff\xfe\xfd\xfc" * 500
    return io.BytesIO(content)


@pytest.fixture
def large_excel_file():
    """Create a large Excel file exceeding 50MB limit."""
    # Create a file larger than MAX_UPLOAD_SIZE_MB
    size_mb = settings.max_upload_size_mb + 1
    file_content = b"x" * (size_mb * 1024 * 1024)
    return io.BytesIO(file_content)


class TestDocumentUpload:
    """Tests for POST /documents endpoint."""

    @patch("src.api.routes.documents.process_document")
    def test_upload_valid_document_returns_202(
        self,
        mock_celery_task,
        test_client,
        test_api_key,
        sample_excel_file,
    ):
        """Test successful document upload returns 202 with correct response."""
        test_key, api_key_id = test_api_key

        # Mock Celery task
        mock_task = MagicMock()
        mock_task.id = "test-task-id-12345"
        mock_celery_task.delay.return_value = mock_task

        # Prepare file upload
        sample_excel_file.seek(0)
        files = {"file": ("test_document.xlsx", sample_excel_file, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}

        # Send request
        response = test_client.post(
            "/documents",
            files=files,
            headers={"X-API-Key": test_key},
        )

        # Assertions
        assert response.status_code == 202
        data = response.json()
        assert "document_id" in data
        assert data["filename"] == "test_document.xlsx"
        assert data["status"] == "pending"
        assert "queued for processing" in data["message"]

        # Verify Celery task was queued
        mock_celery_task.delay.assert_called_once()

        # Cleanup: delete created document
        document_id = data["document_id"]
        with get_sync_session() as session:
            doc = session.execute(
                select(Document).where(Document.id == document_id)
            ).scalar_one_or_none()
            if doc:
                session.delete(doc)
                session.commit()

        # Cleanup: delete uploaded file
        storage_path = Path("storage") / document_id
        if storage_path.exists():
            for file in storage_path.iterdir():
                file.unlink()
            storage_path.rmdir()

    @patch("src.api.routes.documents.process_document")
    def test_upload_creates_database_record(
        self,
        mock_celery_task,
        test_client,
        test_api_key,
        sample_excel_file,
    ):
        """Test that document record is created in database."""
        test_key, api_key_id = test_api_key

        # Mock Celery task
        mock_task = MagicMock()
        mock_task.id = "test-task-id-67890"
        mock_celery_task.delay.return_value = mock_task

        # Prepare file upload
        sample_excel_file.seek(0)
        files = {"file": ("db_test.xlsx", sample_excel_file, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}

        # Send request
        response = test_client.post(
            "/documents",
            files=files,
            headers={"X-API-Key": test_key},
        )

        assert response.status_code == 202
        document_id = response.json()["document_id"]

        # Verify database record
        with get_sync_session() as session:
            doc = session.execute(
                select(Document).where(Document.id == document_id)
            ).scalar_one_or_none()

            assert doc is not None
            assert doc.filename == "db_test.xlsx"
            assert doc.status == "pending"
            assert doc.file_path.startswith("storage/")
            assert doc.file_size_bytes > 0
            assert str(doc.api_key_id) == str(api_key_id)

            # Cleanup
            session.delete(doc)
            session.commit()

        # Cleanup: delete uploaded file
        storage_path = Path("storage") / document_id
        if storage_path.exists():
            for file in storage_path.iterdir():
                file.unlink()
            storage_path.rmdir()

    @patch("src.api.routes.documents.process_document")
    def test_upload_saves_file_to_storage(
        self,
        mock_celery_task,
        test_client,
        test_api_key,
        sample_excel_file,
    ):
        """Test that file is saved to correct storage path."""
        test_key, api_key_id = test_api_key

        # Mock Celery task
        mock_task = MagicMock()
        mock_task.id = "test-task-id-storage"
        mock_celery_task.delay.return_value = mock_task

        # Prepare file upload
        sample_excel_file.seek(0)
        files = {"file": ("storage_test.xlsx", sample_excel_file, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}

        # Send request
        response = test_client.post(
            "/documents",
            files=files,
            headers={"X-API-Key": test_key},
        )

        assert response.status_code == 202
        document_id = response.json()["document_id"]

        # Verify file exists on filesystem
        expected_path = Path("storage") / document_id / "storage_test.xlsx"
        assert expected_path.exists()
        assert expected_path.is_file()
        assert expected_path.stat().st_size > 0

        # Cleanup: delete document record
        with get_sync_session() as session:
            doc = session.execute(
                select(Document).where(Document.id == document_id)
            ).scalar_one_or_none()
            if doc:
                session.delete(doc)
                session.commit()

        # Cleanup: delete uploaded file
        if expected_path.exists():
            expected_path.unlink()
        if expected_path.parent.exists():
            expected_path.parent.rmdir()

    def test_upload_file_too_large_returns_413(
        self,
        test_client,
        test_api_key,
        large_excel_file,
    ):
        """Test that files exceeding 50MB return 413 Payload Too Large."""
        test_key, _ = test_api_key

        # Prepare large file upload
        large_excel_file.seek(0)
        files = {"file": ("large_file.xlsx", large_excel_file, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}

        # Send request
        response = test_client.post(
            "/documents",
            files=files,
            headers={"X-API-Key": test_key},
        )

        # Assertions
        assert response.status_code == 413
        data = response.json()
        assert "detail" in data
        assert "error" in data["detail"]
        assert data["detail"]["error"]["code"] == "FILE_TOO_LARGE"
        assert str(settings.max_upload_size_mb) in data["detail"]["error"]["message"]

    def test_upload_without_api_key_returns_401(
        self,
        test_client,
        sample_excel_file,
    ):
        """Test that upload without API key returns 401 Unauthorized."""
        # Prepare file upload
        sample_excel_file.seek(0)
        files = {"file": ("no_auth.xlsx", sample_excel_file, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}

        # Send request without X-API-Key header
        response = test_client.post(
            "/documents",
            files=files,
        )

        # Assertions
        assert response.status_code == 401
        data = response.json()
        assert "detail" in data
        assert "error" in data["detail"]
        assert data["detail"]["error"]["code"] == "MISSING_API_KEY"

    def test_upload_with_invalid_api_key_returns_401(
        self,
        test_client,
        sample_excel_file,
    ):
        """Test that upload with invalid API key returns 401."""
        # Prepare file upload
        sample_excel_file.seek(0)
        files = {"file": ("invalid_auth.xlsx", sample_excel_file, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}

        # Send request with invalid API key
        response = test_client.post(
            "/documents",
            files=files,
            headers={"X-API-Key": "invalid_key_12345"},
        )

        # Assertions
        assert response.status_code == 401
        data = response.json()
        assert "detail" in data
        assert "error" in data["detail"]
        assert data["detail"]["error"]["code"] == "INVALID_API_KEY"

    @patch("src.api.routes.documents.process_document")
    def test_celery_task_queued_with_correct_document_id(
        self,
        mock_celery_task,
        test_client,
        test_api_key,
        sample_excel_file,
    ):
        """Test that Celery task is queued with correct document_id."""
        test_key, api_key_id = test_api_key

        # Mock Celery task
        mock_task = MagicMock()
        mock_task.id = "test-celery-task-id"
        mock_celery_task.delay.return_value = mock_task

        # Prepare file upload
        sample_excel_file.seek(0)
        files = {"file": ("celery_test.xlsx", sample_excel_file, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}

        # Send request
        response = test_client.post(
            "/documents",
            files=files,
            headers={"X-API-Key": test_key},
        )

        assert response.status_code == 202
        document_id = response.json()["document_id"]

        # Verify Celery task was called with correct document_id
        mock_celery_task.delay.assert_called_once_with(document_id)

        # Cleanup
        with get_sync_session() as session:
            doc = session.execute(
                select(Document).where(Document.id == document_id)
            ).scalar_one_or_none()
            if doc:
                session.delete(doc)
                session.commit()

        # Cleanup: delete uploaded file
        storage_path = Path("storage") / document_id
        if storage_path.exists():
            for file in storage_path.iterdir():
                file.unlink()
            storage_path.rmdir()

    # ==================== File Validation Tests (Story 2.3) ====================

    def test_upload_txt_file_returns_400_invalid_extension(
        self,
        test_client,
        test_api_key,
        text_file,
    ):
        """Test that .txt file returns 400 with INVALID_EXTENSION error."""
        test_key, _ = test_api_key

        # Prepare text file upload
        text_file.seek(0)
        files = {"file": ("document.txt", text_file, "text/plain")}

        # Send request
        response = test_client.post(
            "/documents",
            files=files,
            headers={"X-API-Key": test_key},
        )

        # Assertions
        assert response.status_code == 400
        data = response.json()
        assert "detail" in data
        assert "error" in data["detail"]
        assert data["detail"]["error"]["code"] == "INVALID_EXTENSION"
        assert ".txt" in data["detail"]["error"]["message"]

    def test_upload_pdf_renamed_to_xlsx_returns_400_invalid_format(
        self,
        test_client,
        test_api_key,
        pdf_renamed_as_xlsx,
    ):
        """Test that renamed PDF file returns 400 with INVALID_FORMAT error."""
        test_key, _ = test_api_key

        # Prepare renamed PDF file
        pdf_renamed_as_xlsx.seek(0)
        files = {"file": ("renamed.xlsx", pdf_renamed_as_xlsx, "application/pdf")}

        # Send request
        response = test_client.post(
            "/documents",
            files=files,
            headers={"X-API-Key": test_key},
        )

        # Assertions
        assert response.status_code == 400
        data = response.json()
        assert "detail" in data
        assert "error" in data["detail"]
        assert data["detail"]["error"]["code"] == "INVALID_FORMAT"
        assert "signature" in data["detail"]["error"]["message"].lower()

    def test_upload_corrupted_excel_returns_400_corrupted_file(
        self,
        test_client,
        test_api_key,
        corrupted_excel_file,
    ):
        """Test that corrupted Excel file returns 400 with CORRUPTED_FILE error."""
        test_key, _ = test_api_key

        # Prepare corrupted file
        corrupted_excel_file.seek(0)
        files = {"file": ("corrupted.xlsx", corrupted_excel_file, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}

        # Send request
        response = test_client.post(
            "/documents",
            files=files,
            headers={"X-API-Key": test_key},
        )

        # Assertions
        assert response.status_code == 400
        data = response.json()
        assert "detail" in data
        assert "error" in data["detail"]
        assert data["detail"]["error"]["code"] == "CORRUPTED_FILE"
        assert "cannot be opened" in data["detail"]["error"]["message"].lower()

    def test_upload_empty_excel_returns_400_empty_file(
        self,
        test_client,
        test_api_key,
        empty_excel_file,
    ):
        """Test that empty Excel file returns 400 with EMPTY_FILE error."""
        test_key, _ = test_api_key

        # Prepare empty file
        empty_excel_file.seek(0)
        files = {"file": ("empty.xlsx", empty_excel_file, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}

        # Send request
        response = test_client.post(
            "/documents",
            files=files,
            headers={"X-API-Key": test_key},
        )

        # Assertions
        assert response.status_code == 400
        data = response.json()
        assert "detail" in data
        assert "error" in data["detail"]
        assert data["detail"]["error"]["code"] == "EMPTY_FILE"
        assert "empty" in data["detail"]["error"]["message"].lower()

    @patch("src.api.routes.documents.process_document")
    def test_upload_valid_xlsx_passes_validation(
        self,
        mock_celery_task,
        test_client,
        test_api_key,
        sample_excel_file,
    ):
        """Test that valid .xlsx file passes all validation checks."""
        test_key, _ = test_api_key

        # Mock Celery task
        mock_task = MagicMock()
        mock_task.id = "test-validation-task"
        mock_celery_task.delay.return_value = mock_task

        # Prepare valid Excel file
        sample_excel_file.seek(0)
        files = {"file": ("valid_document.xlsx", sample_excel_file, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}

        # Send request
        response = test_client.post(
            "/documents",
            files=files,
            headers={"X-API-Key": test_key},
        )

        # Assertions - should pass all validations
        assert response.status_code == 202
        data = response.json()
        assert data["status"] == "pending"
        assert data["filename"] == "valid_document.xlsx"

        # Cleanup
        document_id = data["document_id"]
        with get_sync_session() as session:
            doc = session.execute(
                select(Document).where(Document.id == document_id)
            ).scalar_one_or_none()
            if doc:
                session.delete(doc)
                session.commit()

        # Cleanup: delete uploaded file
        storage_path = Path("storage") / document_id
        if storage_path.exists():
            for file in storage_path.iterdir():
                file.unlink()
            storage_path.rmdir()

    def test_validation_error_response_structure(
        self,
        test_client,
        test_api_key,
        text_file,
    ):
        """Test that validation errors return consistent response structure."""
        test_key, _ = test_api_key

        text_file.seek(0)
        files = {"file": ("test.txt", text_file, "text/plain")}

        response = test_client.post(
            "/documents",
            files=files,
            headers={"X-API-Key": test_key},
        )

        # Verify response structure matches spec
        assert response.status_code == 400
        data = response.json()

        # Must have detail.error structure
        assert "detail" in data
        assert isinstance(data["detail"], dict)
        assert "error" in data["detail"]

        # Error must have code and message
        error = data["detail"]["error"]
        assert "code" in error
        assert "message" in error
        assert isinstance(error["code"], str)
        assert isinstance(error["message"], str)
        assert len(error["code"]) > 0
        assert len(error["message"]) > 0
