"""Integration tests for background dispatch API behavior."""

from unittest.mock import AsyncMock, patch

import pytest
from httpx import AsyncClient

from backend.app.services.background_dispatch import DispatchEnqueueRejected


class TestBackgroundDispatchArchivesAPI:
    """Tests for archive reprint dispatch endpoint."""

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_reprint_returns_dispatched_payload(
        self, async_client: AsyncClient, archive_factory, printer_factory, db_session, tmp_path
    ):
        """Reprint endpoint returns background dispatch metadata."""
        printer = await printer_factory()
        archive = await archive_factory(
            printer.id,
            filename="widget.gcode.3mf",
            file_path="archives/test/widget.gcode.3mf",
        )

        archive_file = tmp_path / archive.file_path
        archive_file.parent.mkdir(parents=True, exist_ok=True)
        archive_file.write_bytes(b"3mf-data")

        with (
            patch("backend.app.api.routes.archives.settings.base_dir", tmp_path),
            patch("backend.app.services.printer_manager.printer_manager.is_connected", return_value=True),
            patch(
                "backend.app.services.background_dispatch.background_dispatch.dispatch_reprint_archive",
                new=AsyncMock(return_value={"dispatch_job_id": 15, "dispatch_position": 1}),
            ) as mock_dispatch,
        ):
            response = await async_client.post(
                f"/api/v1/archives/{archive.id}/reprint?printer_id={printer.id}",
                json={"plate_id": 2},
            )

        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "dispatched"
        assert data["dispatch_job_id"] == 15
        assert data["dispatch_position"] == 1
        assert data["filename"] == "widget.gcode.3mf"

        mock_dispatch.assert_awaited_once()
        kwargs = mock_dispatch.await_args.kwargs
        assert kwargs["archive_name"].endswith("• Plate 2")
        assert kwargs["options"]["plate_id"] == 2

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_reprint_returns_409_when_enqueue_rejected(
        self, async_client: AsyncClient, archive_factory, printer_factory, db_session, tmp_path
    ):
        """Reprint endpoint maps enqueue rejection to HTTP 409."""
        printer = await printer_factory()
        archive = await archive_factory(
            printer.id,
            filename="widget2.gcode.3mf",
            file_path="archives/test/widget2.gcode.3mf",
        )

        archive_file = tmp_path / archive.file_path
        archive_file.parent.mkdir(parents=True, exist_ok=True)
        archive_file.write_bytes(b"3mf-data")

        with (
            patch("backend.app.api.routes.archives.settings.base_dir", tmp_path),
            patch("backend.app.services.printer_manager.printer_manager.is_connected", return_value=True),
            patch(
                "backend.app.services.background_dispatch.background_dispatch.dispatch_reprint_archive",
                new=AsyncMock(side_effect=DispatchEnqueueRejected("already has a background dispatch")),
            ),
        ):
            response = await async_client.post(
                f"/api/v1/archives/{archive.id}/reprint?printer_id={printer.id}",
                json={"plate_id": 1},
            )

        assert response.status_code == 409
        assert "already has a background dispatch" in response.json()["detail"]


