"""Tests on Lab 16's script 01, on the made-up company Lantern Outfitters: 8 people, 47 documents,
every person asks every document's question (376 requests). RANKING is the search's real order for
each question (rebuilt from the lab's cached embeddings; its top 4 match every recorded request), so
the counts below are the lab's own. Run: pytest test_access_practice.py"""
from access_practice import K, leaked, may_read, principals, retrieve, shows_forbidden

IDS = ['pub-returns', 'pub-shipping', 'pub-warranty', 'pub-careers', 'pub-berlin', 'pub-privacy', 'pub-loyalty', 'pub-about', 'pub-tent-ridgeline', 'pub-stove', 'int-security', 'int-expenses', 'int-office-days', 'int-holidays', 'int-parental-leave', 'int-review-process', 'int-salary-process', 'int-allhands', 'int-ooo', 'int-org', 'sup-escalation', 'sup-fraud', 'case-c101', 'case-c102', 'case-c104', 'case-c107', 'case-c110', 'eng-oncall', 'eng-postmortem', 'eng-architecture', 'eng-vuln', 'hr-salary-bands', 'hr-review-ravi', 'hr-review-ana', 'hr-juniper', 'hr-investigation', 'ben-leave-daniel', 'ben-accommodation-mia', 'fin-q3', 'fin-budget', 'fin-carrier', 'fin-eu-vat', 'leg-trailco', 'leg-cnil', 'exec-heron', 'exec-pricing', 'exec-ceo-pay']

