import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { AgentGrantStore } from "./grant-store.js";
import { GrantManager } from "./grant-manager.js";
import { rmSync, existsSync } from "fs";
import { join } from "path";
import { tmpdir } from "os";
import { randomBytes } from "crypto";

function tmpPath(): string {
  return join(tmpdir(), `mailpouch-grant-mgr-${randomBytes(6).toString("hex")}.json`);
}

describe("GrantManager.check", () => {
  let path: string;
  let store: AgentGrantStore;
  let mgr: GrantManager;

  beforeEach(() => {
    path = tmpPath();
    store = new AgentGrantStore(path);
    mgr = new GrantManager(store);
  });

  afterEach(() => { if (existsSync(path)) rmSync(path, { force: true }); });

  it("denies when no grant exists", () => {
    const r = mgr.check({ clientId: "pmc_unknown", tool: "get_emails", globalPreset: "full" });
    expect(r.allowed).toBe(false);
    expect(r.reason).toMatch(/no grant registered/i);
  });

  it("denies a pending grant with a clear message", () => {
    store.createPending({ clientId: "pmc_1", clientName: "A" });
    const r = mgr.check({ clientId: "pmc_1", tool: "get_emails", globalPreset: "full" });
    expect(r.allowed).toBe(false);
    expect(r.reason).toMatch(/pending user approval/i);
  });

  it("denies a revoked grant", () => {
    store.createPending({ clientId: "pmc_1", clientName: "A" });
    store.deny("pmc_1");
    const r = mgr.check({ clientId: "pmc_1", tool: "get_emails", globalPreset: "full" });
    expect(r.allowed).toBe(false);
    expect(r.reason).toMatch(/revoked/i);
  });

  it("allows an active grant for a tool inside the effective preset", () => {
    store.createPending({ clientId: "pmc_1", clientName: "A" });
    store.approve({ clientId: "pmc_1", preset: "full" });
    const r = mgr.check({ clientId: "pmc_1", tool: "get_emails", globalPreset: "full" });
    expect(r.allowed).toBe(true);
    expect(r.effectivePreset).toBe("full");
  });

  it("intersects grant preset with global preset (global wins when stricter)", () => {
    store.createPending({ clientId: "pmc_1", clientName: "A" });
    store.approve({ clientId: "pmc_1", preset: "full" });
    // Global preset is read_only — delete_email is not in read_only.
    const r = mgr.check({ clientId: "pmc_1", tool: "delete_email", globalPreset: "read_only" });
    expect(r.allowed).toBe(false);
  });

  it("honors explicit tool allow override even when preset would deny", () => {
    store.createPending({ clientId: "pmc_1", clientName: "A" });
    store.approve({
      clientId: "pmc_1",
      preset: "read_only",
      toolOverrides: { send_email: true },
    });
    const r = mgr.check({ clientId: "pmc_1", tool: "send_email", globalPreset: "full" });
    expect(r.allowed).toBe(true);
  });

  it("refuses a tool allow override when the global preset does not permit the tool", () => {
    store.createPending({ clientId: "pmc_1", clientName: "A" });
    store.approve({
      clientId: "pmc_1",
      preset: "full",
      toolOverrides: { delete_email: true },
    });
    const r = mgr.check({ clientId: "pmc_1", tool: "delete_email", globalPreset: "read_only" });
    expect(r.allowed).toBe(false);
    expect(r.reason).toMatch(/global preset/i);
  });

  it("honors explicit tool deny override even when preset would allow", () => {
    store.createPending({ clientId: "pmc_1", clientName: "A" });
    store.approve({
      clientId: "pmc_1",
      preset: "full",
      toolOverrides: { delete_email: false },
    });
    const r = mgr.check({ clientId: "pmc_1", tool: "delete_email", globalPreset: "full" });
    expect(r.allowed).toBe(false);
    expect(r.reason).toMatch(/explicitly denied/i);
  });

  it("auto-expires a grant whose expiresAt has passed", () => {
    store.createPending({ clientId: "pmc_1", clientName: "A" });
    store.approve({
      clientId: "pmc_1",
      preset: "read_only",
      conditions: { expiresAt: new Date(Date.now() - 1_000).toISOString() },
    });
    const r = mgr.check({ clientId: "pmc_1", tool: "get_emails", globalPreset: "full" });
    expect(r.allowed).toBe(false);
    expect(r.reason).toMatch(/expired/i);
    expect(store.get("pmc_1")?.status).toBe("expired");
  });

  it("honors IP pins: allow matching, deny mismatched", () => {
    store.createPending({ clientId: "pmc_1", clientName: "A" });
    store.approve({
      clientId: "pmc_1",
      preset: "read_only",
      conditions: { ipPins: ["10.0.0.5"] },
    });
    expect(
      mgr.check({ clientId: "pmc_1", tool: "get_emails", globalPreset: "full", callerIp: "10.0.0.5" }).allowed,
    ).toBe(true);
    expect(
      mgr.check({ clientId: "pmc_1", tool: "get_emails", globalPreset: "full", callerIp: "10.0.0.6" }).allowed,
    ).toBe(false);
  });

  it("enforces folderAllowlist against the call's folder arg", () => {
    store.createPending({ clientId: "pmc_1", clientName: "A" });
    store.approve({
      clientId: "pmc_1",
      preset: "read_only",
      conditions: { folderAllowlist: ["INBOX", "Sent"] },
    });
    expect(
      mgr.check({
        clientId: "pmc_1", tool: "get_emails", globalPreset: "full",
        args: { folder: "INBOX" },
      }).allowed,
    ).toBe(true);
    expect(
      mgr.check({
        clientId: "pmc_1", tool: "get_emails", globalPreset: "full",
        args: { folder: "Secret" },
      }).allowed,
    ).toBe(false);
  });

  it("skips folder check when the call has no folder-like arg (tool not folder-scoped)", () => {
    store.createPending({ clientId: "pmc_1", clientName: "A" });
    store.approve({
      clientId: "pmc_1",
      preset: "read_only",
      conditions: { folderAllowlist: ["INBOX"] },
    });
    const r = mgr.check({ clientId: "pmc_1", tool: "get_connection_status", globalPreset: "full" });
    expect(r.allowed).toBe(true);
  });

  // ── PERM-011: folder allowlist bypass via email-ID-scoped mutators ──────────
  it("enforces folderAllowlist against an email-ID-scoped tool's sourceFolder arg", () => {
    store.createPending({ clientId: "pmc_1", clientName: "A" });
    store.approve({
      clientId: "pmc_1",
      preset: "full",
      conditions: { folderAllowlist: ["INBOX"] },
    });
    // delete_email targeting a UID in Archive must be blocked when the grant
    // is pinned to INBOX — the folder is carried by `sourceFolder`.
    expect(
      mgr.check({
        clientId: "pmc_1", tool: "delete_email", globalPreset: "full",
        args: { emailId: "42", sourceFolder: "Archive" },
      }).allowed,
    ).toBe(false);
    // Same tool against an allowed folder still passes.
    expect(
      mgr.check({
        clientId: "pmc_1", tool: "delete_email", globalPreset: "full",
        args: { emailId: "42", sourceFolder: "INBOX" },
      }).allowed,
    ).toBe(true);
  });

  it("fails closed for an email-ID-scoped tool with no sourceFolder when allowlisted", () => {
    store.createPending({ clientId: "pmc_1", clientName: "A" });
    store.approve({
      clientId: "pmc_1",
      preset: "full",
      conditions: { folderAllowlist: ["INBOX"] },
    });
    const r = mgr.check({
      clientId: "pmc_1", tool: "delete_email", globalPreset: "full",
      args: { emailId: "42" },
    });
    expect(r.allowed).toBe(false);
  });

  it("leaves get_thread/download_attachment folder-agnostic (gating them here would be false enforcement)", () => {
    // PERM-011 residual: their services don't honor a folder constraint
    // (get_thread assembles INBOX+Sent; download_attachment passes no folder),
    // so they are deliberately NOT gated at this layer — gating would be theatre
    // and download_attachment would fail-closed for every restricted grant.
    store.createPending({ clientId: "pmc_1", clientName: "A" });
    store.approve({ clientId: "pmc_1", preset: "full", conditions: { folderAllowlist: ["INBOX"] } });
    for (const tool of ["get_thread", "download_attachment"]) {
      expect(
        mgr.check({ clientId: "pmc_1", tool, globalPreset: "full", args: { emailId: "42" } }).allowed,
      ).toBe(true);
    }
  });

  // ── PERM-013: custom preset must not widen via intersection ─────────────────
  it("does not let a custom grant preset inherit another preset's enabled map", () => {
    store.createPending({ clientId: "pmc_1", clientName: "A" });
    // grant preset = custom, with delete_email explicitly disabled. The global
    // preset (supervised) DOES allow delete_email. The old rank table treated
    // custom == full, so intersectPresets returned "supervised" and the gate
    // consulted supervised's map — re-enabling the tool the user disabled.
    store.approve({
      clientId: "pmc_1",
      preset: "custom",
      toolOverrides: { delete_email: false },
    });
    const r = mgr.check({ clientId: "pmc_1", tool: "delete_email", globalPreset: "supervised" });
    expect(r.allowed).toBe(false);
  });

  it("a custom grant only allows tools it explicitly overrides to true", () => {
    store.createPending({ clientId: "pmc_1", clientName: "A" });
    store.approve({
      clientId: "pmc_1",
      preset: "custom",
      toolOverrides: { get_emails: true },
    });
    // Explicitly enabled, and within the global ceiling.
    expect(
      mgr.check({ clientId: "pmc_1", tool: "get_emails", globalPreset: "full" }).allowed,
    ).toBe(true);
    // Not overridden → default-deny under custom (no preset map to fall back on).
    expect(
      mgr.check({ clientId: "pmc_1", tool: "send_email", globalPreset: "full" }).allowed,
    ).toBe(false);
  });
});
