"""M4c smoke test — production stage inputs expressed in content units (FR-MFG-04).

Replaces m4b_uom_conversion.py, which tested the per-item UOM conversion table that no longer
exists. Stock is now always a count of the item's base UOM (a bottle, a packet), and the only
place another unit appears is a production formula: a stage input may be written as an amount
of the item's *content* (ml or g), which the server divides by the item's content size to get
the pack count FIFO actually consumes.

What this pins down, none of which any other script covers:

  * 2000 ml against a 500 ml bottle consumes exactly 4.0000 bottles, and the ledger agrees;
  * 300 ml against the same item consumes 0.6000 — **fractional packs are legal**, which is
    the whole reason the quantity columns are (18,4);
  * a Content input on an item with no content size is refused at template save (422), not
    silently treated as packs at stage start.

Self-contained: creates its own item, warehouse, template and runs.

    python Backend/smoke/m4c_content_units.py
"""

from __future__ import annotations

import sys

from smoke_common import bootstrap, drain_stock, seed_costed_stock

WAREHOUSE_CODE = "SMOKE-CONTENT"
TEMPLATE_CODE = "SMOKE-PT-M4C"
SKU = "SMOKE-BOTTLE-500ML"
PLAIN_SKU = "SMOKE-NO-CONTENT"

CONTENT_QTY = 500          # ml held by one bottle
SEED_BOTTLES = 100
UNIT_COST = 3.0


def production_reason(c, code):
    for r in c.get("/reason-codes?context=Production&pageSize=50").body["items"]:
        if r["code"] == code:
            return r["reasonCodeId"]
    sys.exit(f"FATAL: Production reason {code} not seeded.")


def ensure_warehouse(c):
    for w in c.get(f"/warehouses?q={WAREHOUSE_CODE}&pageSize=50").body["items"]:
        if w["code"] == WAREHOUSE_CODE:
            return w["warehouseId"]
    created = c.post("/warehouses", {"code": WAREHOUSE_CODE, "name": "Content-unit smoke warehouse"})
    if created.status != 201:
        sys.exit(f"FATAL: could not create the smoke warehouse: {created.status} {created.body}")
    return created.body["warehouseId"]


def ensure_item(c, sku, name, content_qty, content_unit):
    """Find-or-create; content fields are only sent when the item is meant to have them."""
    for i in c.get(f"/items?q={sku}&pageSize=50").body["items"]:
        if i["sku"] == sku:
            return i

    category = c.get("/categories?pageSize=1").body["items"]
    uoms = c.get("/uoms?pageSize=1").body["items"]
    if not category or not uoms:
        sys.exit("FATAL: need at least one category and one UOM seeded.")

    body = {
        "sku": sku, "name": name,
        "categoryId": category[0]["categoryId"],
        "baseUomId": uoms[0]["uomId"],
        "stockNature": "Stocked", "trackingMode": "None",
    }
    if content_qty is not None:
        body["contentQty"] = content_qty
        body["contentUnit"] = content_unit

    created = c.post("/items", body)
    if created.status != 201:
        sys.exit(f"FATAL: could not create item {sku}: {created.status} {created.body}")
    return created.body


def save_template(c, raw_item, finished_item, qty_per_batch, qty_unit, suffix):
    """
    One template per section, never a shared one.

    Each section starts a run and leaves it InProgress, and a template with a live run is
    edit-locked (FR-MFG-06, 409 TEMPLATE_IN_USE) — so re-saving a single shared code would
    fail from the second section onward for reasons that have nothing to do with content units.
    """
    code = f"{TEMPLATE_CODE}-{suffix}"
    payload = {
        "code": code, "name": f"Content-unit smoke line ({suffix})",
        "stages": [{
            "key": "tmp-mix", "name": "Mix", "estimatedMinutes": 5, "posX": 0, "posY": 0,
            "fieldDefs": [],
            "inputs": [{"source": "Stock", "itemId": raw_item,
                        "qtyUnit": qty_unit, "qtyPerBatch": qty_per_batch}],
            "outputs": [{"key": "tmp-out", "name": "Mixed", "itemId": finished_item,
                         "qtyPerBatch": 1}],
        }],
        "edges": [],
    }
    existing = next((t for t in c.get(f"/production-templates?q={code}").body["items"]
                     if t["code"] == code), None)
    if existing:
        head = c.get(f"/production-templates/{existing['templateId']}")
        return c.put(f"/production-templates/{existing['templateId']}", payload, if_match=head.etag)
    return c.post("/production-templates", payload)


def saved_ok(chk, label, response):
    """Template save is find-or-create, so a POST gives 201 and a PUT gives 200."""
    ok = chk.check(f"{label} -> 200/201", response.status in (200, 201), True)
    if not ok:
        print(f"          server said: {response.body}")
    return ok