class TestBackgroundDispatchLibraryAPI:
    """Tests for library print dispatch endpoint."""

    @pytest.fixture
    async def library_file_factory(self, db_session):
        """Factory to create library files."""

        async def _create_file(**kwargs):
            from backend.app.models.library import LibraryFile

            defaults = {
                "filename": "library_part.gcode.3mf",
                "file_path": "library/files/library_part.gcode.3mf",
                "file_type": "gcode",
                "file_size": 1024,
            }
            defaults.update(kwargs)
            lib_file = LibraryFile(**defaults)
            db_session.add(lib_file)
            await db_session.commit()
            await db_session.refresh(lib_file)
            return lib_file

        return _create_file

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_library_print_returns_dispatched_payload(
        self, async_client: AsyncClient, library_file_factory, printer_factory, db_session, tmp_path
    ):
        """Library print endpoint returns dispatch job metadata."""
        printer = await printer_factory()
        lib_file = await library_file_factory()

        disk_path = tmp_path / lib_file.file_path
        disk_path.parent.mkdir(parents=True, exist_ok=True)
        disk_path.write_bytes(b"library data")

        with (
            patch("backend.app.api.routes.library.app_settings.base_dir", tmp_path),
            patch("backend.app.services.printer_manager.printer_manager.is_connected", return_value=True),
            patch(
                "backend.app.services.background_dispatch.background_dispatch.dispatch_print_library_file",
                new=AsyncMock(return_value={"dispatch_job_id": 21, "dispatch_position": 2}),
            ) as mock_dispatch,
        ):
            response = await async_client.post(
                f"/api/v1/library/files/{lib_file.id}/print?printer_id={printer.id}",
                json={"plate_id": 4},
            )

        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "dispatched"
        assert data["dispatch_job_id"] == 21
        assert data["dispatch_position"] == 2
        assert data["archive_id"] is None

        mock_dispatch.assert_awaited_once()
        kwargs = mock_dispatch.await_args.kwargs
        assert kwargs["filename"].endswith("• Plate 4")
        assert kwargs["options"]["plate_id"] == 4

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_library_print_returns_409_when_enqueue_rejected(
        self, async_client: AsyncClient, library_file_factory, printer_factory, db_session, tmp_path
    ):
        """Library print endpoint maps enqueue rejection to HTTP 409."""
        printer = await printer_factory()
        lib_file = await library_file_factory(filename="another_part.gcode")

        disk_path = tmp_path / lib_file.file_path
        disk_path.parent.mkdir(parents=True, exist_ok=True)
        disk_path.write_bytes(b"library data")

        with (
            patch("backend.app.api.routes.library.app_settings.base_dir", tmp_path),
            patch("backend.app.services.printer_manager.printer_manager.is_connected", return_value=True),
            patch(
                "backend.app.services.background_dispatch.background_dispatch.dispatch_print_library_file",
                new=AsyncMock(side_effect=DispatchEnqueueRejected("queue conflict")),
            ),
        ):
            response = await async_client.post(
                f"/api/v1/library/files/{lib_file.id}/print?printer_id={printer.id}",
                json={"plate_id": 1},
            )

        assert response.status_code == 409
        assert "queue conflict" in response.json()["detail"]

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_library_print_cleanup_flag_defaults_false(
        self, async_client: AsyncClient, library_file_factory, printer_factory, db_session, tmp_path
    ):
        """Absent cleanup_library_after_dispatch in the request body ⇒ False reaches the dispatcher.
        Guards the File Manager / Project Detail paths from accidental deletion."""
        printer = await printer_factory()
        lib_file = await library_file_factory(filename="filemgr_part.gcode.3mf")

        disk_path = tmp_path / lib_file.file_path
        disk_path.parent.mkdir(parents=True, exist_ok=True)
        disk_path.write_bytes(b"library data")

        with (
            patch("backend.app.api.routes.library.app_settings.base_dir", tmp_path),
            patch("backend.app.services.printer_manager.printer_manager.is_connected", return_value=True),
            patch(
                "backend.app.services.background_dispatch.background_dispatch.dispatch_print_library_file",
                new=AsyncMock(return_value={"dispatch_job_id": 30, "dispatch_position": 1}),
            ) as mock_dispatch,
        ):
            response = await async_client.post(
                f"/api/v1/library/files/{lib_file.id}/print?printer_id={printer.id}",
                json={},
            )

        assert response.status_code == 200
        mock_dispatch.assert_awaited_once()
        assert mock_dispatch.await_args.kwargs["cleanup_library_after_dispatch"] is False
        # cleanup flag must never leak into the print-option dict forwarded to MQTT
        assert "cleanup_library_after_dispatch" not in mock_dispatch.await_args.kwargs["options"]

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_library_print_forwards_cleanup_flag_true(
        self, async_client: AsyncClient, library_file_factory, printer_factory, db_session, tmp_path
    ):
        """Direct-Print flow sends cleanup_library_after_dispatch=True, which must reach the dispatcher."""
        printer = await printer_factory()
        lib_file = await library_file_factory(filename="transient_part.gcode.3mf")

        disk_path = tmp_path / lib_file.file_path
        disk_path.parent.mkdir(parents=True, exist_ok=True)
        disk_path.write_bytes(b"library data")

        with (
            patch("backend.app.api.routes.library.app_settings.base_dir", tmp_path),
            patch("backend.app.services.printer_manager.printer_manager.is_connected", return_value=True),
            patch(
                "backend.app.services.background_dispatch.background_dispatch.dispatch_print_library_file",
                new=AsyncMock(return_value={"dispatch_job_id": 31, "dispatch_position": 1}),
            ) as mock_dispatch,
        ):
            response = await async_client.post(
                f"/api/v1/library/files/{lib_file.id}/print?printer_id={printer.id}",
                json={"cleanup_library_after_dispatch": True},
            )

        assert response.status_code == 200
        mock_dispatch.assert_awaited_once()
        assert mock_dispatch.await_args.kwargs["cleanup_library_after_dispatch"] is True


class TestBackgroundDispatchCancelAPI:
    """Tests for /background-dispatch cancel endpoint."""

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_cancel_job_returns_cancelled(self, async_client: AsyncClient):
        """Cancel endpoint returns cancelled for queued job."""
        with patch(
            "backend.app.services.background_dispatch.background_dispatch.cancel_job",
            new=AsyncMock(
                return_value={
                    "cancelled": True,
                    "pending": False,
                    "job_id": 9,
                    "source_name": "cube.gcode.3mf",
                    "printer_id": 1,
                    "printer_name": "Printer A",
                }
            ),
        ):
            response = await async_client.delete("/api/v1/background-dispatch/9")

        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "cancelled"
        assert data["job_id"] == 9

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_cancel_job_returns_cancelling_for_active_job(self, async_client: AsyncClient):
        """Cancel endpoint returns cancelling while active upload is being interrupted."""
        with patch(
            "backend.app.services.background_dispatch.background_dispatch.cancel_job",
            new=AsyncMock(
                return_value={
                    "cancelled": True,
                    "pending": True,
                    "job_id": 10,
                    "source_name": "cube.gcode.3mf",
                    "printer_id": 1,
                    "printer_name": "Printer A",
                }
            ),
        ):
            response = await async_client.delete("/api/v1/background-dispatch/10")

        assert response.status_code == 200
        assert response.json()["status"] == "cancelling"

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_cancel_job_returns_404_when_not_found(self, async_client: AsyncClient):
        """Cancel endpoint returns 404 for unknown job id."""
        with patch(
            "backend.app.services.background_dispatch.background_dispatch.cancel_job",
            new=AsyncMock(return_value={"cancelled": False, "reason": "not_found"}),
        ):
            response = await async_client.delete("/api/v1/background-dispatch/999")

        assert response.status_code == 404
        assert response.json()["detail"] == "Dispatch job not found"
