"""Integration tests for POST /api/v1/spoolman/inventory/sync-ams-weights.

Covers:
  - happy path: synced count incremented, update_spool_full called with correct weight
  - printer offline: assignment skipped
  - spool missing from Spoolman: assignment skipped
  - invalid remain value: assignment skipped
"""

from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from httpx import AsyncClient

SAMPLE_SPOOL = {
    "id": 42,
    "filament": {
        "id": 1,
        "name": "PLA Basic",
        "material": "PLA",
        "weight": 1000,
        "color_hex": "FF0000",
        "vendor": {"id": 1, "name": "BrandX"},
    },
    "remaining_weight": 800.0,
    "used_weight": 200.0,
    "location": None,
    "comment": None,
    "first_used": None,
    "last_used": None,
    "registered": "2024-01-01T00:00:00+00:00",
    "archived": False,
    "price": None,
    "extra": {},
}


@pytest.fixture
async def sync_settings(db_session):
    from backend.app.models.settings import Settings

    db_session.add(Settings(key="spoolman_enabled", value="true"))
    db_session.add(Settings(key="spoolman_url", value="http://localhost:7912"))
    await db_session.commit()


@pytest.fixture
async def test_printer(db_session):
    from backend.app.models.printer import Printer

    printer = Printer(
        name="Sync Printer",
        serial_number="SYNCTEST001",
        ip_address="192.168.1.50",
        access_code="12345678",
    )
    db_session.add(printer)
    await db_session.commit()
    await db_session.refresh(printer)
    return printer


@pytest.fixture
async def slot_assignment(db_session, test_printer):
    from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment

    assignment = SpoolmanSlotAssignment(
        printer_id=test_printer.id,
        ams_id=0,
        tray_id=0,
        spoolman_spool_id=42,
    )
    db_session.add(assignment)
    await db_session.commit()
    return assignment


def _make_spoolman_client(spools=None):
    client = MagicMock()
    client.base_url = "http://localhost:7912"
    client.health_check = AsyncMock(return_value=True)
    client.get_all_spools = AsyncMock(return_value=[SAMPLE_SPOOL] if spools is None else spools)
    client.update_spool_full = AsyncMock(return_value=SAMPLE_SPOOL)
    return client


def _make_printer_state(remain=75):
    state = MagicMock()
    state.raw_data = {
        "ams": [
            {
                "id": 0,
                "tray": [{"id": 0, "remain": remain}],
            }
        ]
    }
    return state


class TestSyncSpoolmanAmsWeights:
    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_happy_path_synced_count(
        self, async_client: AsyncClient, sync_settings, test_printer, slot_assignment
    ):
        """POST /sync-ams-weights syncs one spool, returns synced=1, skipped=0."""
        spoolman_client = _make_spoolman_client()
        printer_state = _make_printer_state(remain=75)

        with (
            patch(
                "backend.app.api.routes.spoolman_inventory._get_client",
                AsyncMock(return_value=spoolman_client),
            ),
            patch("backend.app.api.routes.spoolman_inventory.printer_manager") as pm_mock,
        ):
            pm_mock.get_status = MagicMock(return_value=printer_state)

            response = await async_client.post("/api/v1/spoolman/inventory/sync-ams-weights")

        assert response.status_code == 200
        body = response.json()
        assert body["synced"] == 1
        assert body["skipped"] == 0

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_weight_calculated_correctly(
        self, async_client: AsyncClient, sync_settings, test_printer, slot_assignment
    ):
        """Remaining weight = round(label_weight * remain / 100, 1)."""
        spoolman_client = _make_spoolman_client()
        printer_state = _make_printer_state(remain=75)

        with (
            patch(
                "backend.app.api.routes.spoolman_inventory._get_client",
                AsyncMock(return_value=spoolman_client),
            ),
            patch("backend.app.api.routes.spoolman_inventory.printer_manager") as pm_mock,
        ):
            pm_mock.get_status = MagicMock(return_value=printer_state)

            await async_client.post("/api/v1/spoolman/inventory/sync-ams-weights")

        spoolman_client.update_spool_full.assert_called_once_with(42, remaining_weight=750.0)

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_printer_offline_skipped(
        self, async_client: AsyncClient, sync_settings, test_printer, slot_assignment
    ):
        """Spools whose printer is offline are counted as skipped, not synced."""
        spoolman_client = _make_spoolman_client()

        with (
            patch(
                "backend.app.api.routes.spoolman_inventory._get_client",
                AsyncMock(return_value=spoolman_client),
            ),
            patch("backend.app.api.routes.spoolman_inventory.printer_manager") as pm_mock,
        ):
            pm_mock.get_status = MagicMock(return_value=None)

            response = await async_client.post("/api/v1/spoolman/inventory/sync-ams-weights")

        assert response.status_code == 200
        body = response.json()
        assert body["synced"] == 0
        assert body["skipped"] == 1
        spoolman_client.update_spool_full.assert_not_called()

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_update_spool_full_error_counted_as_skipped(
        self, async_client: AsyncClient, sync_settings, test_printer, slot_assignment
    ):
        """update_spool_full raising HTTPException counts as skipped, not synced."""
        from fastapi import HTTPException

        spoolman_client = _make_spoolman_client()
        spoolman_client.update_spool_full = AsyncMock(side_effect=HTTPException(status_code=503))
        printer_state = _make_printer_state(remain=50)

        with (
            patch(
                "backend.app.api.routes.spoolman_inventory._get_client",
                AsyncMock(return_value=spoolman_client),
            ),
            patch("backend.app.api.routes.spoolman_inventory.printer_manager") as pm_mock,
        ):
            pm_mock.get_status = MagicMock(return_value=printer_state)

            response = await async_client.post("/api/v1/spoolman/inventory/sync-ams-weights")

        assert response.status_code == 200
        body = response.json()
        assert body["synced"] == 0
        assert body["skipped"] == 1

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_invalid_remain_value_skipped(
        self, async_client: AsyncClient, sync_settings, test_printer, slot_assignment
    ):
        """Non-numeric remain value in AMS data is counted as skipped."""
        spoolman_client = _make_spoolman_client()
        printer_state = _make_printer_state(remain="notanumber")

        with (
            patch(
                "backend.app.api.routes.spoolman_inventory._get_client",
                AsyncMock(return_value=spoolman_client),
            ),
            patch("backend.app.api.routes.spoolman_inventory.printer_manager") as pm_mock,
        ):
            pm_mock.get_status = MagicMock(return_value=printer_state)

            response = await async_client.post("/api/v1/spoolman/inventory/sync-ams-weights")

        assert response.status_code == 200
        body = response.json()
        assert body["synced"] == 0
        assert body["skipped"] == 1
        spoolman_client.update_spool_full.assert_not_called()

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_spool_missing_from_spoolman_skipped(
        self, async_client: AsyncClient, sync_settings, test_printer, slot_assignment
    ):
        """Spools not present in Spoolman are counted as skipped."""
        spoolman_client = _make_spoolman_client(spools=[])  # empty — spool 42 is gone
        printer_state = _make_printer_state(remain=50)

        with (
            patch(
                "backend.app.api.routes.spoolman_inventory._get_client",
                AsyncMock(return_value=spoolman_client),
            ),
            patch("backend.app.api.routes.spoolman_inventory.printer_manager") as pm_mock,
        ):
            pm_mock.get_status = MagicMock(return_value=printer_state)

            response = await async_client.post("/api/v1/spoolman/inventory/sync-ams-weights")

        assert response.status_code == 200
        body = response.json()
        assert body["synced"] == 0
        assert body["skipped"] == 1


