"""M5 smoke test — terminal approve = production receipt + cost pool (FR-MFG-13).

Three things are proven here, the last two being the ones most likely to be silently wrong:

  A. A terminal approve creates a finished-goods layer costed at costPool / goodQty,
     posts a PRDR inbound ledger entry, and completes the run.
  B. Sum-of-ledger reconciliation: PRDR.value == sum(PRDI.value) - sum(PRDL.value) EXACTLY.
     This is what the `valueOverride` parameter added to PostLedgerAsync exists for — the
     6 dp unit cost, multiplied out over 100+ units, drifts past the ledger's 4 dp tick.
  C. A batch/serial-tracked finished item is refused rather than silently receiving
     untracked stock (docs/30 defines no batch creation on receipt).

Depends on the run m4_stage_actions.py leaves with its terminal stage InProgress:

    python Backend/smoke/m4_stage_actions.py
    python Backend/smoke/m5_receipt.py
"""

from __future__ import annotations

import json
import os
import sys

from smoke_common import bootstrap, drain_stock, seed_costed_stock

STATE_FILE = "m4_state.json"
WAREHOUSE_CODE = "SMOKE-PRD"
SCRAP = 2

# Part B — the rounding case. The drift only appears when costPool / goodQty needs more than
# 6 decimal places, so the numbers are chosen so it does:
#   consumed = 3.5 x 300 = 1050 base units at 1.234567  ->  pool = 1296.29535
#   unitCost = round(1296.29535 / 300, 6) = 4.320985  (from 4.3209845, away from zero)
#   naive qty x unitCost = 300 x 4.320985 = 1296.2955   != round(pool, 4) = 1296.2954
# Without valueOverride the ledger would carry 1296.2955 and stop reconciling to the pool.
BIG_TEMPLATE_CODE = "SMOKE-PT-M5B"
BIG_TARGET = 300
BIG_QTY_PER_BATCH = 3.5
BIG_UNIT_COST = 1.234567


def on_hand(c, item, wh):
    return float(c.get(f"/stock/on-hand?itemId={item}&warehouseId={wh}").body["onHand"])


