"""Tests on Lab 12's scripts 03 and 04, from the recorded runs behind the video (gpt-5.4-nano, 2 November 2026,
each caching test run three times; one batch of 20 tickets, also sent twice as normal calls).
Run: pytest test_cache_practice.py"""
import random

from cache_practice import (agreement, batch_cost, call_cost, can_cache, hits_after_first, match_by_id,
                            missing_ids, reusable_start)

# cached_tokens for each call: three runs of five calls per test
SAME_START = [[0, 1792, 0, 1792, 1792], [0, 1792, 1792, 1792, 1792], [0, 1792, 1792, 1792, 1792]]
TIME_AT_START = [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]
TIME_AT_END = [[0, 0, 1792, 1792, 1792], [0, 0, 1792, 1792, 1792], [0, 1792, 1792, 1792, 1792]]
SHORT = [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]

# Run 2 of the same-start test: (input tokens, cached tokens, output tokens) per call
RUN_2 = [(2859, 0, 60), (2862, 1792, 60), (2859, 1792, 60), (2858, 1792, 60), (2859, 1792, 60)]

# The batch's categories for the 20 tickets, and the two rounds of normal calls, in ticket order
BATCH = ['bug', 'billing', 'feature_request', 'security', 'account', 'bug', 'account', 'billing', 'feature_request', 'bug', 'bug', 'billing', 'account', 'feature_request', 'security', 'bug', 'billing', 'feature_request', 'bug', 'security']
NORMAL_1 = ['bug', 'billing', 'feature_request', 'security', 'account', 'bug', 'account', 'billing', 'bug', 'bug', 'bug', 'billing', 'account', 'feature_request', 'security', 'bug', 'billing', 'feature_request', 'account', 'security']
NORMAL_2 = ['bug', 'billing', 'feature_request', 'security', 'account', 'bug', 'account', 'billing', 'bug', 'bug', 'bug', 'feature_request', 'account', 'feature_request', 'security', 'bug', 'billing', 'feature_request', 'bug', 'security']


def per_1000_calls(dollars):
    return round(dollars * 1000 * 100, 1)  # cents


def test_a_cached_call_costs_about_half():
    first = call_cost(RUN_2[0][0], RUN_2[0][1], RUN_2[0][2])
    third = call_cost(RUN_2[2][0], RUN_2[2][1], RUN_2[2][2])
    assert per_1000_calls(first) == 64.7
    assert per_1000_calls(third) == 32.4
    assert 0.49 <= 1 - third / first <= 0.51


def test_only_the_cached_part_is_cheap():
    # 1,792 cached tokens: 3.6 cents per 1,000 calls; the other 1,067 input: 21.3; the 60-token answer: 7.5
    assert per_1000_calls(call_cost(1792, 1792, 0)) == 3.6
    assert per_1000_calls(call_cost(1067, 0, 0)) == 21.3
    assert per_1000_calls(call_cost(0, 0, 60)) == 7.5


def test_every_hit_was_1792_tokens():
    hits = {c for run in SAME_START + TIME_AT_END for c in run if c}
    assert hits == {1792}
    assert all(run[0] == 0 for run in SAME_START + TIME_AT_START + TIME_AT_END + SHORT)


def test_same_start_hit_11_of_12():
    assert hits_after_first(SAME_START) == (11, 12)


def test_a_timestamp_at_the_start_breaks_it_at_the_end_it_doesnt():
    assert hits_after_first(TIME_AT_START) == (0, 12)
    assert hits_after_first(TIME_AT_END) == (10, 12)


def test_short_prompts_never_hit():
    assert hits_after_first(SHORT) == (0, 12)
    assert not can_cache(60)
    assert can_cache(2860)
    assert can_cache(1024) and not can_cache(1023)


MANUAL = "Ledgerly support manual. " * 40


def test_reuse_stops_at_the_first_difference():
    a = MANUAL + "How do I reset my password?"
    b = MANUAL + "How do I export my data?"
    assert reusable_start(a, b) == len(MANUAL) + len("How do I ")
    assert reusable_start(a, a) == len(a)


def test_a_changing_line_at_the_top_leaves_nothing_to_reuse():
    a = "Current time: 2026-11-02T09:16:44.617319\n" + MANUAL
    b = "Current time: 2026-11-02T09:16:46.902113\n" + MANUAL
    assert reusable_start(a, b) == len("Current time: 2026-11-02T09:16:4")
    end_a = MANUAL + "Current time: 2026-11-02T09:16:44.617319"
    end_b = MANUAL + "Current time: 2026-11-02T09:16:46.902113"
    assert reusable_start(end_a, end_b) >= len(MANUAL)


def results(order):
    return [{"custom_id": f"ticket-{i:03d}", "answer": BATCH[i]} for i in order]


def test_match_by_custom_id_whatever_the_order():
    order = list(range(20))
    random.Random(54).shuffle(order)
    by_id = match_by_id(results(order))
    assert [by_id[f"ticket-{i:03d}"] for i in range(20)] == BATCH
    # Matching by position would have given most tickets someone else's answer
    by_position = [r["answer"] for r in results(order)]
    assert by_position != BATCH


def test_find_the_requests_to_resubmit():
    sent = [f"ticket-{i:03d}" for i in range(20)]
    assert missing_ids(sent, results(range(20))) == []
    got = [r for r in results(range(20)) if r["custom_id"] not in ("ticket-003", "ticket-017")]
    assert missing_ids(sent, got[::-1]) == ["ticket-003", "ticket-017"]


def test_the_batch_costs_half():
    # The batch's own tokens: 1,039 in, 254 out. 2.6 cents per 1,000 tickets at list price, 1.3 as a batch.
    listed = call_cost(1039, 0, 254)
    assert round(batch_cost(1039, 254), 8) == round(listed / 2, 8)
    assert round(listed / 20 * 1000 * 100, 1) == 2.6
    assert round(batch_cost(1039, 254) / 20 * 1000 * 100, 1) == 1.3


def test_batch_answers_vary_as_much_as_normal_ones():
    assert agreement(BATCH, NORMAL_1) == 18
    assert agreement(BATCH, NORMAL_2) == 18
    assert agreement(NORMAL_1, NORMAL_2) == 18
