"""Unit tests for service order workshop integration.

These tests use mocked DB cursors and inventory_engine so they do not require
PostgreSQL or network access.
"""

import os
import sys

POS_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, POS_DIR)

from unittest import mock  # noqa: E402

import pytest  # noqa: E402
from services import service_order_engine as engine  # noqa: E402


class MockCursor:
    """Simple programmable cursor mock."""

    def __init__(self, responses=None):
        self.responses = responses or []
        self._calls = []
        self._response_index = 0

    def execute(self, sql, params=None):
        self._calls.append((sql, params))

    def fetchone(self):
        if self._response_index < len(self.responses):
            resp = self.responses[self._response_index]
            self._response_index += 1
            return resp
        return None

    def fetchall(self):
        if self._response_index < len(self.responses):
            resp = self.responses[self._response_index]
            self._response_index += 1
            return resp
        return []

    def close(self):
        pass


class MockConn:
    def __init__(self, cursor):
        self._cursor = cursor

    def cursor(self):
        return self._cursor

    def commit(self):
        pass

    def rollback(self):
        pass


@pytest.fixture
def conn():
    return MockConn(MockCursor())


def test_generate_order_number_first_of_day(conn):
    conn._cursor.responses = [(None,)]
    number = engine._generate_order_number(conn)
    # Format DDMMYYYY-N
    assert len(number.split("-")) == 2
    assert number.split("-")[1] == "1"


def test_generate_order_number_increments(conn):
    from datetime import datetime
    today = datetime.utcnow().strftime('%d%m%Y')
    conn._cursor.responses = [(f"{today}-42",)]
    number = engine._generate_order_number(conn)
    assert number == f"{today}-43"


@mock.patch("services.inventory_engine.get_stock", return_value=10)
@mock.patch("services.inventory_engine.record_operation", return_value=123)
def test_reserve_item_inserts_so_reserve_and_updates_quantity(mock_record, mock_stock, conn):

    conn._cursor.responses = [
        (1, 5, 3, "pending", "SO-2026-0001", 2),  # item lookup (branch_id=2)
        None,  # update
    ]

    result = engine.reserve_item(conn, 7, branch_id=99, employee_id=9)

    assert result["reserved"] == 3
    mock_stock.assert_called_once_with(conn, 5, 2)
    mock_record.assert_called_once()
    args = mock_record.call_args.args
    assert args[3] == "SO_RESERVE"
    assert args[4] == -3


@mock.patch("services.inventory_engine.get_stock", return_value=1)
def test_reserve_item_raises_when_insufficient_stock(mock_stock, conn):
    conn._cursor.responses = [
        (1, 5, 3, "pending", "SO-2026-0001", 2),
    ]

    with pytest.raises(ValueError, match="Insufficient stock"):
        engine.reserve_item(conn, 7, branch_id=99)


@mock.patch("services.inventory_engine.record_operation", return_value=124)
def test_release_item_restores_stock(mock_record, conn):
    conn._cursor.responses = [
        (1, 5, 2, 2, "SO-2026-0001"),  # item lookup (reserved_quantity=2)
        None,  # update
    ]

    result = engine.release_item(conn, 7, employee_id=9)

    assert result["released"] == 2
    args = mock_record.call_args.args
    assert args[3] == "SO_RELEASE"
    assert args[4] == 2


@mock.patch("services.inventory_engine.record_operation", return_value=125)
def test_convert_to_sale_creates_sale_and_consumes_inventory(mock_record, conn):
    # Mock get_service_order response
    so = {
        "id": 1,
        "order_number": "SO-2026-0001",
        "status": "ready",
        "sale_id": None,
        "branch_id": 2,
        "customer_id": 3,
        "items": [
            {
                "id": 10,
                "inventory_id": 5,
                "part_number": "BP-123",
                "name": "Bujia",
                "quantity": 2,
                "unit_price": 150.0,
                "unit_cost": 80.0,
                "status": "pending",
                "reserved_quantity": 2,
            }
        ],
        "labor": [
            {
                "description": "Cambio de bujias",
                "hours": 1,
                "hourly_rate": 250,
                "total_cost": 250,
                "status": "completed",
            }
        ],
    }

    cur = conn._cursor
    cur.responses = [
        (1, "2026-01-01 10:00:00"),  # sale insert -> id=1
    ]

    with mock.patch.object(engine, "get_service_order", return_value=so):
        result = engine.convert_to_sale(conn, 1, {"payment_method": "efectivo", "sale_type": "cash"}, employee_id=9)

    assert result["sale_id"] == 1
    assert result["items_count"] == 2
    assert result["total"] == pytest.approx(2 * 150 * 1.16 + 250 * 1.16, 0.01)

    # inventory operations: SO_RELEASE + SALE
    assert mock_record.call_count == 2
    first = mock_record.call_args_list[0].args
    second = mock_record.call_args_list[1].args
    assert first[3] == "SO_RELEASE"
    assert first[4] == 2
    assert second[3] == "SALE"
    assert second[4] == -2


def test_convert_to_sale_raises_when_already_converted(conn):
    so = {"status": "ready", "sale_id": 99, "branch_id": 1, "customer_id": 1, "items": [], "labor": []}
    conn._cursor.responses = [(1,)]
    with (
        mock.patch.object(engine, "get_service_order", return_value=so),
        pytest.raises(ValueError, match="already converted"),
    ):
        engine.convert_to_sale(conn, 1, {})


def test_convert_to_sale_raises_when_cancelled(conn):
    so = {"status": "cancelled", "sale_id": None, "branch_id": 1, "customer_id": 1, "items": [], "labor": []}
    conn._cursor.responses = [(1,)]
    with (
        mock.patch.object(engine, "get_service_order", return_value=so),
        pytest.raises(ValueError, match="cancelled"),
    ):
        engine.convert_to_sale(conn, 1, {})


def test_assign_mechanic_updates_employee(conn):
    conn._cursor.responses = [(1,), None]
    result = engine.assign_mechanic(conn, 1, 7)
    assert result["employee_id"] == 7


def test_assign_mechanic_raises_when_order_missing(conn):
    conn._cursor.responses = [None]
    with pytest.raises(ValueError, match="not found"):
        engine.assign_mechanic(conn, 1, 7)


def test_service_catalog_crud(conn):
    # create
    conn._cursor.responses = [(1,)]
    result = engine.create_service_catalog_item(
        conn, 1, {"name": "Afinacion", "suggested_hours": 2, "suggested_rate": 300}
    )
    assert result["id"] == 1

    # list
    conn._cursor = MockCursor(
        [
            [(1, 1, "Afinacion", "", 2, 300, True, None, None)],
        ]
    )
    items = engine.list_service_catalog(conn)
    assert len(items) == 1
    assert items[0]["name"] == "Afinacion"

    # update
    conn._cursor = MockCursor([None])
    ok = engine.update_service_catalog_item(conn, 1, {"name": "Afinacion mayor"})
    assert ok is True

    # delete
    conn._cursor = MockCursor([None])
    ok = engine.delete_service_catalog_item(conn, 1)
    assert ok is True
