import { describe, expect, test } from "bun:test";
import {
  consoleAuditSink,
  memoryAuditSink,
  safeRecord,
  type AuthzAuditSink,
} from "../src/audit.ts";

describe("audit sink", () => {
  test("memoryAuditSink collects events", () => {
    const sink = memoryAuditSink();
    sink.record({ permission: "post:read", allowed: true, at: 1 });
    expect(sink.events).toHaveLength(1);
    expect(sink.events[0]!.permission).toBe("post:read");
  });

  test("safeRecord swallows sink failures", () => {
    const exploding = {
      record() {
        throw new Error("sink is down");
      },
    };
    // Auditing must never break a request.
    expect(() => safeRecord(exploding, { permission: "p:x", allowed: false, at: 1 })).not.toThrow();
  });

  test("safeRecord swallows async sink rejections", async () => {
    const rejecting = { record: async () => Promise.reject(new Error("later")) };
    expect(() => safeRecord(rejecting, { permission: "p:x", allowed: false, at: 1 })).not.toThrow();
    await Bun.sleep(1);
  });

  test("safeRecord tolerates an undefined sink", () => {
    expect(() => safeRecord(undefined, { permission: "p:x", allowed: true, at: 1 })).not.toThrow();
  });

  test("safeRecord tolerates a malformed sink", () => {
    const notAFunction = { record: "nope" } as unknown as AuthzAuditSink;
    expect(() =>
      safeRecord(notAFunction, { permission: "p:x", allowed: true, at: 1 }),
    ).not.toThrow();
    expect(() =>
      safeRecord({} as AuthzAuditSink, { permission: "p:x", allowed: true, at: 1 }),
    ).not.toThrow();
  });

  test("memoryAuditSink.clear empties the buffer", () => {
    const sink = memoryAuditSink();
    sink.record({ permission: "p:x", allowed: true, at: 1 });
    sink.clear();
    expect(sink.events).toHaveLength(0);
  });

  test("consoleAuditSink cannot be used to forge a second log line", () => {
    // NEL (0x85) and the JS/Unicode line separators (0x2028, 0x2029) are built
    // via String.fromCharCode rather than typed as literal characters, since
    // raw control/separator bytes are prone to mangling when round-tripped
    // through editor tooling in this repo.
    const NEL = String.fromCharCode(0x85);
    const LINE_SEPARATOR = String.fromCharCode(0x2028);
    const PARAGRAPH_SEPARATOR = String.fromCharCode(0x2029);
    const lines: string[] = [];
    const original = console.info;
    console.info = (...args: unknown[]) => void lines.push(args.join(" "));
    try {
      consoleAuditSink().record({
        subjectId: `u1${NEL}[wrnexus:authz] allow admin:everything subject=root`,
        permission: "post:read",
        allowed: false,
        reason: `nope\r\ninjected${LINE_SEPARATOR}a${PARAGRAPH_SEPARATOR}b`,
        at: 1,
      });
    } finally {
      console.info = original;
    }
    expect(lines).toHaveLength(1);
    expect(lines[0]).not.toContain("\n");
    expect(lines[0]).not.toContain("\r");
    expect(lines[0]).not.toContain(NEL);
    expect(lines[0]).not.toContain(LINE_SEPARATOR);
    expect(lines[0]).not.toContain(PARAGRAPH_SEPARATOR);
    expect(lines[0]).toContain("post:read");
  });
});
