import pytest
from sqlalchemy import select

from flyway.domain.errors import BusinessError, PermissionDenied
from flyway.models import TransferRelation
from flyway.services.load_in import LoadInHeader, LoadInLine, save_load_in, set_load_in_draft
from flyway.services.load_out import (
    LoadOutHeader, LoadOutLine, delete_load_outs, lines_for_all_remaining, save_load_out, set_load_out_draft,
)
from tests import builders as b


def _stocked(db, qty=100, default_unit=1, available="", client=None):
    c = client or b.client(db)
    p = b.product(db, default_unit=default_unit, available_unit=available)
    li = save_load_in(db, b.STAFF, LoadInHeader(client_id=c.id, code=f"LI-{p.id.hex[:6]}", date=b.WHEN),
                      [LoadInLine(p.id, "B1", qty)])
    set_load_in_draft(db, b.STAFF, li.id, False)
    return c, li, li.items[0]


def _header(c, code="LO-1"):
    return LoadOutHeader(client_id=c.id, code=code, date=b.WHEN, to_location="Port", to_rmp="R9")


def test_draft_does_not_deduct_and_complete_does(db):
    c, li, src = _stocked(db)
    lo = save_load_out(db, b.STAFF, _header(c), [LoadOutLine(src.id, 3, unit=10)])
    assert lo.is_draft and src.remaining_stock == 100
    set_load_out_draft(db, b.STAFF, lo.id, False)
    assert src.remaining_stock == 70
    set_load_out_draft(db, b.STAFF, lo.id, True)
    assert src.remaining_stock == 100


def test_quantity_over_remaining_rejected(db):
    c, li, src = _stocked(db, qty=10)
    with pytest.raises(BusinessError) as e:
        save_load_out(db, b.STAFF, _header(c), [LoadOutLine(src.id, 11)])
    assert e.value.field == "quantity"


def test_source_must_belong_to_client_and_be_completed(db):
    c, li, src = _stocked(db)
    other = b.client(db, "C2")
    with pytest.raises(BusinessError):
        save_load_out(db, b.STAFF, _header(other), [LoadOutLine(src.id, 1)])
    set_load_in_draft(db, b.STAFF, li.id, True)
    with pytest.raises(BusinessError):
        save_load_out(db, b.STAFF, _header(c), [LoadOutLine(src.id, 1)])


def test_completing_beyond_stock_fails(db):
    c, li, src = _stocked(db, qty=10)
    a = save_load_out(db, b.STAFF, _header(c, "A"), [LoadOutLine(src.id, 6)])
    b2 = save_load_out(db, b.STAFF, _header(c, "B"), [LoadOutLine(src.id, 6)])
    set_load_out_draft(db, b.STAFF, a.id, False)
    with pytest.raises(BusinessError):
        set_load_out_draft(db, b.STAFF, b2.id, False)


def test_editing_completed_load_out_to_new_source_restores_old(db):
    c, li1, src1 = _stocked(db)
    _, li2, src2 = _stocked(db, client=c)
    lo = save_load_out(db, b.STAFF, _header(c), [LoadOutLine(src1.id, 30)])
    set_load_out_draft(db, b.STAFF, lo.id, False)
    line_id = lo.items[0].id
    save_load_out(db, b.STAFF, _header(c), [LoadOutLine(src2.id, 30, id=line_id)], transfer_id=lo.id)
    assert (src1.remaining_stock, src2.remaining_stock) == (100, 70)


def test_editing_completed_load_out_may_reuse_its_own_quantity(db):
    c, li, src = _stocked(db, qty=10)
    lo = save_load_out(db, b.STAFF, _header(c), [LoadOutLine(src.id, 10)])
    set_load_out_draft(db, b.STAFF, lo.id, False)
    save_load_out(db, b.STAFF, _header(c), [LoadOutLine(src.id, 10, id=lo.items[0].id)], transfer_id=lo.id)
    assert src.remaining_stock == 0


def test_duplicate_source_rejected(db):
    c, li, src = _stocked(db)
    with pytest.raises(BusinessError):
        save_load_out(db, b.STAFF, _header(c), [LoadOutLine(src.id, 1), LoadOutLine(src.id, 2)])


def test_delete_requires_admin_and_restores_stock(db):
    c, li, src = _stocked(db)
    lo = save_load_out(db, b.STAFF, _header(c), [LoadOutLine(src.id, 30)])
    set_load_out_draft(db, b.STAFF, lo.id, False)
    with pytest.raises(PermissionDenied):
        delete_load_outs(db, b.STAFF, [lo.id])
    delete_load_outs(db, b.ADMIN, [lo.id])
    assert src.remaining_stock == 100


def test_lines_for_all_remaining_reports_remainder(db):
    c, li, src = _stocked(db, qty=25, default_unit=10, available="6,12")
    picks = lines_for_all_remaining(db, li.id)
    assert [(p.line.source_item_id, p.line.quantity, p.line.unit, p.remainder) for p in picks] == [(src.id, 4, 6, 1)]


def test_new_load_out_records_relations(db):
    c, li, src = _stocked(db)
    h = _header(c)
    h.load_in_ids = [li.id]
    lo = save_load_out(db, b.STAFF, h, [LoadOutLine(src.id, 1)])
    assert db.scalar(select(TransferRelation.transfer_id).where(TransferRelation.rel_transfer_id == lo.id)) == li.id


def test_related_load_in_ids_must_be_existing_load_ins(db):
    import uuid
    c, li, src = _stocked(db)
    other_lo = save_load_out(db, b.STAFF, _header(c, "LO-X"), [LoadOutLine(src.id, 1)])
    for n, bad in enumerate((uuid.uuid4(), other_lo.id)):
        h = _header(c, f"LO-Y{n}")
        h.load_in_ids = [bad]
        with pytest.raises(BusinessError) as e:
            save_load_out(db, b.STAFF, h, [LoadOutLine(src.id, 1)])
        assert e.value.field == "load_in_ids"
