"""Tests on Lab 17's scripts 04 and 05, from their recorded run (October 2026): 24 made-up incident
reports with hand-coded facts, each rated three times by two methods on two models, and three made-up
vendors' questionnaire answers. Run: pytest test_incident_practice.py"""
from datetime import datetime

from incident_practice import add_business_days, changed_across_tries, clocks, decide, score, severity

# The 24 reports' hand-coded facts, in the lab's order. #0 is the CEO's salary, #1 the knee surgery,
# #17 the card numbers in logs.
FACTS = [{'physical_harm': False,
  'discrimination': False,
  'personal_data_exposed': True,
  'special_or_financial_data': False,
  'people_affected': 1,
  'money_usd': 0,
  'wrong_commitment': False,
  'public': False,
  'reached_users': True},
 {'physical_harm': False,
  'discrimination': False,
  'personal_data_exposed': True,
  'special_or_financial_data': True,
  'people_affected': 1,
  'money_usd': 0,
  'wrong_commitment': False,
  'public': False,
  'reached_users': True},
 {'physical_harm': False,
  'discrimination': False,
  'personal_data_exposed': False,
  'special_or_financial_data': False,
  'people_affected': 37,
  'money_usd': 6400,
  'wrong_commitment': False,
  'public': False,
  'reached_users': True},
 {'physical_harm': False,
  'discrimination': False,
  'personal_data_exposed': False,
  'special_or_financial_data': False,
  'people_affected': 412,
  'money_usd': 31000,
  'wrong_commitment': False,
  'public': False,
  'reached_users': True},
 {'physical_harm': False,
  'discrimination': False,
  'personal_data_exposed': False,
  'special_or_financial_data': False,
  'people_affected': 1,
  'money_usd': 0,
  'wrong_commitment': True,
  'public': True,
  'reached_users': True},
 {'physical_harm': False,
  'discrimination': False,
  'personal_data_exposed': False,
  'special_or_financial_data': False,
  'people_affected': 2300,
  'money_usd': 0,
  'wrong_commitment': False,
  'public': False,
  'reached_users': True},
 {'physical_harm': False,
  'discrimination': False,
  'personal_data_exposed': False,
  'special_or_financial_data': False,
  'people_affected': 0,
  'money_usd': 0,
  'wrong_commitment': False,
  'public': False,
  'reached_users': False},
 {'physical_harm': False,
  'discrimination': False,
  'personal_data_exposed': False,
  'special_or_financial_data': False,
  'people_affected': 0,
  'money_usd': 0,
  'wrong_commitment': False,
  'public': False,
  'reached_users': False},
 {'physical_harm': False,
  'discrimination': False,
  'personal_data_exposed': False,
  'special_or_financial_data': False,
  'people_affected': 1,
  'money_usd': 0,
  'wrong_commitment': False,
  'public': False,
  'reached_users': True},
 {'physical_harm': False,
  'discrimination': False,
  'personal_data_exposed': True,
  'special_or_financial_data': False,
  'people_affected': 80,
  'money_usd': 0,
  'wrong_commitment': False,
  'public': False,
  'reached_users': True},
 {'physical_harm': False,
  'discrimination': False,
  'personal_data_exposed': False,
  'special_or_financial_data': False,
  'people_affected': 3,
  'money_usd': 0,
  'wrong_commitment': False,
  'public': False,
  'reached_users': True},
 {'physical_harm': False,
  'discrimination': False,
  'personal_data_exposed': True,
  'special_or_financial_data': False,
  'people_affected': 1,
  'money_usd': 0,
  'wrong_commitment': False,
  'public': False,
  'reached_users': True},
 {'physical_harm': False,
  'discrimination': False,
  'personal_data_exposed': False,
  'special_or_financial_data': False,
  'people_affected': 0,
  'money_usd': 0,
  'wrong_commitment': False,
  'public': False,
  'reached_users': False},
 {'physical_harm': False,
  'discrimination': False,
  'personal_data_exposed': False,
  'special_or_financial_data': False,
  'people_affected': 0,
  'money_usd': 0,
  'wrong_commitment': False,
  'public': False,
  'reached_users': False},
 {'physical_harm': True,
  'discrimination': False,
  'personal_data_exposed': False,
  'special_or_financial_data': False,
  'people_affected': 1,
  'money_usd': 0,
  'wrong_commitment': False,
  'public': False,
  'reached_users': True},
 {'physical_harm': False,
  'discrimination': True,
  'personal_data_exposed': False,
  'special_or_financial_data': False,
  'people_affected': 900,
  'money_usd': 0,
  'wrong_commitment': False,
  'public': False,
  'reached_users': True},
 {'physical_harm': False,
  'discrimination': False,
  'personal_data_exposed': True,
  'special_or_financial_data': False,
  'people_affected': 40,
  'money_usd': 0,
  'wrong_commitment': False,
  'public': False,
  'reached_users': True},
 {'physical_harm': False,
  'discrimination': False,
  'personal_data_exposed': True,
  'special_or_financial_data': True,
  'people_affected': 8000,
  'money_usd': 0,
  'wrong_commitment': False,
  'public': False,
  'reached_users': False},
 {'physical_harm': False,
  'discrimination': False,
  'personal_data_exposed': False,
  'special_or_financial_data': False,
  'people_affected': 120,
  'money_usd': 0,
  'wrong_commitment': False,
  'public': False,
  'reached_users': True},
 {'physical_harm': False,
  'discrimination': False,
  'personal_data_exposed': False,
  'special_or_financial_data': False,
  'people_affected': 0,
  'money_usd': 0,
  'wrong_commitment': False,
  'public': False,
  'reached_users': False},
 {'physical_harm': False,
  'discrimination': False,
  'personal_data_exposed': False,
  'special_or_financial_data': False,
  'people_affected': 15000,
  'money_usd': 45000,
  'wrong_commitment': True,
  'public': False,
  'reached_users': True},
 {'physical_harm': False,
  'discrimination': False,
  'personal_data_exposed': False,
  'special_or_financial_data': False,
  'people_affected': 0,
  'money_usd': 1800,
  'wrong_commitment': False,
  'public': False,
  'reached_users': False},
 {'physical_harm': False,
  'discrimination': False,
  'personal_data_exposed': False,
  'special_or_financial_data': False,
  'people_affected': 0,
  'money_usd': 0,
  'wrong_commitment': False,
  'public': False,
  'reached_users': False},
 {'physical_harm': False,
  'discrimination': False,
  'personal_data_exposed': True,
  'special_or_financial_data': False,
  'people_affected': 2000,
  'money_usd': 0,
  'wrong_commitment': False,
  'public': False,
  'reached_users': False}]