def consume_once(c, chk, tid, wh, target_qty, label, expected_packs):
    """
    Create a run, start its only stage, and assert what FIFO actually took.

    `expected_packs` is compared against the RAW formula quantity resolved to packs, which is
    only valid while the run scale factor is 1. Run creation computes
    `ratio = targetQty / terminalOutput.qtyPerBatch` and pre-scales every planned quantity, so
    this holds because `save_template` pins the terminal output to `qtyPerBatch: 1` and every
    caller here passes `target_qty=1`. Change either and these numbers move by that ratio.
    """
    created = c.post("/production-runs", {"templateId": tid, "targetQty": target_qty, "warehouseId": wh})
    if not chk.status(f"{label}: create the run", created, 201):
        return
    run = created.body
    stage_id = run["stages"][0]["runStageId"]

    started = c.post(f"/production-runs/{run['runId']}/stages/{stage_id}/start",
                     idempotency_key=f"m4c-{run['runId']}")
    if not chk.status(f"{label}: POST .../start", started, 200):
        return

    consumed = started.body["consumed"]
    if not chk.check(f"{label}: exactly one input consumed", len(consumed), 1):
        return
    chk.check(f"{label}: consumed {expected_packs} base units",
              round(float(consumed[0]["qty"]), 4), expected_packs)

    rows = c.get(f"/stock/ledger?sourceDocType=PRDI&sourceDocId={run['runId']}&pageSize=50").body["items"]
    chk.check(f"{label}: one PRDI ledger row", len(rows), 1)
    if rows:
        chk.check(f"{label}: ledger qtyBase matches the consumption",
                  round(abs(float(rows[0]["qtyBase"])), 4), expected_packs)

    # Cancel so the script is re-runnable. A run left InProgress edit-locks its template
    # (FR-MFG-06), so the next execution could not re-save it and would fail with a 409 that
    # says nothing about content units. Cancelling also returns the consumed stock (FR-MFG-17),
    # which keeps the seeded on-hand stable across runs.
    c.post(f"/production-runs/{run['runId']}/cancel",
           {"reasonCodeId": production_reason(c, "PRD-CANCEL"), "note": "m4c cleanup"})


def cancel_stale_runs(c):
    """
    Cancel any InProgress run this script left behind previously.

    Self-healing rather than merely tidy: a live run edit-locks its template, so without this
    a re-run (or an earlier interrupted run) fails at template save with 409 TEMPLATE_IN_USE —
    a failure that looks like a content-unit bug and is not one.
    """
    ours = {t["templateId"] for t in c.get(f"/production-templates?q={TEMPLATE_CODE}&pageSize=50").body["items"]
            if t["code"].startswith(TEMPLATE_CODE)}
    if not ours:
        return
    stale = [r for r in c.get("/production-runs?status=InProgress&pageSize=200").body["items"]
             if r["templateId"] in ours]
    for r in stale:
        c.post(f"/production-runs/{r['runId']}/cancel",
               {"reasonCodeId": production_reason(c, "PRD-CANCEL"), "note": "m4c stale cleanup"})
    if stale:
        print(f"cancelled {len(stale)} stale run(s) from a previous execution")


def main():
    c, chk, args = bootstrap(__doc__)
    print(f"API {args.api}")
    cancel_stale_runs(c)

    bottle = ensure_item(c, SKU, "Smoke syrup 500ml bottle", CONTENT_QTY, "Ml")
    plain = ensure_item(c, PLAIN_SKU, "Smoke item with no content", None, None)
    finished = c.get("/items?pageSize=5&status=Active").body["items"][0]
    print(f"bottle={bottle['itemId']} contentBaseQty={bottle.get('contentBaseQty')} "
          f"plain={plain['itemId']} finished={finished['itemId']}")

    chk.section("0. Content size normalises to a base unit on the item")
    chk.check("contentBaseQty is the entered ml", float(bottle["contentBaseQty"]), float(CONTENT_QTY))
    chk.check("contentBaseUnit is Ml", bottle["contentBaseUnit"], "Ml")

    wh = ensure_warehouse(c)
    drain_stock(c, wh)
    seed_costed_stock(c, wh, [(bottle["itemId"], SEED_BOTTLES, UNIT_COST)])

    # ------------------------------------------------- whole packs out of content units
    chk.section("1. A content quantity resolves to whole packs (2000 ml / 500 ml = 4)")
    saved = save_template(c, bottle["itemId"], finished["itemId"], 2000, "Content", "whole")
    if saved_ok(chk, "save the Content template", saved):
        consume_once(c, chk, saved.body["templateId"], wh, 1, "2000 ml", 4.0)

    # --------------------------------------------------------------- fractional packs
    chk.section("2. A content quantity below one pack consumes a FRACTION of one (300 ml = 0.6)")
    saved = save_template(c, bottle["itemId"], finished["itemId"], 300, "Content", "frac")
    if saved_ok(chk, "save the fractional Content template", saved):
        consume_once(c, chk, saved.body["templateId"], wh, 1, "300 ml", 0.6)

    # ------------------------------------------------------------------ the guard rail
    chk.section("3. Content units are refused on an item that has no content size")
    refused = save_template(c, plain["itemId"], finished["itemId"], 100, "Content", "nocontent")
    chk.status("Content input on a contentless item", refused, 422)

    chk.section("4. The same item still works when the formula is written in packs")
    saved = save_template(c, bottle["itemId"], finished["itemId"], 3, "Pack", "pack")
    if saved_ok(chk, "save the Pack template", saved):
        consume_once(c, chk, saved.body["templateId"], wh, 1, "3 bottles", 3.0)

    return chk.finish("M4c")


if __name__ == "__main__":
    sys.exit(main())