def ledger_sum(c, run_id, source):
    rows = c.get(f"/stock/ledger?sourceDocType={source}&sourceDocId={run_id}&pageSize=200").body["items"]
    return sum(float(r["value"]) for r in rows), rows


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

    if not os.path.exists(STATE_FILE):
        sys.exit(f"FATAL: {STATE_FILE} missing — run m4_stage_actions.py first.")
    state = json.load(open(STATE_FILE))
    rid, wh = state["runId"], state["warehouseId"]
    asm_id, finished = state["assembleStageId"], state["finishedItemId"]

    detail = c.get(f"/production-runs/{rid}").body
    asm = next((s for s in detail["stages"] if s["runStageId"] == asm_id), None)
    if asm is None:
        sys.exit(f"FATAL: stage {asm_id} not on run {rid}.")
    if asm["status"] != "InProgress":
        sys.exit(f"FATAL: expected the terminal stage InProgress, found {asm['status']} — re-run m4.")

    pool_before = float(detail["costPool"]["net"])
    fin_before = on_hand(c, finished, wh)
    print(f"run={rid} costPool={pool_before} finishedOnHand={fin_before}")

    # ------------------------------------------------- complete the terminal
    chk.section("A1. Complete the terminal stage with scrap (FR-MFG-11)")
    out_id = asm["outputs"][0]["runOutputId"]
    scrap_reason = next(r["reasonCodeId"] for r in c.get("/reason-codes?context=Production&pageSize=50").body["items"]
                        if r["code"] == "PRD-SCRAP")

    done = c.post(f"/production-runs/{rid}/stages/{asm_id}/complete", {
        "outputs": [{"runOutputId": out_id, "producedQty": 50, "scrappedQty": SCRAP,
                     "scrapReasonCodeId": scrap_reason}],
    })
    chk.status("complete the terminal stage", done, 200)
    if done.status != 200:
        return chk.finish("M5")
    chk.check("stage Done", done.body["status"], "Done")
    chk.check("scrap recorded", float(done.body["outputs"][0]["scrappedQty"]), float(SCRAP))
    chk.check("scrap reason recorded", done.body["outputs"][0]["scrapReasonCodeId"], scrap_reason)

    # Scrap is absorbed into the pool, not written off (FR-MFG-11): no extra ledger row.
    chk.check("scrap posts no ledger entry",
              float(c.get(f"/production-runs/{rid}").body["costPool"]["net"]), pool_before)

    # ------------------------------------------------------------- the receipt
    chk.section("A2. Terminal approve posts the receipt and completes the run (FR-MFG-13)")
    good = 50 - SCRAP
    approved = c.post(f"/production-runs/{rid}/stages/{asm_id}/approve")
    chk.status("approve the terminal stage", approved, 200)
    if approved.status != 200:
        return chk.finish("M5")

    chk.check("stage Approved", approved.body["status"], "Approved")
    chk.check("run Completed", approved.body["runStatus"], "Completed")
    chk.check("no WIP transfers from a terminal stage", approved.body["transfers"], [])

    receipt = approved.body["receipt"]
    chk.check("receipt returned", receipt is not None, True)
    if receipt:
        chk.check("receipt is for the finished item", receipt["itemId"], finished)
        chk.check("received the good quantity (produced - scrapped)",
                  float(receipt["qtyReceived"]), float(good))
        chk.check("receipt warehouse is the run warehouse", receipt["warehouseId"], wh)
        chk.check("layer created", receipt["layerId"] > 0, True)

    pool = approved.body["costPool"]
    chk.check("cost pool reported", pool is not None, True)
    if pool and receipt:
        chk.check("pool net = consumed - returned",
                  round(float(pool["net"]), 4),
                  round(float(pool["consumed"]) - float(pool["returned"]), 4))
        chk.check("pool net matches the pool before approval", float(pool["net"]), pool_before)
        expected_unit = round(float(pool["net"]) / float(good), 6)
        chk.check("unitCost = costPool / goodQty", float(receipt["unitCost"]), expected_unit)
        chk.check("receipt value = the cost pool exactly",
                  float(receipt["value"]), round(float(pool["net"]), 4))

    chk.check("finished on-hand rose by the good quantity", on_hand(c, finished, wh), fin_before + good)

    completed = c.get(f"/production-runs/{rid}").body
    chk.check("completedAt stamped", completed["completedAt"] is not None, True)
    chk.check("run status persisted as Completed", completed["status"], "Completed")

    # ------------------------------------------------- ledger reconciliation
    chk.section("A3. Ledger reconciles to the cost pool")
    issued, prdi = ledger_sum(c, rid, "PRDI")
    returned, _ = ledger_sum(c, rid, "PRDL")
    received, prdr = ledger_sum(c, rid, "PRDR")
    chk.check("one PRDR row", len(prdr), 1)
    if prdr:
        chk.check("PRDR direction is In", prdr[0]["direction"], "In")
    chk.check("sum(PRDI) - sum(PRDL) == sum(PRDR)  [the core invariant]",
              round(issued - returned, 4), round(received, 4))
    print(f"        issued={issued} returned={returned} received={received}")

    # --------------------------------------------------- closed-run guards
    chk.section("A4. A completed run is closed to further action")
    chk.status("approve again", c.post(f"/production-runs/{rid}/stages/{asm_id}/approve"), 409)
    chk.status("start a stage on a completed run",
               c.post(f"/production-runs/{rid}/stages/{state['assembleStageId']}/start"), 409)
    chk.status("edit quantities on a completed run",
               c.put(f"/production-runs/{rid}/stages/{asm_id}/quantities", {"inputs": [], "outputs": []}), 409)

    # ------------------------------------ the rounding case (>= 100 units)
    chk.section("B. Rounding: 300 units, where a 6 dp unit cost drifts past the 4 dp ledger tick")
    raw = state["rawItemId"]
    raw_uom = next(i["baseUomId"] for i in c.get("/items?pageSize=200").body["items"]
                   if i["itemId"] == raw)
    finished_b = state["finishedItemId"]

    # A fresh single-stage template: a pack-counted input, so no content division muddies
    # the arithmetic; qtyPerBatch 3.5 so consumed is NOT a multiple of the target (which is what
    # forces pool / goodQty to repeat).
    payload_b = {
        "code": BIG_TEMPLATE_CODE, "name": "Smoke M5 rounding line",
        "stages": [{
            "key": "tmp-b", "name": "Mix", "estimatedMinutes": 5, "posX": 0, "posY": 0,
            "fieldDefs": [],
            "inputs": [{"source": "Stock", "itemId": raw,
                        "qtyPerBatch": BIG_QTY_PER_BATCH}],
            "outputs": [{"key": "tmp-bo", "name": "Mixed", "itemId": finished_b,
                         "qtyPerBatch": 1}],
        }],
        "edges": [],
    }
    existing_b = next((t for t in c.get(f"/production-templates?q={BIG_TEMPLATE_CODE}").body["items"]
                       if t["code"] == BIG_TEMPLATE_CODE), None)
    if existing_b:
        hb = c.get(f"/production-templates/{existing_b['templateId']}")
        rb = c.put(f"/production-templates/{existing_b['templateId']}", payload_b, if_match=hb.etag)
        tid = existing_b["templateId"] if rb.status in (200, 409) else None
    else:
        rb = c.post("/production-templates", payload_b)
        tid = rb.body["templateId"] if rb.status == 201 else None

    if tid is None:
        chk.check("create the rounding template", False, True)
    else:
        # Drain first, then seed at the exact unit cost the arithmetic above assumes. Without
        # the drain, FIFO would consume whatever earlier scripts left behind (at their costs)
        # and the pool would not match the figures this section reasons about — the assertions
        # would still "pass" while testing something else entirely.
        drain_stock(c, wh)
        seed_costed_stock(c, wh, [(raw, 5000, BIG_UNIT_COST)])

        big = c.post("/production-runs", {"templateId": tid, "targetQty": BIG_TARGET, "warehouseId": wh})
        if big.status != 201:
            chk.check(f"create the {BIG_TARGET}-unit run", big.status, 201)
        else:
            brid = big.body["runId"]
            bstage = big.body["stages"][0]
            bout = bstage["outputs"][0]["runOutputId"]

            s = c.post(f"/production-runs/{brid}/stages/{bstage['runStageId']}/start")
            chk.status("start", s, 200)
            d = c.post(f"/production-runs/{brid}/stages/{bstage['runStageId']}/complete",
                       {"outputs": [{"runOutputId": bout, "producedQty": BIG_TARGET, "scrappedQty": 0}]})
            chk.status("complete", d, 200)
            a = c.post(f"/production-runs/{brid}/stages/{bstage['runStageId']}/approve")
            chk.status("approve (single stage is the terminal)", a, 200)

            if a.status == 200:
                bpool = float(a.body["costPool"]["net"])
                brec = a.body["receipt"]
                unit = float(brec["unitCost"])
                naive = round(unit * BIG_TARGET, 4)
                chk.check("cost pool is non-zero", bpool > 0, True)
                chk.check("receipt value == cost pool exactly", float(brec["value"]), round(bpool, 4))
                print(f"        pool={bpool}  unitCost={unit}  naive qty*unit={naive}")
                # This is the assertion that justifies the valueOverride parameter existing.
                # If the naive product ever equals the pool, the fixture stopped exercising
                # the rounding path and the test has quietly gone blind.
                chk.check("naive qty x unitCost really would have drifted (fixture still valid)",
                          naive != round(bpool, 4), True)

                bissued, _ = ledger_sum(c, brid, "PRDI")
                breturned, _ = ledger_sum(c, brid, "PRDL")
                breceived, _ = ledger_sum(c, brid, "PRDR")
                chk.check("sum(PRDI) - sum(PRDL) == sum(PRDR) at 300 units",
                          round(bissued - breturned, 4), round(breceived, 4))

    # ------------------------------------------- tracked finished goods
    chk.section("C. A batch/serial-tracked finished item is refused")
    tracked = next((i for i in c.get("/items?pageSize=200&status=Active").body["items"]
                    if i.get("trackingMode") in ("Batch", "Serial")), None)
    if tracked is None:
        chk.check("skipped: no batch/serial-tracked item in the database", True, True)
        print("        NOTE: the TrackingMode guard in PostReceiptAsync is unexercised here.")
    else:
        print(f"        using tracked item {tracked['itemId']} ({tracked['trackingMode']})")
        payload = {
            "code": "SMOKE-PT-TRACKED", "name": "Tracked finished good",
            "stages": [{
                "key": "tmp-one", "name": "Make", "estimatedMinutes": 1, "posX": 0, "posY": 0,
                "fieldDefs": [], "inputs": [],
                "outputs": [{"key": "tmp-o", "name": "Tracked", "itemId": tracked["itemId"],
                             "qtyPerBatch": 1}],
            }],
            "edges": [],
        }
        made = c.post("/production-templates", payload)
        ttid = made.body["templateId"] if made.status == 201 else next(
            (t["templateId"] for t in c.get("/production-templates?q=SMOKE-PT-TRACKED").body["items"]
             if t["code"] == "SMOKE-PT-TRACKED"), None)
        if ttid:
            tr = c.post("/production-runs", {"templateId": ttid, "targetQty": 1, "warehouseId": wh})
            if tr.status == 201:
                st = tr.body["stages"][0]["runStageId"]
                to = tr.body["stages"][0]["outputs"][0]["runOutputId"]
                c.post(f"/production-runs/{tr.body['runId']}/stages/{st}/start")
                c.post(f"/production-runs/{tr.body['runId']}/stages/{st}/complete",
                       {"outputs": [{"runOutputId": to, "producedQty": 1, "scrappedQty": 0}]})
                chk.status("approve a tracked finished good",
                           c.post(f"/production-runs/{tr.body['runId']}/stages/{st}/approve"), 422)

    return chk.finish("M5")


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