"""Tests on Lab 15's script 01 (recorded run, October 2026): made-up support messages with planted
personal data, and what gpt-5.4-mini found in each. Eight are the 60 "written" messages where the
model left personal data in the log; four are fill-in-the-blank messages with look-alikes.
Run: pytest test_redact_practice.py"""
from redact_practice import combine, covered, detect, luhn_ok, merge, redact, ssn_ok, still_unsafe

# id: (text, planted values [(type, start, end)], the model's spans [(type, start, end)])
MESSAGES = {
    "llm-007": (
        "Hi, I need help with an order issue and I’m hoping you can check it for me. The order number is 4805-3137-1544-1511, and the account name is Wei Schulz. I also want to verify the billing information tied to 583-36-2247 because something looks off.",
        [("CARD", 96, 115), ("PERSON", 141, 151), ("SSN", 207, 218)],
        [("PERSON", 141, 151), ("SSN", 207, 218)],
    ),
    "llm-000": (
        "Hi, I’m reaching out because I need help with an order issue and I can’t get the status to update on my account. The details for my ticket are 4829-3001-7154-5770 and daniel.tanaka@example.com. For verification, my reference number is 477-40-6854.",
        [("CARD", 143, 162), ("EMAIL", 167, 192), ("SSN", 235, 246)],
        [("EMAIL", 167, 192), ("SSN", 235, 246)],
    ),
    "llm-004": (
        "Hi, I’m having trouble with my order and need someone to look into it. The shipping address should be 382 Harbor St, Madison, WI 53703, and the order/reference number is 124-33-9922. Also, I used card 4671 6258 2820 0828 for the purchase, but I’m not seeing the charge correctly reflected on my account.",
        [("SSN", 170, 181), ("ADDRESS", 102, 134), ("CARD", 201, 220)],
        [("ADDRESS", 102, 134), ("CARD", 201, 220)],
    ),
    "llm-030": (
        "Hi support, I’m having trouble with my account and need help updating my contact details. My order reference is 108-70-4032, and you can reach me at tom.mensah@example.com. Also, the payment card I used was 4680 9596 1860 6319.",
        [("SSN", 112, 123), ("EMAIL", 149, 171), ("CARD", 207, 226)],
        [("EMAIL", 149, 171), ("CARD", 207, 226)],
    ),
    "llm-035": (
        "Hi support, I’m following up on an issue with my recent order and need help checking the payment details on file. For reference, my account number is 367-19-8779, and the transfer details I was given were DE20 7886 2349 5256 0205 16 and 4478 3327 7153 9329. Please let me know what the next step is and whether you need anything else from me.",
        [("SSN", 150, 161), ("IBAN", 205, 232), ("CARD", 237, 256)],
        [("SSN", 150, 161), ("IBAN", 205, 232)],
    ),
    "llm-040": (
        "Hi, I’m having trouble with my account and need someone to look into a billing issue. My reference number is 204-30-1046, and you can reach me at james.mensah@example.org. The payment method on file was 4904 6606 4625 8582, and I want to make sure everything is correct.",
        [("SSN", 109, 120), ("EMAIL", 146, 170), ("CARD", 203, 222)],
        [("EMAIL", 146, 170), ("CARD", 203, 222)],
    ),
    "llm-041": (
        "Hi, I’m having trouble with my recent order and need help updating my shipping details. The card I used ends in DE51 7623 2033 2642 7942 06, and the delivery address should be 689 Lake Drive, Springfield, IL 62704. For verification, my reference number is 230-22-5664.",
        [("IBAN", 112, 139), ("ADDRESS", 176, 213), ("SSN", 256, 267)],
        [("CARD", 112, 139), ("ADDRESS", 176, 213)],
    ),
    "llm-056": (
        "Hi, I’m having trouble with my account and need someone to look into it. My reference number is 4528835919835714, my phone number is 296-54-5016, and I noticed the login attempts came from 150.231.65.58. Please let me know what the next steps are.",
        [("CARD", 96, 112), ("SSN", 133, 144), ("IP", 189, 202)],
        [("PHONE", 133, 144), ("IP", 189, 202)],
    ),
    "tpl-001": (
        "Hello! Ivan Moreau here. Reach me at (713) 555-1077 or ivan.moreau@mail.test. Invoice 2953502819 lists the wrong price: $650.43, not what I agreed.",
        [("PERSON", 7, 18), ("PHONE", 37, 51), ("EMAIL", 55, 76)],
        [("PERSON", 7, 18), ("PHONE", 37, 51), ("EMAIL", 55, 76)],
    ),
    "tpl-111": (
        "Hi, this is James Petrov. I was charged twice on card 4157870490541967 for order 4077001400188961. Please update my email to james.petrov@mail.test and my phone to +1 748 555 1214. The app crashed on version 1.5.0.79 after I paid $1498.13.",
        [("PERSON", 12, 24), ("CARD", 54, 70), ("EMAIL", 125, 147), ("PHONE", 164, 179)],
        [("PERSON", 12, 24), ("CARD", 54, 70), ("EMAIL", 125, 147), ("PHONE", 164, 179)],
    ),
    "tpl-004": (
        "The app crashed on version 2.16.0.18 after I paid $1203.56. I can't log in from 102.78.100.138. Ticket INC-10978.",
        [("IP", 80, 94)],
        [("IP", 80, 94)],
    ),
    "tpl-000": (
        "Order 8639767179579576 still hasn't arrived. Total was $1787.56.",
        [],
        [],
    ),
}
WRITTEN = [k for k in MESSAGES if k.startswith("llm-")]