# ---------------------------------------------------------------------------
# F6: AMS sync edge cases
# ---------------------------------------------------------------------------


class TestSyncAmsEdgeCases:
    """F6: Edge cases for remain values and AMS data format."""

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_null_remain_skipped(self, async_client: AsyncClient, sync_settings, test_printer, slot_assignment):
        """remain=null → tray skipped, synced=0 skipped=1."""
        spoolman_client = _make_spoolman_client()
        printer_state = _make_printer_state(remain=None)

        with (
            patch(
                "backend.app.api.routes.spoolman_inventory._get_client",
                AsyncMock(return_value=spoolman_client),
            ),
            patch("backend.app.api.routes.spoolman_inventory.printer_manager") as pm_mock,
        ):
            pm_mock.get_status = MagicMock(return_value=printer_state)
            resp = await async_client.post("/api/v1/spoolman/inventory/sync-ams-weights")

        assert resp.status_code == 200
        body = resp.json()
        assert body["synced"] == 0
        assert body["skipped"] == 1
        spoolman_client.update_spool_full.assert_not_called()

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_remain_above_100_skipped(
        self, async_client: AsyncClient, sync_settings, test_printer, slot_assignment
    ):
        """remain=101 → out-of-range, tray skipped."""
        spoolman_client = _make_spoolman_client()
        printer_state = _make_printer_state(remain=101)

        with (
            patch(
                "backend.app.api.routes.spoolman_inventory._get_client",
                AsyncMock(return_value=spoolman_client),
            ),
            patch("backend.app.api.routes.spoolman_inventory.printer_manager") as pm_mock,
        ):
            pm_mock.get_status = MagicMock(return_value=printer_state)
            resp = await async_client.post("/api/v1/spoolman/inventory/sync-ams-weights")

        assert resp.status_code == 200
        body = resp.json()
        assert body["synced"] == 0
        assert body["skipped"] == 1

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_negative_remain_skipped(
        self, async_client: AsyncClient, sync_settings, test_printer, slot_assignment
    ):
        """remain=-1 → out-of-range, tray skipped."""
        spoolman_client = _make_spoolman_client()
        printer_state = _make_printer_state(remain=-1)

        with (
            patch(
                "backend.app.api.routes.spoolman_inventory._get_client",
                AsyncMock(return_value=spoolman_client),
            ),
            patch("backend.app.api.routes.spoolman_inventory.printer_manager") as pm_mock,
        ):
            pm_mock.get_status = MagicMock(return_value=printer_state)
            resp = await async_client.post("/api/v1/spoolman/inventory/sync-ams-weights")

        assert resp.status_code == 200
        body = resp.json()
        assert body["synced"] == 0
        assert body["skipped"] == 1

    @pytest.mark.asyncio
    @pytest.mark.integration
    async def test_dict_wrapped_ams_format(
        self, async_client: AsyncClient, sync_settings, test_printer, slot_assignment
    ):
        """Double-nested ams format: raw_data['ams'] is a dict with 'ams' key → list."""
        spoolman_client = _make_spoolman_client()

        state = MagicMock()
        state.raw_data = {
            "ams": {
                "ams": [
                    {
                        "id": 0,
                        "tray": [{"id": 0, "remain": 75}],
                    }
                ]
            }
        }

        with (
            patch(
                "backend.app.api.routes.spoolman_inventory._get_client",
                AsyncMock(return_value=spoolman_client),
            ),
            patch("backend.app.api.routes.spoolman_inventory.printer_manager") as pm_mock,
        ):
            pm_mock.get_status = MagicMock(return_value=state)
            resp = await async_client.post("/api/v1/spoolman/inventory/sync-ams-weights")

        assert resp.status_code == 200
        body = resp.json()
        assert body["synced"] == 1
        assert body["skipped"] == 0
