# Copyright (C) 2026 Garudex Labs.  All Rights Reserved.
# Caracal, a product of Garudex Labs
#
# Tests for caracalai_core.audit client signing, persistence, drops, and replay.

import shutil
import tempfile
import time
import unittest
from pathlib import Path

from caracalai_core.audit import AuditClient, AuditEvent
from caracalai_core.logging import create_logger


class FakeStreamer:
    def __init__(self):
        self.calls: list[tuple[str, dict]] = []
        self.fail_next = 0

    def xadd(self, stream, fields):
        if self.fail_next > 0:
            self.fail_next -= 1
            raise RuntimeError("redis down")
        self.calls.append((stream, dict(fields)))
        return "1-0"


def _event(id_: str = "ev-1") -> AuditEvent:
    return AuditEvent(
        id=id_,
        zone_id="z1",
        event_type="authorization_decision",
        request_id="r1",
        decision="allow",
        evaluation_status="success",
        determining_policies_json=[],
        diagnostics_json={},
        occurred_at="2026-01-01T00:00:00Z",
    )


class AuditClientTests(unittest.TestCase):
    def setUp(self) -> None:
        self.tmp = Path(tempfile.mkdtemp(prefix="caracal-audit-"))
        self.replay_dir = self.tmp / "replay"
        self.replay_dir.mkdir()
        self.logger = create_logger("audit-test", "fatal")

    def tearDown(self) -> None:
        shutil.rmtree(self.tmp, ignore_errors=True)

    def test_requires_hmac_in_production(self):
        with self.assertRaisesRegex(ValueError, "audit_hmac_key is required"):
            AuditClient(streamer=FakeStreamer(), replay_dir=self.replay_dir, logger=self.logger, production=True)

    def test_rejects_short_audit_hmac_key(self):
        with self.assertRaisesRegex(ValueError, "at least 32 bytes"):
            AuditClient(streamer=FakeStreamer(), replay_dir=self.replay_dir, logger=self.logger, audit_hmac_key=b"short")

    def test_signs_events_when_key_present(self):
        s = FakeStreamer()
        c = AuditClient(streamer=s, replay_dir=self.replay_dir, logger=self.logger, audit_hmac_key=b"k" * 32, flush_ttl_ms=10)
        c.start()
        c.emit(_event())
        time.sleep(0.05)
        c.close()
        self.assertEqual(len(s.calls), 1)
        fields = s.calls[0][1]
        self.assertIn("sig", fields)
        self.assertEqual(len(fields["sig"]), 64)

    def test_persists_on_sink_failure(self):
        s = FakeStreamer()
        s.fail_next = 100
        c = AuditClient(streamer=s, replay_dir=self.replay_dir, logger=self.logger, flush_ttl_ms=10)
        c.start()
        c.emit(_event())
        time.sleep(0.05)
        c.close()
        self.assertTrue(list(self.replay_dir.glob("*.ndjson")))

    def test_drops_on_overflow(self):
        s = FakeStreamer()
        s.fail_next = 1_000_000
        c = AuditClient(
            streamer=s, replay_dir=self.replay_dir, logger=self.logger,
            buffer_cap=2, flush_batch=1_000_000, flush_ttl_ms=1_000_000,
        )
        c.start()
        for _ in range(10):
            c.emit(_event())
        self.assertGreater(c.dropped(), 0)
        c.close()

    def test_replays_persisted_on_start(self):
        s1 = FakeStreamer()
        s1.fail_next = 100
        c1 = AuditClient(streamer=s1, replay_dir=self.replay_dir, logger=self.logger, flush_ttl_ms=10)
        c1.start()
        c1.emit(_event())
        time.sleep(0.05)
        c1.close()
        self.assertEqual(len(list(self.replay_dir.glob("*.ndjson"))), 1)

        s2 = FakeStreamer()
        c2 = AuditClient(streamer=s2, replay_dir=self.replay_dir, logger=self.logger, flush_ttl_ms=10)
        c2.start()
        c2.close()
        self.assertEqual(len(s2.calls), 1)
        self.assertEqual(list(self.replay_dir.glob("*.ndjson")), [])


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