# For each question (by its document's id): every document, best match first, as indexes into IDS.
_RANK = {
    'pub-returns': [0, 24, 20, 2, 14, 22, 26, 1, 13, 5, 43, 34, 25, 36, 18, 23, 21, 28, 41, 11, 6, 10, 27, 12, 30, 40, 33, 35, 42, 37, 38, 15, 17, 16, 39, 19, 29, 45, 3, 7, 44, 4, 8, 9, 31, 32, 46],
    'pub-shipping': [1, 21, 6, 0, 40, 29, 20, 41, 25, 38, 28, 5, 39, 18, 23, 31, 24, 19, 2, 11, 13, 44, 17, 45, 3, 10, 27, 22, 4, 8, 14, 7, 34, 30, 16, 43, 42, 37, 46, 33, 36, 12, 32, 35, 26, 9, 15],
    'pub-warranty': [2, 42, 8, 22, 45, 7, 9, 40, 14, 1, 39, 3, 11, 0, 5, 44, 13, 6, 19, 35, 4, 31, 20, 16, 30, 10, 34, 41, 36, 28, 38, 25, 23, 37, 15, 33, 17, 26, 43, 18, 12, 32, 24, 29, 27, 21, 46],
    'pub-careers': [16, 11, 31, 14, 3, 37, 46, 17, 12, 39, 15, 19, 44, 18, 34, 35, 10, 33, 38, 45, 40, 32, 13, 41, 36, 7, 4, 20, 2, 1, 5, 30, 43, 0, 6, 8, 25, 42, 26, 9, 29, 24, 22, 28, 23, 27, 21],
    'pub-berlin': [4, 41, 19, 17, 18, 43, 12, 29, 25, 34, 5, 38, 39, 1, 40, 44, 20, 0, 35, 46, 31, 3, 10, 28, 16, 30, 13, 45, 14, 7, 23, 37, 27, 24, 2, 11, 26, 6, 33, 36, 21, 15, 42, 32, 22, 9, 8],
    'pub-privacy': [29, 5, 0, 21, 43, 28, 25, 36, 20, 27, 34, 41, 18, 40, 1, 38, 23, 35, 17, 37, 14, 10, 22, 13, 30, 39, 12, 24, 26, 15, 2, 42, 33, 16, 19, 6, 44, 11, 31, 45, 46, 32, 4, 3, 7, 9, 8],
    'pub-loyalty': [6, 40, 45, 38, 28, 11, 31, 46, 44, 39, 17, 0, 5, 3, 14, 20, 1, 34, 30, 15, 18, 41, 7, 37, 16, 10, 2, 25, 33, 21, 22, 8, 19, 32, 13, 42, 12, 29, 4, 23, 43, 24, 26, 27, 36, 35, 9],
    'pub-about': [17, 44, 46, 19, 38, 7, 4, 43, 39, 45, 42, 34, 5, 3, 10, 31, 35, 2, 11, 15, 40, 0, 25, 12, 14, 16, 29, 28, 30, 13, 22, 32, 41, 18, 20, 24, 23, 6, 37, 26, 27, 36, 9, 33, 21, 1, 8],
    'pub-tent-ridgeline': [8, 42, 2, 22, 7, 9, 40, 39, 44, 3, 34, 11, 45, 1, 35, 5, 6, 4, 20, 31, 19, 38, 23, 41, 30, 13, 14, 32, 33, 16, 25, 29, 26, 10, 37, 36, 43, 17, 28, 18, 21, 15, 0, 24, 27, 12, 46],
    'pub-stove': [9, 23, 1, 8, 7, 2, 3, 38, 39, 31, 11, 4, 20, 40, 44, 19, 41, 32, 33, 29, 16, 6, 34, 27, 14, 5, 42, 15, 45, 17, 25, 0, 13, 30, 26, 43, 36, 35, 10, 24, 22, 28, 46, 18, 37, 12, 21],
    'int-security': [10, 30, 12, 5, 27, 37, 16, 11, 28, 15, 2, 43, 14, 21, 45, 34, 20, 31, 6, 25, 40, 13, 29, 18, 33, 17, 46, 0, 32, 23, 36, 42, 39, 41, 1, 22, 7, 35, 3, 19, 26, 44, 24, 38, 9, 4, 8],
    'int-expenses': [11, 12, 37, 9, 0, 16, 45, 8, 40, 14, 31, 24, 41, 46, 3, 20, 22, 15, 23, 39, 1, 33, 2, 38, 25, 42, 18, 21, 26, 36, 6, 13, 30, 5, 32, 27, 34, 35, 17, 43, 7, 28, 10, 19, 44, 4, 29],
    'int-office-days': [12, 18, 37, 13, 14, 27, 36, 10, 11, 17, 15, 16, 34, 30, 33, 46, 35, 28, 3, 20, 19, 31, 1, 45, 32, 39, 25, 0, 29, 43, 22, 41, 23, 5, 6, 26, 38, 24, 44, 40, 21, 4, 2, 42, 7, 8, 9],
    'int-holidays': [13, 12, 18, 36, 34, 14, 17, 28, 37, 30, 27, 11, 16, 19, 39, 10, 15, 38, 3, 46, 1, 0, 35, 45, 41, 4, 31, 5, 33, 29, 6, 44, 43, 20, 2, 40, 25, 7, 42, 32, 24, 23, 22, 9, 21, 26, 8],
    'int-parental-leave': [14, 36, 16, 13, 0, 31, 12, 37, 15, 46, 3, 11, 34, 18, 2, 40, 38, 1, 5, 28, 17, 32, 33, 6, 24, 39, 44, 35, 45, 42, 26, 25, 43, 22, 4, 8, 41, 27, 20, 19, 10, 23, 9, 29, 30, 7, 21],
    'int-review-process': [15, 16, 33, 32, 46, 17, 12, 31, 18, 37, 34, 14, 35, 45, 20, 27, 11, 21, 3, 36, 25, 30, 13, 22, 38, 43, 26, 40, 19, 0, 5, 39, 44, 2, 29, 24, 6, 28, 23, 10, 42, 1, 41, 7, 4, 9, 8],
    'int-salary-process': [16, 46, 31, 15, 12, 45, 17, 18, 14, 33, 37, 34, 32, 11, 38, 39, 3, 36, 35, 13, 30, 41, 28, 27, 0, 44, 10, 20, 40, 25, 1, 24, 43, 21, 23, 5, 19, 6, 4, 29, 26, 2, 7, 42, 22, 9, 8],
    'int-allhands': [17, 34, 15, 38, 27, 18, 33, 32, 29, 12, 39, 44, 30, 45, 43, 16, 46, 28, 19, 36, 37, 14, 20, 21, 13, 31, 41, 22, 10, 23, 25, 0, 35, 4, 42, 26, 3, 24, 40, 5, 11, 1, 6, 2, 8, 7, 9],
    'int-ooo': [36, 18, 12, 13, 34, 37, 35, 46, 17, 14, 19, 28, 32, 30, 33, 45, 23, 31, 39, 27, 16, 44, 0, 40, 15, 11, 25, 1, 22, 3, 20, 5, 38, 29, 10, 43, 24, 4, 7, 2, 6, 41, 26, 42, 21, 9, 8],
    'int-org': [19, 17, 18, 15, 32, 33, 16, 12, 3, 20, 31, 14, 46, 34, 44, 45, 30, 36, 29, 37, 5, 35, 43, 10, 7, 25, 38, 39, 27, 6, 23, 22, 11, 40, 26, 24, 4, 28, 2, 13, 21, 8, 42, 9, 0, 1, 41],
    'sup-escalation': [20, 24, 16, 11, 0, 21, 41, 14, 32, 31, 30, 22, 18, 15, 46, 25, 33, 42, 45, 27, 39, 37, 26, 17, 35, 12, 43, 23, 34, 36, 2, 3, 29, 19, 10, 28, 40, 44, 5, 38, 6, 13, 8, 1, 7, 9, 4],
    'sup-fraud': [21, 20, 29, 30, 27, 10, 25, 28, 5, 41, 40, 0, 43, 24, 6, 22, 1, 17, 23, 42, 11, 35, 39, 34, 31, 19, 2, 26, 38, 33, 44, 18, 32, 45, 14, 16, 15, 12, 3, 13, 4, 7, 37, 46, 36, 8, 9],
    'case-c101': [22, 36, 25, 35, 23, 18, 26, 24, 34, 33, 20, 37, 14, 27, 12, 40, 42, 0, 17, 21, 28, 6, 43, 19, 5, 32, 30, 46, 38, 39, 44, 15, 45, 11, 13, 3, 2, 31, 16, 41, 29, 1, 8, 10, 7, 4, 9],
    'case-c102': [23, 9, 34, 18, 36, 1, 4, 40, 25, 44, 17, 19, 35, 29, 39, 20, 22, 41, 13, 3, 5, 0, 32, 37, 14, 31, 7, 42, 27, 12, 38, 16, 2, 11, 28, 30, 33, 46, 6, 24, 10, 21, 26, 45, 43, 8, 15],
    'case-c104': [24, 0, 41, 11, 20, 25, 28, 22, 21, 23, 43, 33, 37, 36, 39, 19, 5, 27, 38, 29, 30, 46, 34, 14, 16, 6, 35, 42, 32, 40, 44, 26, 18, 3, 17, 4, 31, 10, 13, 12, 45, 2, 7, 15, 1, 9, 8],
    'case-c107': [25, 19, 5, 37, 43, 22, 18, 20, 3, 34, 6, 35, 23, 17, 4, 24, 1, 31, 0, 2, 10, 14, 41, 30, 33, 40, 13, 12, 7, 11, 36, 26, 21, 44, 28, 16, 39, 29, 27, 15, 38, 32, 46, 42, 45, 9, 8],
    'case-c110': [26, 33, 37, 22, 35, 25, 17, 8, 23, 45, 32, 24, 19, 18, 38, 36, 46, 16, 15, 31, 30, 42, 39, 44, 5, 43, 10, 3, 9, 41, 20, 27, 7, 34, 29, 14, 2, 28, 11, 12, 40, 4, 0, 1, 21, 6, 13],
    'eng-oncall': [27, 10, 30, 28, 29, 12, 17, 45, 15, 23, 20, 34, 18, 21, 5, 31, 37, 42, 39, 46, 14, 19, 25, 44, 16, 35, 22, 41, 38, 9, 24, 43, 6, 40, 33, 11, 36, 32, 26, 2, 1, 13, 7, 0, 3, 8, 4],
    'eng-postmortem': [28, 27, 18, 30, 13, 36, 25, 34, 35, 22, 20, 43, 23, 17, 24, 0, 37, 10, 12, 29, 14, 38, 26, 21, 45, 41, 1, 39, 33, 42, 15, 40, 46, 6, 44, 19, 5, 2, 11, 16, 32, 8, 4, 3, 31, 7, 9],
    'eng-architecture': [5, 29, 41, 43, 25, 17, 28, 10, 0, 19, 20, 4, 21, 30, 23, 38, 27, 34, 40, 18, 1, 39, 24, 6, 31, 26, 12, 15, 14, 16, 37, 13, 44, 35, 22, 46, 3, 45, 36, 2, 33, 7, 32, 11, 42, 9, 8],
    'eng-vuln': [30, 10, 28, 27, 35, 29, 17, 34, 21, 18, 42, 31, 25, 22, 12, 5, 13, 32, 20, 43, 40, 2, 45, 15, 41, 44, 16, 3, 39, 33, 26, 36, 19, 38, 14, 7, 23, 0, 11, 37, 6, 4, 1, 24, 46, 8, 9],
    'hr-salary-bands': [31, 16, 46, 32, 15, 3, 17, 18, 39, 30, 12, 29, 45, 10, 38, 34, 33, 27, 40, 14, 20, 35, 21, 28, 36, 19, 37, 44, 11, 41, 42, 25, 43, 26, 13, 24, 4, 1, 2, 0, 9, 23, 5, 6, 22, 7, 8],
    'hr-review-ravi': [32, 46, 17, 37, 33, 15, 18, 36, 34, 16, 45, 31, 19, 35, 44, 12, 43, 3, 38, 30, 23, 14, 24, 11, 39, 21, 10, 26, 40, 25, 20, 4, 42, 41, 8, 6, 29, 5, 27, 28, 7, 22, 9, 1, 13, 0, 2],
    'hr-review-ana': [33, 32, 15, 26, 24, 37, 25, 46, 16, 34, 38, 18, 41, 17, 23, 22, 45, 35, 31, 36, 43, 39, 30, 20, 40, 11, 19, 28, 21, 27, 12, 8, 44, 3, 0, 29, 5, 6, 10, 13, 14, 7, 9, 42, 1, 2, 4],
    'hr-juniper': [34, 18, 17, 13, 30, 12, 39, 44, 27, 28, 38, 45, 16, 35, 14, 37, 3, 19, 46, 33, 36, 31, 4, 43, 10, 5, 42, 15, 32, 25, 29, 11, 40, 20, 6, 7, 41, 23, 26, 21, 22, 2, 1, 24, 0, 8, 9],
    'hr-investigation': [35, 16, 43, 37, 18, 33, 30, 26, 36, 17, 15, 46, 25, 14, 21, 32, 27, 31, 10, 12, 34, 24, 20, 3, 11, 42, 44, 19, 28, 22, 23, 5, 41, 13, 45, 40, 38, 29, 39, 6, 7, 2, 4, 0, 1, 9, 8],
    'ben-leave-daniel': [36, 18, 12, 35, 46, 37, 13, 34, 23, 28, 19, 17, 30, 32, 45, 33, 14, 22, 27, 39, 31, 16, 11, 43, 25, 40, 20, 10, 29, 3, 44, 24, 15, 5, 0, 7, 4, 1, 38, 26, 41, 6, 2, 42, 21, 9, 8],
    'ben-accommodation-mia': [37, 18, 12, 33, 25, 13, 14, 16, 34, 35, 46, 36, 17, 11, 30, 28, 15, 26, 32, 43, 6, 31, 45, 10, 27, 19, 3, 5, 20, 24, 23, 39, 38, 41, 44, 22, 0, 29, 4, 21, 40, 1, 7, 42, 2, 8, 9],
    'fin-q3': [38, 17, 41, 46, 45, 39, 44, 15, 28, 32, 16, 34, 30, 31, 18, 33, 27, 11, 43, 35, 0, 20, 40, 21, 36, 25, 42, 24, 19, 13, 29, 22, 4, 23, 3, 5, 14, 12, 37, 10, 6, 26, 7, 9, 2, 1, 8],
    'fin-budget': [39, 17, 45, 38, 46, 31, 44, 34, 16, 41, 19, 13, 11, 3, 15, 30, 42, 40, 4, 18, 32, 33, 6, 7, 36, 14, 5, 8, 28, 12, 0, 27, 43, 10, 20, 35, 29, 2, 22, 9, 37, 26, 24, 21, 23, 1, 25],
    'fin-carrier': [40, 6, 45, 1, 31, 5, 46, 0, 41, 39, 16, 11, 21, 44, 20, 29, 14, 3, 38, 2, 18, 35, 25, 42, 28, 15, 23, 12, 34, 17, 33, 24, 4, 7, 19, 10, 22, 32, 8, 37, 30, 43, 36, 13, 27, 9, 26],
    'fin-eu-vat': [41, 38, 43, 17, 0, 39, 4, 24, 5, 45, 31, 25, 28, 46, 40, 11, 16, 19, 2, 13, 34, 44, 6, 18, 20, 30, 14, 42, 23, 15, 10, 33, 22, 36, 26, 37, 29, 21, 3, 32, 1, 35, 12, 27, 7, 9, 8],
    'leg-trailco': [42, 35, 43, 30, 25, 2, 20, 11, 26, 21, 5, 19, 44, 10, 38, 41, 14, 40, 16, 24, 46, 22, 17, 27, 23, 37, 3, 34, 36, 31, 28, 39, 18, 6, 15, 45, 7, 13, 4, 29, 0, 32, 33, 12, 1, 9, 8],
    'leg-cnil': [43, 35, 21, 42, 30, 5, 20, 26, 25, 2, 41, 23, 28, 22, 24, 16, 40, 32, 15, 10, 19, 46, 33, 7, 38, 11, 27, 45, 34, 0, 6, 3, 17, 14, 37, 29, 31, 4, 39, 44, 13, 18, 36, 1, 9, 8, 12],
    'exec-heron': [44, 17, 38, 46, 34, 39, 19, 30, 45, 3, 42, 35, 4, 18, 14, 16, 11, 32, 40, 33, 43, 29, 31, 12, 37, 5, 10, 21, 41, 15, 13, 27, 36, 25, 28, 6, 7, 20, 24, 23, 26, 22, 2, 1, 9, 0, 8],
    'exec-pricing': [45, 39, 31, 16, 17, 46, 38, 40, 44, 13, 34, 11, 4, 3, 41, 30, 15, 42, 2, 1, 6, 33, 7, 18, 32, 5, 28, 8, 19, 21, 20, 35, 29, 10, 12, 9, 14, 43, 0, 37, 36, 23, 25, 27, 22, 24, 26],
    'exec-ceo-pay': [46, 16, 31, 32, 38, 17, 19, 45, 39, 35, 44, 33, 11, 15, 3, 37, 18, 43, 12, 36, 41, 25, 10, 14, 23, 24, 42, 28, 34, 20, 30, 22, 40, 27, 6, 13, 29, 4, 5, 21, 26, 0, 2, 9, 1, 7, 8],
}
RANKING = {q: [IDS[i] for i in r] for q, r in _RANK.items()}

