"""Integrität: Kontext nach der Freigabe verändert (Prompt, Memory, Anhang je einzeln)."""
import hashlib
import tempfile
import unittest
from dataclasses import replace
from pathlib import Path

from aria_router.adapter_contract import ResultStatus
from aria_router.adapters import MockAdapter
from aria_router.claude_channel import ClaudeChannelAdapter
from aria_router.codex_adapter import CodexAdapter
from aria_router.context import Attachment, ContextEnvelope, MemorySnippet, canonical_json
from aria_router.core import PrivacyMatrix, PrivacyTier
from aria_router.runner import MockRunner

from tests.helpers import AdapterHarness


class ContextIntegrityTests(unittest.TestCase):
    def setUp(self):
        self._tmp = tempfile.TemporaryDirectory()
        self.h = AdapterHarness(Path(self._tmp.name))
        self.adapter = MockAdapter(self.h.verifier, "claude")
        self.memory = (MemorySnippet("brain/notiz.md", "Notiz eins", PrivacyTier.PUBLIC),)
        self.attachment = Attachment(kind="document", file_id="BQAC1", name="a.txt", mime="text/plain",
                                     data=b"inhalt")
        self.job = self.h.job("prüfe das", memory=self.memory, attachments=[self.attachment])
        self.clearance = self.h.clear(self.adapter, self.job)

    def tearDown(self):
        self._tmp.cleanup()

    def tampered(self, **changes):
        return replace(self.job, envelope=replace(self.job.envelope, **changes))

    def assertMismatch(self, job):
        result = self.adapter.execute(self.clearance, job)
        self.assertEqual(result.status, ResultStatus.BLOCKED)
        self.assertEqual(result.error_code, "CONTEXT_DIGEST_MISMATCH")
        self.assertEqual(self.adapter.calls, 0)

    def test_unchanged_context_passes(self):
        self.assertEqual(self.adapter.execute(self.clearance, self.job).status, ResultStatus.SUCCESS)

    def test_prompt_changed_after_clearance(self):
        self.assertMismatch(self.tampered(prompt="prüfe das bitte"))

    def test_memory_changed_after_clearance(self):
        self.assertMismatch(self.tampered(memory=(MemorySnippet("brain/notiz.md", "Notiz zwei", PrivacyTier.PUBLIC),)))

    def test_memory_added_after_clearance(self):
        self.assertMismatch(self.tampered(memory=self.memory + (MemorySnippet("brain/b.md", "mehr"),)))

    def test_attachment_bytes_changed_after_clearance(self):
        self.assertMismatch(self.tampered(attachments=(replace(self.attachment, data=b"anders"),)))

    def test_attachment_metadata_changed_after_clearance(self):
        self.assertMismatch(self.tampered(attachments=(replace(self.attachment, name="b.txt"),)))

    def test_attachment_removed_after_clearance(self):
        self.assertMismatch(self.tampered(attachments=()))

    def test_claude_meta_changed_after_clearance(self):
        adapter = ClaudeChannelAdapter(self.h.verifier)
        job = self.h.job("hallo")
        clearance = self.h.clear(adapter, job)
        forged = replace(job, sender_username="jemand_anders")
        result = adapter.execute(clearance, forged)
        self.assertEqual(result.error_code, "CONTEXT_DIGEST_MISMATCH")

    def test_codex_outbound_covers_stdin_and_files(self):
        matrix = PrivacyMatrix.from_mapping({"codex": {PrivacyTier.UNKNOWN, PrivacyTier.PUBLIC}})
        harness = AdapterHarness(Path(self._tmp.name) / "codex", matrix=matrix)
        runner = MockRunner()
        adapter = CodexAdapter(harness.verifier, runner=runner)
        job = harness.job("#public prüfe", memory=self.memory, attachments=[self.attachment])
        result = adapter.execute(harness.clear(adapter, job), job)
        self.assertEqual(result.status, ResultStatus.SUCCESS)
        spec = runner.calls[0]
        self.assertEqual(spec.files, (("01-a.txt", b"inhalt"),))
        self.assertEqual(spec.stdin.decode(), "#public prüfe\n\n[Aria-Kontext: Memory-Ausschnitte]\n"
                                              "- (brain/notiz.md) Notiz eins\n\n[Anhänge im Arbeitsverzeichnis]\n"
                                              "- attachments/01-a.txt")
        tampered = replace(job, envelope=replace(job.envelope, attachments=(replace(self.attachment, data=b"x"),)))
        second = adapter.execute(harness.clear(adapter, job), tampered)
        self.assertEqual(second.error_code, "CONTEXT_DIGEST_MISMATCH")
        self.assertEqual(len(runner.calls), 1)

    def test_canonical_serialization_stable(self):
        a = ContextEnvelope("prüfe", self.memory, (self.attachment,))
        b = ContextEnvelope("prüfe", (MemorySnippet("brain/notiz.md", "Notiz eins", PrivacyTier.PUBLIC),),
                            (Attachment(kind="document", file_id="BQAC1", name="a.txt", mime="text/plain",
                                        data=b"inhalt"),))
        self.assertEqual(a.digest(), b.digest())
        self.assertIn("prüfe".encode("utf-8"), a.canonical())
        self.assertEqual(canonical_json({"b": 1, "a": 2}), canonical_json({"a": 2, "b": 1}))

    def test_attachment_hash_from_bytes(self):
        self.assertEqual(self.attachment.content_sha256(), hashlib.sha256(b"inhalt").hexdigest())
        with self.assertRaises(ValueError) as ctx:
            Attachment(kind="document", file_id="x", data=b"inhalt", sha256=hashlib.sha256(b"x").hexdigest())
        self.assertEqual(str(ctx.exception), "ATTACHMENT_HASH_MISMATCH")

    def test_mutable_bytes_rejected(self):
        with self.assertRaises(ValueError):
            Attachment(kind="document", file_id="x", data=bytearray(b"inhalt"))

    def test_digest_is_integrity_not_authorization(self):
        # Wer den Digest kennt, kann damit nichts ausführen: ohne Signatur keine Freigabe.
        forged = replace(self.clearance, mac="0" * 64)
        result = self.adapter.execute(forged, self.job)
        self.assertEqual(result.error_code, "INVALID_SIGNATURE")
        self.assertEqual(self.adapter.calls, 0)


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