def spans(text, triples):
    return [{"type": t, "start": s, "end": e, "text": text[s:e]} for t, s, e in triples]


def gold(k):
    text, g, _ = MESSAGES[k]
    return spans(text, g)


def model(k):
    text, _, m = MESSAGES[k]
    return spans(text, m)


def test_luhn_the_cold_open_card():
    # The card the message called an "order number": its total is 70.
    assert luhn_ok("4805313715441511")
    assert luhn_ok("4157870490541967")


def test_luhn_rejects_one_changed_digit_and_order_numbers():
    assert not luhn_ok("4805313715441512")  # total 71
    assert not luhn_ok("8639767179579576")  # a 16-digit order number from the test
    assert not luhn_ok("4077001400188961")
    assert not luhn_ok("1") and not luhn_ok("12a4")


def test_ssn_rules():
    assert ssn_ok("583", "36", "2247")
    for bad in [("000", "12", "3456"), ("666", "12", "3456"), ("912", "12", "3456"), ("123", "00", "4567"), ("123", "45", "0000")]:
        assert not ssn_ok(*bad), bad


def test_merge_keeps_the_longest():
    a = {"type": "IBAN", "start": 0, "end": 27, "text": ""}
    b = {"type": "PHONE", "start": 5, "end": 17, "text": ""}
    c = {"type": "EMAIL", "start": 30, "end": 40, "text": ""}
    assert merge([b, c, a]) == [a, c]
    assert merge([]) == []


def test_patterns_go_by_digits_not_words():
    # The cold open: the model missed the card; the patterns found it, but not the name.
    text = MESSAGES["llm-007"][0]
    found = detect(text)
    assert [f["type"] for f in found] == ["CARD", "SSN"]
    assert found[0]["text"] == "4805-3137-1544-1511"
    # An order number fails the checksum, so it isn't a card.
    assert detect(MESSAGES["tpl-000"][0]) == []


def test_look_alikes_have_the_same_shape():
    # The price of patterns: an invoice number looks like a phone, a version like an IP.
    invoice = [f["text"] for f in detect(MESSAGES["tpl-001"][0]) if f["type"] == "PHONE"]
    assert invoice == ["(713) 555-1077", "2953502819"]
    ips = [f["text"] for f in detect(MESSAGES["tpl-004"][0]) if f["type"] == "IP"]
    assert ips == ["2.16.0.18", "102.78.100.138"]


def test_covered_ignores_the_label():
    # The model removed this SSN as PHONE ("my phone number is ..."): it's gone from the log.
    ssn = [g for g in gold("llm-056") if g["type"] == "SSN"][0]
    assert covered(ssn, model("llm-056"))
    assert not covered(ssn, [])
    # One character of slack each side: "(415) 555-0100" covers "415) 555-0100".
    assert covered({"start": 1, "end": 14}, [{"start": 0, "end": 14}])
    assert not covered({"start": 0, "end": 14}, [{"start": 2, "end": 14}])


def test_the_model_left_these_eight_unsafe():
    assert sum(still_unsafe(gold(k), model(k)) for k in WRITTEN) == 8


def test_patterns_alone_leave_names_and_addresses():
    unsafe = [k for k in MESSAGES if still_unsafe(gold(k), detect(MESSAGES[k][0]))]
    assert unsafe == ["llm-007", "llm-004", "llm-041", "tpl-001", "tpl-111"]


def test_both_together_leave_nothing():
    for k in MESSAGES:
        both = combine(detect(MESSAGES[k][0]), model(k))
        assert not still_unsafe(gold(k), both), k


def test_combine_takes_only_names_and_addresses_from_the_model():
    # The model's CARD label on an IBAN (llm-041) is dropped; the pattern's IBAN is kept.
    both = combine(detect(MESSAGES["llm-041"][0]), model("llm-041"))
    assert [f["type"] for f in both] == ["IBAN", "ADDRESS", "SSN"]


def test_redact_with_pseudonyms():
    text = MESSAGES["tpl-001"][0]
    both = combine(detect(text), model("tpl-001"))
    assert redact(text, both, pseudonymize=True) == (
        "Hello! [PERSON_1] here. Reach me at [PHONE_1] or [EMAIL_1]. "
        "Invoice [PHONE_2] lists the wrong price: $650.43, not what I agreed."
    )
    assert redact(text, both).startswith("Hello! [PERSON] here. Reach me at [PHONE] or [EMAIL]. Invoice [PHONE]")


def test_same_value_same_pseudonym():
    text = "Mail a@x.io, not b@x.io. Again: a@x.io"
    found = [{"type": "EMAIL", "start": m, "end": m + 6, "text": text[m:m + 6]} for m in (5, 17, 32)]
    assert redact(text, found, pseudonymize=True) == "Mail [EMAIL_1], not [EMAIL_2]. Again: [EMAIL_1]"