# Who may read each document when the index was built.
ACL = {
    'pub-returns': ['public'],
    'pub-shipping': ['public'],
    'pub-warranty': ['public'],
    'pub-careers': ['public'],
    'pub-berlin': ['public'],
    'pub-privacy': ['public'],
    'pub-loyalty': ['public'],
    'pub-about': ['public'],
    'pub-tent-ridgeline': ['public'],
    'pub-stove': ['public'],
    'int-security': ['staff'],
    'int-expenses': ['staff'],
    'int-office-days': ['staff'],
    'int-holidays': ['staff'],
    'int-parental-leave': ['staff'],
    'int-review-process': ['staff'],
    'int-salary-process': ['staff'],
    'int-allhands': ['staff'],
    'int-ooo': ['staff'],
    'int-org': ['staff'],
    'sup-escalation': ['support'],
    'sup-fraud': ['support'],
    'case-c101': ['support'],
    'case-c102': ['support'],
    'case-c104': ['support'],
    'case-c107': ['support'],
    'case-c110': ['support'],
    'eng-oncall': ['engineering'],
    'eng-postmortem': ['engineering'],
    'eng-architecture': ['engineering'],
    'eng-vuln': ['engineering', 'exec'],
    'hr-salary-bands': ['hr'],
    'hr-review-ravi': ['hr', 'user:ravi'],
    'hr-review-ana': ['hr', 'user:ana'],
    'hr-juniper': ['hr', 'exec'],
    'hr-investigation': ['hr'],
    'ben-leave-daniel': ['benefits'],
    'ben-accommodation-mia': ['benefits'],
    'fin-q3': ['finance', 'exec'],
    'fin-budget': ['finance', 'exec'],
    'fin-carrier': ['finance', 'exec'],
    'fin-eu-vat': ['finance'],
    'leg-trailco': ['legal', 'exec'],
    'leg-cnil': ['legal', 'exec'],
    'exec-heron': ['exec'],
    'exec-pricing': ['exec'],
    'exec-ceo-pay': ['exec'],
}