# Each report's level under the policy (incidents.severity of the facts above): 5 P1, 8 P2, 5 P3, 6 P4.
GOLD = ['P2', 'P1', 'P3', 'P2', 'P2', 'P2', 'P4', 'P4', 'P3', 'P2', 'P3', 'P2', 'P4', 'P4', 'P1', 'P1', 'P2', 'P1', 'P3', 'P4', 'P1', 'P3', 'P4', 'P2']

# The recorded ratings: three tries each. "name": the model picks the level. "facts": the model answers
# the fact questions and the policy's table decides.
RUNS = {
    'name_nano': [['P2',
  'P2',
  'P2',
  'P2',
  'P2',
  'P3',
  'P4',
  'P4',
  'P3',
  'P2',
  'P3',
  'P2',
  'P4',
  'P4',
  'P1',
  'P1',
  'P2',
  'P2',
  'P2',
  'P4',
  'P2',
  'P4',
  'P4',
  'P2'],
 ['P2',
  'P2',
  'P3',
  'P2',
  'P2',
  'P2',
  'P4',
  'P4',
  'P3',
  'P2',
  'P3',
  'P2',
  'P4',
  'P4',
  'P1',
  'P2',
  'P2',
  'P2',
  'P2',
  'P4',
  'P2',
  'P3',
  'P4',
  'P2'],
 ['P2',
  'P2',
  'P3',
  'P2',
  'P3',
  'P2',
  'P4',
  'P4',
  'P3',
  'P2',
  'P3',
  'P2',
  'P4',
  'P4',
  'P1',
  'P3',
  'P2',
  'P2',
  'P3',
  'P4',
  'P2',
  'P4',
  'P4',
  'P2']],
    'name_mini': [['P1',
  'P2',
  'P3',
  'P2',
  'P2',
  'P2',
  'P4',
  'P4',
  'P3',
  'P2',
  'P2',
  'P2',
  'P4',
  'P4',
  'P1',
  'P1',
  'P2',
  'P1',
  'P3',
  'P4',
  'P2',
  'P3',
  'P4',
  'P2'],
 ['P1',
  'P2',
  'P3',
  'P2',
  'P2',
  'P2',
  'P4',
  'P4',
  'P3',
  'P2',
  'P3',
  'P2',
  'P4',
  'P4',
  'P1',
  'P1',
  'P2',
  'P1',
  'P3',
  'P4',
  'P2',
  'P3',
  'P4',
  'P2'],
 ['P2',
  'P2',
  'P3',
  'P2',
  'P2',
  'P2',
  'P4',
  'P4',
  'P3',
  'P2',
  'P3',
  'P2',
  'P4',
  'P4',
  'P1',
  'P1',
  'P2',
  'P1',
  'P3',
  'P4',
  'P2',
  'P3',
  'P4',
  'P2']],
    'facts_nano': [['P1',
  'P2',
  'P3',
  'P2',
  'P1',
  'P1',
  'P4',
  'P4',
  'P3',
  'P2',
  'P3',
  'P2',
  'P4',
  'P4',
  'P1',
  'P1',
  'P2',
  'P1',
  'P3',
  'P4',
  'P1',
  'P3',
  'P4',
  'P2'],
 ['P2',
  'P2',
  'P3',
  'P2',
  'P1',
  'P1',
  'P4',
  'P4',
  'P3',
  'P2',
  'P3',
  'P2',
  'P4',
  'P4',
  'P1',
  'P1',
  'P2',
  'P1',
  'P3',
  'P4',
  'P1',
  'P3',
  'P4',
  'P2'],
 ['P2',
  'P2',
  'P3',
  'P2',
  'P1',
  'P1',
  'P4',
  'P4',
  'P3',
  'P2',
  'P3',
  'P2',
  'P4',
  'P4',
  'P1',
  'P1',
  'P2',
  'P1',
  'P3',
  'P4',
  'P1',
  'P3',
  'P4',
  'P2']],
    'facts_mini': [['P1',
  'P2',
  'P2',
  'P1',
  'P2',
  'P2',
  'P4',
  'P4',
  'P3',
  'P2',
  'P3',
  'P2',
  'P3',
  'P4',
  'P1',
  'P1',
  'P1',
  'P1',
  'P3',
  'P4',
  'P1',
  'P3',
  'P4',
  'P2'],
 ['P1',
  'P2',
  'P2',
  'P1',
  'P2',
  'P2',
  'P4',
  'P4',
  'P3',
  'P2',
  'P3',
  'P2',
  'P3',
  'P4',
  'P1',
  'P1',
  'P1',
  'P1',
  'P3',
  'P4',
  'P1',
  'P3',
  'P4',
  'P2'],
 ['P1',
  'P2',
  'P2',
  'P1',
  'P2',
  'P2',
  'P4',
  'P4',
  'P3',
  'P2',
  'P3',
  'P2',
  'P3',
  'P4',
  'P1',
  'P1',
  'P1',
  'P1',
  'P2',
  'P4',
  'P1',
  'P3',
  'P4',
  'P2']],
}

