"""Tests on Lab 13's crash test (06_crash_resume.py), shrunk: Dana's and Grace's refunds and Dana's cancelled
lantern order, each killed right after the shop committed the write, then started again. Plus the random-kill
arithmetic from the recorded run (210 kills, a 0.132 ms gap). Run: pytest test_durable_practice.py"""
import uuid

from durable_practice import Killed, Shop, answer, expected_gap_hits, kill_and_resume, run, step_key


def task(email, order_id, write):
    """Dana's five tools, in the recorded order: four reads, then the write."""
    return [
        ("find_customer", {"email": email}),
        ("list_orders", {"customer": email}),
        ("get_order", {"order_id": order_id}),
        ("get_refunds", {"order_id": order_id}),
        write,
    ]


# Dana ordered two sleeping pads at $79.50 and one arrived torn; Grace's $68 trekking poles never arrived.
DANA_ORDERS = {"A-1001": {"customer": "dana", "price": 79.5, "qty": 2, "status": "delivered"}}
DANA = task("dana", "A-1001", ("issue_refund", {"order_id": "A-1001", "amount": 79.5}))
GRACE_ORDERS = {"A-1010": {"customer": "grace", "price": 68.0, "qty": 1, "status": "delivered"}}
GRACE = task("grace", "A-1010", ("issue_refund", {"order_id": "A-1010", "amount": 68.0}))
LANTERN_ORDERS = {"A-1020": {"customer": "dana", "price": 24.0, "qty": 1, "status": "processing"}}
LANTERN = task("dana", "A-1020", ("cancel_order", {"order_id": "A-1020"}))
REFUND = 4  # the write is step 4 (counting from 0)


def never_killed(steps, orders):
    shop = Shop(orders)
    return shop, run(steps, shop, {}, "T07")


def test_step_key_stays_the_same_on_a_repeat():
    assert step_key("T07", 4) == "T07:4"
    assert step_key("T07", 4) == step_key("T07", 4) != step_key("T07", 5) != step_key("T06", 5)


def test_shop_runs_a_key_once():
    shop = Shop(DANA_ORDERS)
    first = shop.call("issue_refund", {"order_id": "A-1001", "amount": 79.5}, key="T07:4")
    again = shop.call("issue_refund", {"order_id": "A-1001", "amount": 79.5}, key="T07:4")
    assert first == again == {"refunded": 79.5}
    assert shop.refunds == [("A-1001", 79.5)]
    shop.call("issue_refund", {"order_id": "A-1001", "amount": 79.5})  # no key: a new refund, within the limit
    assert shop.refunds == [("A-1001", 79.5), ("A-1001", 79.5)]
    assert "error" in shop.call("issue_refund", {"order_id": "A-1001", "amount": 79.5})  # nothing left to refund


def test_kill_lands_after_the_commit():
    shop, journal = Shop(DANA_ORDERS), {}
    try:
        run(DANA, shop, journal, "T07", kill_after_commit=REFUND)
        raise AssertionError("expected the agent to be killed")
    except Killed:
        pass
    assert shop.refunds == [("A-1001", 79.5)]  # the shop has the refund...
    assert len(journal["results"]) == 4        # ...and the journal doesn't


def test_start_over_redoes_every_step():
    shop, results = kill_and_resume(DANA, DANA_ORDERS, journal_on=False, keys=False, kill_at=REFUND)
    assert shop.refunds == [("A-1001", 79.5), ("A-1001", 79.5)]
    assert shop.calls == 10  # all five tools, twice


def test_journal_still_refunds_dana_twice():
    shop, results = kill_and_resume(DANA, DANA_ORDERS, journal_on=True, keys=False, kill_at=REFUND)
    assert shop.calls == 6  # the four reads came from the journal; the refund was sent again
    assert shop.refunds == [("A-1001", 79.5), ("A-1001", 79.5)]  # two pads, so the second was within the limit
    assert answer(results) == "Done: refunded $79.50"  # and she's told she got one


def test_refused_repeat_changes_graces_answer():
    _, ref = never_killed(GRACE, GRACE_ORDERS)
    assert answer(ref) == "Done: refunded $68.00"
    shop, results = kill_and_resume(GRACE, GRACE_ORDERS, journal_on=True, keys=False, kill_at=REFUND, run_id="T06")
    assert shop.refunds == [("A-1010", 68.0)]  # refunded once...
    assert answer(results) == "The system blocked it, so I've passed this to a person."  # ...and told it was blocked
    shop, results = kill_and_resume(LANTERN, LANTERN_ORDERS, journal_on=True, keys=False, kill_at=REFUND, run_id="T16")
    assert "error" in results[REFUND]  # cancelling a cancelled order is refused


def test_keys_make_the_repeat_harmless():
    for steps, orders, rid in ((DANA, DANA_ORDERS, "T07"), (GRACE, GRACE_ORDERS, "T06"), (LANTERN, LANTERN_ORDERS, "T16")):
        ref_shop, ref = never_killed(steps, orders)
        shop, results = kill_and_resume(steps, orders, journal_on=True, keys=True, kill_at=REFUND, run_id=rid)
        assert results == ref and answer(results) == answer(ref)
        assert shop.refunds == ref_shop.refunds and shop.orders == ref_shop.orders
    shop, _ = kill_and_resume(DANA, DANA_ORDERS, journal_on=True, keys=True, kill_at=REFUND)
    assert list(shop.keys) == ["T07:4"]  # only the write carries a key


def test_a_random_key_protects_nothing():
    fresh = lambda run_id, step: str(uuid.uuid4())  # a new ID on every attempt looks like a new refund
    shop, _ = kill_and_resume(DANA, DANA_ORDERS, journal_on=True, keys=True, kill_at=REFUND, key_fn=fresh)
    assert shop.refunds == [("A-1001", 79.5), ("A-1001", 79.5)]


# The 14 recorded tasks: (writes committed, seconds for a replayed run that was never killed). 15 random kills each.
RUNS = [(1, 2.171), (1, 2.014), (1, 2.068), (1, 2.138), (0, 2.007), (1, 2.064), (1, 2.047),
        (1, 2.034), (1, 1.543), (2, 2.332), (2, 2.23), (1, 1.889), (2, 2.263), (0, 1.829)]


def test_random_kills_almost_never_hit_the_gap():
    hits = expected_gap_hits(RUNS, gap_ms=0.132, kills_per_run=15)
    assert round(hits, 3) == 0.016
    assert round(210 / hits, -3) == 13000  # about one hit in 13,000 kills
    assert round(expected_gap_hits([(1, 2.0)], gap_ms=100, kills_per_run=1), 3) == 0.056