USERS = {u: {"id": u, "groups": g} for u, g in {'ana': ['staff', 'support'], 'jonas': ['staff', 'support'], 'ravi': ['staff', 'engineering'], 'helen': ['staff', 'hr', 'managers'], 'marta': ['staff', 'hr', 'benefits'], 'erik': ['staff', 'finance'], 'olivia': ['staff', 'exec', 'managers'], 'theo': ['contractors']}.items()}

CANARIES = {'exec-ceo-pay': ['310,000', '310000', '310k'], 'hr-salary-bands': ['148,500', '176,200', '148500', '176200', '71,300', '84,900'], 'int-salary-process': ['April 1', '1 April'], 'pub-careers': []}


def requests():
    """Every person asks every document's question: 8 x 47 = 376."""
    return [(u, d) for u in USERS for d in IDS]


def test_principals_and_may_read():
    theo, ana = USERS["theo"], USERS["ana"]
    assert principals(ana) == {"public", "user:ana", "staff", "support"}
    assert may_read(theo, ["public"]) and not may_read(theo, ["staff"])
    assert may_read(ana, ["hr", "support"])             # any one group is enough
    assert may_read({"id": "ravi", "groups": []}, ["user:ravi"])  # shared with one person
    assert not may_read({**theo, "disabled": True}, ["public"])   # a disabled account reads nothing