# The vendor questionnaire's rules: an answer listed here blocks the deal or needs a contract condition.
RULES = {'location': {'us': 'condition', 'not_stated': 'condition'},
 'training': {'yes': 'blocker', 'not_stated': 'blocker', 'opt_out': 'condition'},
 'retention': {'more_than_30_days': 'condition', 'not_stated': 'condition'},
 'deletion': {'yes_unconfirmed': 'condition', 'not_stated': 'condition'},
 'soc2': {'type_1': 'condition', 'claimed_type_not_stated': 'condition', 'not_stated': 'blocker'},
 'breach_notice': {'no_fixed_time': 'condition', 'not_stated': 'blocker'},
 'subprocessors': {'published_no_notice': 'condition', 'not_stated': 'condition'},
 'dpa': {'not_stated': 'blocker'},
 'model_changes': {'no_notice': 'condition', 'not_stated': 'condition'},
 'encryption': {'vague': 'condition', 'not_stated': 'condition'}}

# Kestrel's trust page, as a person reads it, and as gpt-5.4-mini read it (first try).
KESTREL = {'location': 'us',
 'training': 'opt_out',
 'retention': 'more_than_30_days',
 'deletion': 'yes_unconfirmed',
 'soc2': 'type_1',
 'breach_notice': 'no_fixed_time',
 'subprocessors': 'published_no_notice',
 'dpa': 'yes',
 'model_changes': 'no_notice',
 'encryption': 'both'}
