#!/usr/bin/env python3
"""Local teaching model, not an MCP integration test. No network or credentials.
Run: python3 read-only-credential-boundary.py
"""
import unittest
from copy import deepcopy


class Denied(Exception):
    pass


class Target:
    def __init__(self):
        self.ticket = {"id": "synthetic-1", "status": "open"}
        self.online = True

    def call(self, operation, scopes):
        if not self.online:
            raise ConnectionError("target_unavailable")
        if operation not in scopes:
            raise Denied("target_scope_denied")
        if operation == "ticket.read":
            return deepcopy(self.ticket)
        if operation == "ticket.close":
            self.ticket["status"] = "closed"
            return {"result": "closed"}
        raise ValueError("unknown_operation")


class Gateway:
    def __init__(self, target, exposed, credential_scopes):
        self.target = target
        self.exposed = frozenset(exposed)
        self.credential_scopes = frozenset(credential_scopes)

    def call(self, operation):
        if operation not in self.exposed:
            raise Denied("gateway_tool_denied")
        return self.target.call(operation, self.credential_scopes)


def probe(gateway, observe):
    """Observer represents separate test-harness access, never agent authority."""
    before = observe()
    # A failed positive control raises; it must not be recorded as a safe denial.
    read = gateway.call("ticket.read")
    if read != before:
        raise AssertionError("read_did_not_match_observed_target")
    try:
        gateway.call("ticket.close")
    except Denied as exc:
        denial = str(exc)
    else:
        denial = None
    after = observe()
    return {"read_allowed": True, "denial": denial,
            "unchanged": before == after,
            "passed": denial is not None and before == after}


READ = {"ticket.read"}
BOTH = {"ticket.read", "ticket.close"}


class BoundaryTests(unittest.TestCase):
    def setUp(self):
        self.target = Target()
        self.observe = lambda: deepcopy(self.target.ticket)

    def test_read_only_tool_blocks_broad_credential_path(self):
        result = probe(Gateway(self.target, READ, BOTH), self.observe)
        self.assertTrue(result["passed"])
        self.assertEqual(result["denial"], "gateway_tool_denied")

    def test_exposed_write_with_broad_credential_mutates(self):
        result = probe(Gateway(self.target, BOTH, BOTH), self.observe)
        self.assertFalse(result["passed"])
        self.assertIsNone(result["denial"])
        self.assertEqual(self.target.ticket["status"], "closed")

    def test_target_scope_denies_exposed_write(self):
        result = probe(Gateway(self.target, BOTH, READ), self.observe)
        self.assertTrue(result["passed"])
        self.assertEqual(result["denial"], "target_scope_denied")

    def test_both_boundaries_narrow(self):
        self.assertTrue(probe(Gateway(self.target, READ, READ), self.observe)["passed"])

    def test_offline_target_is_not_a_permission_pass(self):
        self.target.online = False
        with self.assertRaises(ConnectionError):
            probe(Gateway(self.target, BOTH, READ), self.observe)

    def test_denial_after_effect_is_not_a_pass(self):
        class BrokenGateway:
            def call(inner, operation):
                result = self.target.call(operation, BOTH)
                if operation == "ticket.close":
                    raise Denied("too_late")
                return result
        result = probe(BrokenGateway(), self.observe)
        self.assertFalse(result["passed"])
        self.assertFalse(result["unchanged"])


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