"""Executable teaching model, not a distributed authorization implementation.
Run: python3 rag-cache-revocation.py
No network, third-party packages, or persistent writes.
"""
from dataclasses import dataclass
import unittest


@dataclass(frozen=True)
class Answer:
    text: str
    tenant: str
    principal: str
    epoch: int
    dependencies: frozenset[str]


class Policy:
    def __init__(self):
        self.epoch = 1
        self.available = True
        self.allowed = {('north', 'alice'): {'pricing'}, ('south', 'bob'): set()}

    def snapshot(self, tenant, principal):
        if not self.available:
            raise PermissionError('Policy service unavailable: no cached fallback')
        return self.epoch, frozenset(self.allowed.get((tenant, principal), set()))

    def revoke(self):
        self.allowed[('north', 'alice')].clear()
        self.epoch += 1


def release(answer, policy, tenant, principal):
    """Check at release, not just before model generation.

    Snapshot and return are sequential in this fixture. Real systems need an
    explicit consistency/lease protocol and a defined revocation contract.
    """
    epoch, allowed = policy.snapshot(tenant, principal)
    if (answer.tenant, answer.principal) != (tenant, principal):
        raise PermissionError('Wrong tenant or principal')
    if answer.epoch != epoch:
        raise PermissionError('Stale authorization epoch: regenerate')
    if not answer.dependencies or not answer.dependencies <= allowed:
        raise PermissionError('Missing or unauthorized provenance')
    return answer.text


class RevocationTests(unittest.TestCase):
    def setUp(self):
        self.policy = Policy()
        self.answer = Answer('Restricted pricing summary', 'north', 'alice', 1,
                             frozenset({'pricing'}))

    def test_authorized_hit(self):
        self.assertEqual(release(self.answer, self.policy, 'north', 'alice'),
                         'Restricted pricing summary')

    def test_naive_cache_reproduces_leak(self):
        cache = {'pricing question': self.answer.text}
        self.policy.revoke()
        self.assertEqual(cache['pricing question'], 'Restricted pricing summary')

    def test_revoked_hit_denied(self):
        self.policy.revoke()
        with self.assertRaises(PermissionError):
            release(self.answer, self.policy, 'north', 'alice')

    def test_other_tenant_denied(self):
        with self.assertRaises(PermissionError):
            release(self.answer, self.policy, 'south', 'bob')

    def test_revocation_during_generation_denied(self):
        epoch, docs = self.policy.snapshot('north', 'alice')
        self.policy.revoke()  # Simulates generation latency after retrieval.
        generated = Answer('Generated after retrieval', 'north', 'alice', epoch, docs)
        with self.assertRaises(PermissionError):
            release(generated, self.policy, 'north', 'alice')

    def test_policy_outage_denied(self):
        self.policy.available = False
        with self.assertRaises(PermissionError):
            release(self.answer, self.policy, 'north', 'alice')

    def test_unknown_provenance_denied(self):
        answer = Answer('No source manifest', 'north', 'alice', 1, frozenset())
        with self.assertRaises(PermissionError):
            release(answer, self.policy, 'north', 'alice')


if __name__ == '__main__':
    unittest.main(verbosity=2)