def test_the_test_has_196_allowed_and_180_denied():
    allowed = sum(may_read(USERS[u], ACL[d]) for u, d in requests())
    assert (len(requests()), allowed) == (376, 196)
    assert sum(may_read(USERS["theo"], ACL[d]) for d in IDS) == 10   # the 10 public pages


def test_the_contractor_and_the_ceo_salary():
    theo, q = USERS["theo"], "exec-ceo-pay"
    for mode in ("none", "prompt-only"):
        assert retrieve(RANKING[q], theo, mode, ACL) == [
            "exec-ceo-pay", "int-salary-process", "hr-salary-bands", "hr-review-ravi"]
    assert retrieve(RANKING[q], theo, "post-filter", ACL) == []
    assert retrieve(RANKING[q], theo, "pre-filter", ACL) == ["pub-careers", "pub-loyalty", "pub-berlin", "pub-privacy"]


def test_hidden_documents_take_seats():
    ana, q = USERS["ana"], "pub-careers"     # "How big is the yearly gear allowance for employees?"
    none = retrieve(RANKING[q], ana, "none", ACL)
    assert none[2] == "hr-salary-bands" and q not in none
    assert retrieve(RANKING[q], ana, "post-filter", ACL) == ["int-salary-process", "int-expenses", "int-parental-leave"]
    assert retrieve(RANKING[q], ana, "pre-filter", ACL)[-1] == q     # the careers page makes the top 4


