"""Original fictional inventory exercise. Python standard library only.
Run: python inventory-case.py
Orders placed after day d arrive before demand on day d + lead_days.
Unfilled demand is lost, not backordered. Pending orders count in position.
"""
from __future__ import annotations
from dataclasses import dataclass
import json
import unittest

DEMAND = [12, 18, 15, 21, 14, 20, 10]

@dataclass(frozen=True)
class Day:
    day: int
    demand: int
    receipts: int
    fulfilled: int
    lost: int
    ending: int
    ordered: int
    pending_after_order: int

def simulate(demand: list[int], threshold: int, initial: int = 40,
             quantity: int = 60, lead_days: int = 3) -> tuple[list[Day], list[tuple[int, int]]]:
    for name, value in [('threshold', threshold), ('initial', initial),
                        ('quantity', quantity), ('lead_days', lead_days)]:
        if type(value) is not int or value < 0:
            raise ValueError(f'{name} must be a nonnegative integer')
    if quantity == 0 or lead_days == 0:
        raise ValueError('quantity and lead_days must be positive')
    if any(type(value) is not int or value < 0 for value in demand):
        raise ValueError('demand must contain nonnegative integers')
    on_hand, pipeline, rows = initial, [], []
    for day, requested in enumerate(demand, 1):
        receipts = sum(q for due, q in pipeline if due == day)
        pipeline = [(due, q) for due, q in pipeline if due > day]
        on_hand += receipts
        fulfilled = min(on_hand, requested)
        on_hand -= fulfilled
        ordered = 0
        if on_hand + sum(q for _, q in pipeline) <= threshold:
            ordered = quantity
            pipeline.append((day + lead_days, quantity))
        rows.append(Day(day, requested, receipts, fulfilled, requested - fulfilled,
                        on_hand, ordered, sum(q for _, q in pipeline)))
    return rows, pipeline

def summary(threshold: int) -> dict:
    rows, pipeline = simulate(DEMAND, threshold)
    filled = sum(r.fulfilled for r in rows)
    return {'threshold': threshold, 'demand': sum(DEMAND), 'fulfilled': filled,
            'lost': sum(r.lost for r in rows), 'receipts': sum(r.receipts for r in rows),
            'ending': rows[-1].ending, 'pending': sum(q for _, q in pipeline),
            'orders': sum(r.ordered > 0 for r in rows),
            'unit_fill_rate': filled / sum(DEMAND)}

class InventoryChecks(unittest.TestCase):
    def test_baseline(self):
        result = summary(25)
        self.assertEqual([result[k] for k in ['fulfilled','lost','receipts','ending','pending','orders']],
                         [84,26,60,16,60,2])
    def test_earlier_order(self):
        result = summary(40)
        self.assertEqual([result[k] for k in ['fulfilled','lost','receipts','ending','pending','orders']],
                         [105,5,120,55,0,2])
    def test_conservation(self):
        for threshold in [0,25,40,100]:
            rows, _ = simulate(DEMAND, threshold)
            self.assertEqual(40 + sum(r.receipts for r in rows) - sum(r.fulfilled for r in rows), rows[-1].ending)
            for r in rows:
                self.assertEqual(r.fulfilled+r.lost, r.demand)
                self.assertGreaterEqual(r.ending, 0)
    def test_arrival_order(self):
        rows, pending = simulate(DEMAND,25)
        self.assertEqual([r.day for r in rows if r.receipts], [5])
        self.assertEqual(pending, [(10,60)])
    def test_in_transit_prevents_repeat_order(self):
        rows, _ = simulate(DEMAND,25)
        self.assertEqual([r.day for r in rows if r.ordered], [2,7])
    def test_empty_demand(self):
        self.assertEqual(simulate([],25), ([],[]))
    def test_invalid_inputs(self):
        for demand in [[-1],[True],[1.5]]:
            with self.assertRaises(ValueError): simulate(demand,25)
        with self.assertRaises(ValueError): simulate(DEMAND,25,lead_days=0)

if __name__ == '__main__':
    print(json.dumps([summary(25),summary(40)], indent=2))
    unittest.main()
