import datetime as dt

import pytest
from sqlalchemy import select

from flyway.domain.errors import BusinessError, PermissionDenied
from flyway.models import AuditLog, ProductBatch, Supplier, TransferItem, TransferRelation
from flyway.services.load_in import LoadInHeader, LoadInLine, delete_load_ins, save_load_in, set_load_in_draft
from tests import builders as b


def _header(c, code="LI-1", **kw):
    return LoadInHeader(client_id=c.id, code=code, date=b.WHEN, from_location="Farm", from_rmp="R1", **kw)


def _setup(db):
    c = b.client(db)
    p = b.product(db, default_unit=1, available_unit="6")
    return c, p


def test_create_is_draft_and_auto_creates_batch_and_supplier(db):
    c, p = _setup(db)
    t = save_load_in(db, b.STAFF, _header(c), [LoadInLine(p.id, "B9", 10, used_by_date=dt.date(2027, 1, 1))])
    assert t.is_draft and not t.is_load_out and t.operator == "Sam Staff"
    batch = db.scalar(select(ProductBatch).where(ProductBatch.number == "B9"))
    assert batch.used_by_date == dt.date(2027, 1, 1)
    assert db.scalar(select(Supplier).where(Supplier.rmp_number == "R1")).name == "Farm"
    assert t.items[0].remaining_stock == 10
    assert db.scalar(select(AuditLog.action)) == "create"


def test_code_unique_ignoring_case_and_spaces(db):
    c, p = _setup(db)
    save_load_in(db, b.STAFF, _header(c, "LI-1"), [LoadInLine(p.id, "B1", 1)])
    with pytest.raises(BusinessError) as e:
        save_load_in(db, b.STAFF, _header(c, " li-1 "), [LoadInLine(p.id, "B1", 1)])
    assert e.value.field == "code"


def test_same_product_and_batch_lines_are_merged(db):
    c, p = _setup(db)
    t = save_load_in(db, b.STAFF, _header(c), [LoadInLine(p.id, "B1", 2, unit=6), LoadInLine(p.id, " b1", 3)])
    assert [(i.quantity, i.unit) for i in t.items] == [(15, 1)]


def test_naive_datetime_rejected(db):
    c, p = _setup(db)
    h = _header(c)
    h.date = dt.datetime(2026, 3, 2, 15, 0)
    with pytest.raises(BusinessError) as e:
        save_load_in(db, b.STAFF, h, [LoadInLine(p.id, "B1", 1)])
    assert e.value.field == "date"


def _completed_with_shipment(db, shipped=4):
    c, p = _setup(db)
    t = save_load_in(db, b.STAFF, _header(c), [LoadInLine(p.id, "B1", 10)])
    set_load_in_draft(db, b.STAFF, t.id, False)
    lo = b.transfer(db, c, load_out=True)
    b.item(db, lo, t.items[0].batch, shipped, main=t.items[0])
    return c, p, t


def test_cannot_remove_line_used_by_load_out(db):
    c, p, t = _completed_with_shipment(db)
    other = b.product(db, title="P2")
    with pytest.raises(BusinessError):
        save_load_in(db, b.STAFF, _header(c), [LoadInLine(other.id, "X", 1)], transfer_id=t.id)


def test_reducing_quantity_below_shipped_is_rejected(db):
    c, p, t = _completed_with_shipment(db, shipped=4)
    line = LoadInLine(p.id, "B1", 3, id=t.items[0].id)
    with pytest.raises(BusinessError):
        save_load_in(db, b.STAFF, _header(c), [line], transfer_id=t.id)


def test_used_line_cannot_change_batch_or_product(db):
    c, p, t = _completed_with_shipment(db)
    other = b.product(db, title="P2")
    for product_id, batch in ((p.id, "B2"), (other.id, "B1")):
        with pytest.raises(BusinessError):
            save_load_in(db, b.STAFF, _header(c), [LoadInLine(product_id, batch, 10, id=t.items[0].id)], transfer_id=t.id)
    db.rollback()


def test_used_load_in_cannot_change_client(db):
    c, p, t = _completed_with_shipment(db)
    other = b.client(db, name="C2")
    with pytest.raises(BusinessError) as e:
        save_load_in(db, b.STAFF, _header(other), [LoadInLine(p.id, "B1", 10, id=t.items[0].id)], transfer_id=t.id)
    assert e.value.field == "client_id"


def test_unused_load_in_can_change_client_and_batch(db):
    c, p = _setup(db)
    t = save_load_in(db, b.STAFF, _header(c), [LoadInLine(p.id, "B1", 10)])
    other = b.client(db, name="C2")
    t = save_load_in(db, b.STAFF, _header(other), [LoadInLine(p.id, "B2", 10, id=t.items[0].id)], transfer_id=t.id)
    assert t.client_id == other.id and t.items[0].batch.number == "B2"


def test_edit_keeps_line_and_recalculates(db):
    c, p, t = _completed_with_shipment(db, shipped=4)
    save_load_in(db, b.STAFF, _header(c), [LoadInLine(p.id, "B1", 6, id=t.items[0].id)], transfer_id=t.id)
    assert t.items[0].remaining_stock == 2


def test_delete_requires_admin_and_is_refused_when_used(db):
    c, p, t = _completed_with_shipment(db)
    with pytest.raises(PermissionDenied):
        delete_load_ins(db, b.STAFF, [t.id])
    with pytest.raises(BusinessError):
        delete_load_ins(db, b.ADMIN, [t.id])


def test_delete_removes_relation_links(db):
    c, p = _setup(db)
    t = save_load_in(db, b.STAFF, _header(c), [LoadInLine(p.id, "B1", 1)])
    lo = b.transfer(db, c, load_out=True, draft=True)
    db.add(TransferRelation(load_in=t, owner=lo))
    db.flush()
    delete_load_ins(db, b.ADMIN, [t.id])
    db.flush()
    assert db.scalar(select(TransferRelation)) is None
    assert db.scalar(select(TransferItem)) is None


def test_complete_and_revert_are_audited(db):
    c, p = _setup(db)
    t = save_load_in(db, b.STAFF, _header(c), [LoadInLine(p.id, "B1", 1)])
    set_load_in_draft(db, b.STAFF, t.id, False)
    set_load_in_draft(db, b.STAFF, t.id, True)
    actions = db.scalars(select(AuditLog.action).order_by(AuditLog.id)).all()
    assert actions == ["create", "complete", "revert"]