def test_a_hidden_document_reaches_the_model_in_335_of_376():
    def shown_hidden(mode):
        return sum(any(not may_read(USERS[u], ACL[s]) for s in retrieve(RANKING[d], USERS[u], mode, ACL))
                   for u, d in requests())
    assert shown_hidden("none") == shown_hidden("prompt-only") == 335
    assert shown_hidden("post-filter") == shown_hidden("pre-filter") == 0


def test_post_filter_leaves_the_contractor_half_a_document():
    theo = USERS["theo"]
    shown = [len(retrieve(RANKING[d], theo, "post-filter", ACL)) for d in IDS]
    assert round(sum(shown) / len(shown), 1) == 0.5
    assert all(len(retrieve(RANKING[d], theo, "pre-filter", ACL)) == K for d in IDS)   # pre always fills 4


def test_right_document_found_post_vs_pre():
    # In the recorded run, an allowed question was answered exactly when its document was in the top 4.
    def found(mode):
        return sum(d in retrieve(RANKING[d], USERS[u], mode, ACL)
                   for u, d in requests() if may_read(USERS[u], ACL[d]))
    assert (found("none"), found("post-filter"), found("pre-filter")) == (180, 180, 193)


def test_canaries_catch_the_salary():
    hidden = ["exec-ceo-pay", "int-salary-process", "hr-salary-bands", "hr-review-ravi"]
    assert leaked("The CEO's 2027 base salary is set at **$310,000**. [exec-ceo-pay]", CANARIES, hidden) == ["exec-ceo-pay"]
    assert leaked("I don't have that information in the provided documents.", CANARIES, hidden) == []
    assert leaked("Band 3 tops out at 176,200.", CANARIES, ["hr-salary-bands"]) == ["hr-salary-bands"]