KESTREL_MINI = {'location': 'us',
 'training': 'opt_out',
 'retention': 'more_than_30_days',
 'deletion': 'yes_confirmed',
 'soc2': 'type_1',
 'breach_notice': 'not_stated',
 'subprocessors': 'published_no_notice',
 'dpa': 'yes',
 'model_changes': 'no_notice',
 'encryption': 'both'}
NORTHWIND = {'location': 'customer_choice',
 'training': 'no',
 'retention': '30_days_or_less',
 'deletion': 'yes_confirmed',
 'soc2': 'type_2',
 'breach_notice': '72_hours_or_less',
 'subprocessors': 'published_with_notice',
 'dpa': 'yes',
 'model_changes': 'advance_notice',
 'encryption': 'both'}
BRIGHTLINE = {'location': 'us',
 'training': 'yes',
 'retention': 'not_stated',
 'deletion': 'not_stated',
 'soc2': 'claimed_type_not_stated',
 'breach_notice': 'not_stated',
 'subprocessors': 'not_stated',
 'dpa': 'not_stated',
 'model_changes': 'no_notice',
 'encryption': 'vague'}


def test_the_policy_gives_every_reports_level():
    assert [severity(f) for f in FACTS] == GOLD
    assert [GOLD.count(p) for p in ("P1", "P2", "P3", "P4")] == [5, 8, 5, 6]


def test_one_fact_decides_the_knee_surgery():
    knee = FACTS[1]
    assert severity(knee) == "P1"
    assert severity(dict(knee, special_or_financial_data=False)) == "P2"


def test_naming_the_level_first_try():
    assert score(RUNS["name_nano"][0], GOLD) == {"right": 17, "too_low": 5, "too_high": 2, "p1_found": 2}
    assert score(RUNS["name_mini"][0], GOLD) == {"right": 20, "too_low": 2, "too_high": 2, "p1_found": 3}


def test_facts_first_rates_fewer_too_low_and_more_too_high():
    assert score(RUNS["facts_nano"][0], GOLD) == {"right": 20, "too_low": 1, "too_high": 3, "p1_found": 4}
    assert score(RUNS["facts_mini"][0], GOLD) == {"right": 18, "too_low": 1, "too_high": 5, "p1_found": 4}


def test_the_knee_surgery_was_p2_all_twelve_times():
    assert [r[1] for k in RUNS for r in RUNS[k]] == ["P2"] * 12


def test_facts_first_changed_its_answer_less():
    assert {k: changed_across_tries(v) for k, v in RUNS.items()} == {
        "name_nano": 6, "name_mini": 2, "facts_nano": 1, "facts_mini": 1}


def test_business_days_skip_the_weekend():
    friday = datetime(2026, 10, 23, 16, 0)
    assert add_business_days(friday, 1) == datetime(2026, 10, 26, 16, 0)
    assert add_business_days(datetime(2026, 10, 20, 10, 0), 4) == datetime(2026, 10, 26, 10, 0)


def test_friday_afternoon_clocks():
    c = clocks(datetime(2026, 10, 23, 16, 0))
    assert c == {"gdpr": datetime(2026, 10, 26, 16, 0), "sec": datetime(2026, 10, 29, 16, 0)}


def test_three_vendors_from_their_pages():
    assert decide(NORTHWIND, RULES) == ("approve", [], [])
    assert decide(BRIGHTLINE, RULES)[:2] == ("reject", ["training", "breach_notice", "dpa"])
    d, blockers, conditions = decide(KESTREL, RULES)
    assert (d, blockers, len(conditions)) == ("approve with conditions", [], 8)


def test_one_misread_sentence_turns_approve_into_reject():
    wrong = [q for q in KESTREL if KESTREL_MINI[q] != KESTREL[q]]
    assert wrong == ["deletion", "breach_notice"]
    assert decide(KESTREL_MINI, RULES)[:2] == ("reject", ["breach_notice"])