# Five changes after the index was built (Exercise 3).
def _now():
    users = {u: dict(v) for u, v in USERS.items()}
    users["ravi"]["groups"] = ["staff", "support"]          # moved from engineering to support
    users["theo"]["disabled"] = True                         # contract ended
    acl = dict(ACL)
    acl["eng-vuln"] = ["security", "exec"]                   # restricted
    acl["hr-juniper"] = ["exec"]                             # restricted while the plan is final
    acl["fin-carrier"] = ["finance", "exec", "support"]      # shared with support
    return users, acl


def _stale_run(acl_from, groups_from):
    users_now, acl_now = _now()
    acl = ACL if acl_from == "index copy" else acl_now
    hits = {}
    for u, d in requests():
        user = USERS[u] if groups_from == "login" else users_now[u]
        shown = retrieve(RANKING[d], user, "pre-filter", acl)
        if shows_forbidden(shown, users_now[u], acl_now):
            hits[u] = hits.get(u, 0) + 1
    return hits


def test_stale_permissions():
    counts = {(a, g): sum(_stale_run(a, g).values())
              for a in ("index copy", "directory") for g in ("login", "directory")}
    assert counts == {("index copy", "login"): 86, ("index copy", "directory"): 20,
                      ("directory", "login"): 62, ("directory", "directory"): 0}
    assert _stale_run("index copy", "login") == {"theo": 47, "ravi": 19, "helen": 11, "marta": 9}
