/**
 * @file Tests for subagent tool-event attribution.
 *
 * Subagent tool calls (Read, Bash, Edit, etc.) never fire hooks on the
 * parent session — they only show up in the subagent's own JSONL file.
 * Without dedicated extraction, every subagent ends up with at most a
 * single spawn event, leaving 561/561 historical subagents with 0–5
 * events instead of the dozens-to-hundreds they actually performed.
 *
 * This suite verifies that:
 *   1. parseSubagentFile pairs tool_use blocks with their tool_result
 *      counterparts and surfaces them as `toolEvents`.
 *   2. importSubagentFromJsonl emits PreToolUse + PostToolUse events
 *      under the subagent's own `agent_id`, so the UI attributes them
 *      to the subagent rather than the main agent.
 *   3. Re-running the import is idempotent — no duplicate event rows.
 *   4. When a live subagent (created via PreToolUse "Agent" hook) matches
 *      the JSONL by type + start time, events attach to the live row
 *      instead of creating a duplicate JSONL-keyed row.
 *
 * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
 */

const { describe, it, before, after } = require("node:test");
const assert = require("node:assert/strict");
const path = require("path");
const fs = require("fs");
const os = require("os");

const TEST_DB = path.join(os.tmpdir(), `dashboard-subagent-test-${Date.now()}-${process.pid}.db`);
process.env.DASHBOARD_DB_PATH = TEST_DB;

const dbModule = require("../db");
const { db, stmts } = dbModule;
const importHistory = require("../../scripts/import-history");
const { calculateCost } = require("../routes/pricing");

after(() => {
  if (db) db.close();
  for (const suffix of ["", "-wal", "-shm"]) {
    try {
      fs.unlinkSync(TEST_DB + suffix);
    } catch {
      /* ignore */
    }
  }
});

// ── Fixture helpers ──────────────────────────────────────────────────

function writeSubagentJsonl(filePath, lines) {
  fs.writeFileSync(filePath, lines.map((o) => JSON.stringify(o)).join("\n"));
}

/**
 * Builds a minimal subagent JSONL with two tool calls — one Read with a
 * paired tool_result, and one Bash with a paired error tool_result.
 */
function buildSubagentLines(agentType = "coder") {
  return [
    {
      type: "user",
      timestamp: "2026-04-28T10:00:00.000Z",
      message: { content: [{ type: "text", text: "Investigate the bug" }] },
    },
    {
      type: "assistant",
      timestamp: "2026-04-28T10:00:01.000Z",
      message: {
        model: "claude-opus-4-7",
        content: [
          {
            type: "tool_use",
            id: "toolu_read_001",
            name: "Read",
            input: { file_path: "/tmp/foo.py" },
          },
        ],
        usage: {
          input_tokens: 50,
          output_tokens: 20,
          cache_read_input_tokens: 0,
          cache_creation_input_tokens: 0,
        },
      },
    },
    {
      type: "user",
      timestamp: "2026-04-28T10:00:02.000Z",
      message: {
        content: [
          {
            type: "tool_result",
            tool_use_id: "toolu_read_001",
            content: "def main():\n    pass\n",
          },
        ],
      },
    },
    {
      type: "assistant",
      timestamp: "2026-04-28T10:00:03.000Z",
      message: {
        model: "claude-opus-4-7",
        content: [
          {
            type: "tool_use",
            id: "toolu_bash_002",
            name: "Bash",
            input: { command: "ls /tmp" },
          },
        ],
      },
    },
    {
      type: "user",
      timestamp: "2026-04-28T10:00:04.000Z",
      message: {
        content: [
          {
            type: "tool_result",
            tool_use_id: "toolu_bash_002",
            content: "ls: cannot access /tmp: not allowed",
            is_error: true,
          },
        ],
      },
    },
    {
      // Meta: keeps file timestamps coherent
      type: "user",
      timestamp: "2026-04-28T10:00:05.000Z",
      message: { content: [{ type: "text", text: "done" }] },
    },
  ].map((line, i) => {
    // Inject agentType into one entry as a hint, mirroring real CC output
    if (i === 0) line.agentType = agentType;
    return line;
  });
}

function writeMetaJson(filePath, agentType) {
  fs.writeFileSync(filePath, JSON.stringify({ agentType }));
}

/**
 * Minimal subagent JSONL on a specific model carrying one usage record, so
 * parseSubagentFile yields a single token bucket keyed by that model.
 */
function buildModelSubLines(agentType, model, usage, startedAt = "2026-05-01T10:00:00.000Z") {
  const t0 = startedAt;
  return [
    {
      type: "user",
      timestamp: t0,
      agentType,
      message: { content: [{ type: "text", text: "go" }] },
    },
    {
      type: "assistant",
      timestamp: "2026-05-01T10:00:01.000Z",
      message: {
        model,
        content: [{ type: "tool_use", id: `toolu_${agentType}_1`, name: "Read", input: {} }],
        usage: {
          input_tokens: usage.input || 0,
          output_tokens: usage.output || 0,
          cache_read_input_tokens: usage.cacheRead || 0,
          cache_creation_input_tokens: usage.cacheWrite || 0,
        },
      },
    },
    {
      type: "user",
      timestamp: "2026-05-01T10:00:02.000Z",
      message: {
        content: [{ type: "tool_result", tool_use_id: `toolu_${agentType}_1`, content: "ok" }],
      },
    },
  ];
}

// Lay out <base>/<sessionId>/subagents/agent-*.jsonl and return the matching
// transcriptPath (<base>/<sessionId>.jsonl) that scanAndImportSubagents expects.
function buildSubagentDir(sessionId, files) {
  const base = fs.mkdtempSync(path.join(os.tmpdir(), `sa-scan-${process.pid}-`));
  const subDir = path.join(base, sessionId, "subagents");
  fs.mkdirSync(subDir, { recursive: true });
  for (const f of files) {
    writeSubagentJsonl(path.join(subDir, `agent-${f.id}.jsonl`), f.lines);
    // Companion meta.json so parseSubagentFile resolves agentType (the live-match key).
    if (f.agentType) {
      fs.writeFileSync(
        path.join(subDir, `agent-${f.id}.meta.json`),
        JSON.stringify({ agentType: f.agentType })
      );
    }
  }
  return { transcriptPath: path.join(base, `${sessionId}.jsonl`), base };
}

// ── Tests ────────────────────────────────────────────────────────────

describe("parseSubagentFile — tool event extraction", () => {
  it("pairs tool_use with tool_result and returns ordered toolEvents", async () => {
    const tmpFile = path.join(os.tmpdir(), `agent-${Date.now()}-${process.pid}.jsonl`);
    writeSubagentJsonl(tmpFile, buildSubagentLines("coder"));
    writeMetaJson(tmpFile.replace(/\.jsonl$/, ".meta.json"), "coder");

    try {
      const data = await importHistory.parseSubagentFile(tmpFile);
      assert.ok(data, "subagent data should parse");
      assert.equal(data.agentType, "coder");
      assert.ok(Array.isArray(data.toolEvents));
      assert.equal(data.toolEvents.length, 2);

      const [readEv, bashEv] = data.toolEvents;
      assert.equal(readEv.tool_use_id, "toolu_read_001");
      assert.equal(readEv.tool_name, "Read");
      assert.deepEqual(readEv.tool_input, { file_path: "/tmp/foo.py" });
      assert.equal(readEv.is_error, false);
      assert.equal(typeof readEv.pre_timestamp, "string");
      assert.equal(typeof readEv.post_timestamp, "string");
      assert.ok(readEv.tool_response);

      assert.equal(bashEv.tool_use_id, "toolu_bash_002");
      assert.equal(bashEv.is_error, true);
    } finally {
      fs.unlinkSync(tmpFile);
      try {
        fs.unlinkSync(tmpFile.replace(/\.jsonl$/, ".meta.json"));
      } catch {
        /* ignore */
      }
    }
  });

  it("emits a tool_use even when no matching tool_result exists yet (live tail)", async () => {
    const tmpFile = path.join(os.tmpdir(), `agent-tail-${Date.now()}-${process.pid}.jsonl`);
    writeSubagentJsonl(tmpFile, [
      {
        type: "assistant",
        timestamp: "2026-04-28T10:00:01.000Z",
        message: {
          model: "claude-opus-4-7",
          content: [{ type: "tool_use", id: "toolu_pending", name: "Read", input: {} }],
        },
      },
    ]);

    try {
      const data = await importHistory.parseSubagentFile(tmpFile);
      assert.equal(data.toolEvents.length, 1);
      const ev = data.toolEvents[0];
      assert.equal(ev.tool_use_id, "toolu_pending");
      assert.equal(ev.post_timestamp, null);
      assert.equal(ev.tool_response, null);
    } finally {
      fs.unlinkSync(tmpFile);
    }
  });
});

describe("importSubagentFromJsonl — event attribution", () => {
  const sessionId = "test-sess-attribution";
  const mainAgentId = `${sessionId}-main`;

  before(() => {
    // Seed session + main agent so importSubagentFromJsonl has parents to point at.
    stmts.insertSession.run(sessionId, "Test Session", "active", "/tmp", null, null);
    stmts.insertAgent.run(
      mainAgentId,
      sessionId,
      "Main Agent",
      "main",
      null,
      "waiting",
      null,
      null,
      null
    );
  });

  it("creates one subagent row and per-call PreToolUse + PostToolUse events", async () => {
    const tmpFile = path.join(os.tmpdir(), `agent-attr-${Date.now()}-${process.pid}.jsonl`);
    writeSubagentJsonl(tmpFile, buildSubagentLines("coder"));
    writeMetaJson(tmpFile.replace(/\.jsonl$/, ".meta.json"), "coder");

    try {
      const data = await importHistory.parseSubagentFile(tmpFile);
      const created = importHistory.importSubagentFromJsonl(dbModule, sessionId, mainAgentId, data);
      assert.ok(created > 0, "should create at least the agent + spawn + 4 events");

      const subId = `${sessionId}-jsonl-${data.agentId}`;
      const subAgent = stmts.getAgent.get(subId);
      assert.ok(subAgent, "JSONL-keyed subagent row should exist");
      assert.equal(subAgent.parent_agent_id, mainAgentId);

      const toolEvents = db
        .prepare(
          "SELECT event_type, tool_name FROM events WHERE agent_id = ? AND event_type IN ('PreToolUse', 'PostToolUse') ORDER BY id ASC"
        )
        .all(subId);
      assert.equal(toolEvents.length, 4, "expected 2 Pre + 2 Post events under subagent's id");
      assert.deepEqual(
        toolEvents.map((e) => `${e.event_type}:${e.tool_name}`),
        ["PreToolUse:Read", "PostToolUse:Read", "PreToolUse:Bash", "PostToolUse:Bash"]
      );

      // Spawn marker lives under the main agent so the parent chain shows
      // "Subagent spawned: coder" alongside main's other actions.
      const spawnEvents = db
        .prepare(
          "SELECT 1 FROM events WHERE agent_id = ? AND event_type = 'PreToolUse' AND tool_name = 'Agent'"
        )
        .all(mainAgentId);
      assert.equal(spawnEvents.length, 1);
    } finally {
      fs.unlinkSync(tmpFile);
      try {
        fs.unlinkSync(tmpFile.replace(/\.jsonl$/, ".meta.json"));
      } catch {
        /* ignore */
      }
    }
  });

  it("is idempotent — re-running does not duplicate events", async () => {
    const tmpFile = path.join(os.tmpdir(), `agent-idem-${Date.now()}-${process.pid}.jsonl`);
    writeSubagentJsonl(tmpFile, buildSubagentLines("reviewer"));
    writeMetaJson(tmpFile.replace(/\.jsonl$/, ".meta.json"), "reviewer");

    try {
      const data = await importHistory.parseSubagentFile(tmpFile);
      importHistory.importSubagentFromJsonl(dbModule, sessionId, mainAgentId, data);
      const subId = `${sessionId}-jsonl-${data.agentId}`;
      const before = db.prepare("SELECT COUNT(*) AS c FROM events WHERE agent_id = ?").get(subId).c;

      // Second run — should be a no-op.
      importHistory.importSubagentFromJsonl(dbModule, sessionId, mainAgentId, data);
      const after = db.prepare("SELECT COUNT(*) AS c FROM events WHERE agent_id = ?").get(subId).c;

      assert.equal(after, before, "idempotent re-import — no new rows");
    } finally {
      fs.unlinkSync(tmpFile);
      try {
        fs.unlinkSync(tmpFile.replace(/\.jsonl$/, ".meta.json"));
      } catch {
        /* ignore */
      }
    }
  });

  it("merges into a live subagent when one matches — no JSONL-keyed duplicate row", async () => {
    // Simulate a live PreToolUse Agent hook having pre-created a subagent row.
    const liveSubId = "live-uuid-xyz";
    const startedAt = "2026-04-28T10:00:00.000Z";
    stmts.insertAgent.run(
      liveSubId,
      sessionId,
      "Live Coder",
      "subagent",
      "live-coder",
      "completed",
      "task",
      mainAgentId,
      null
    );
    db.prepare("UPDATE agents SET started_at = ?, ended_at = ?, updated_at = ? WHERE id = ?").run(
      startedAt,
      startedAt,
      startedAt,
      liveSubId
    );

    const tmpFile = path.join(os.tmpdir(), `agent-live-${Date.now()}-${process.pid}.jsonl`);
    const lines = buildSubagentLines("live-coder");
    writeSubagentJsonl(tmpFile, lines);
    writeMetaJson(tmpFile.replace(/\.jsonl$/, ".meta.json"), "live-coder");

    try {
      const data = await importHistory.parseSubagentFile(tmpFile);
      importHistory.importSubagentFromJsonl(dbModule, sessionId, mainAgentId, data);

      const jsonlSubId = `${sessionId}-jsonl-${data.agentId}`;
      assert.equal(
        stmts.getAgent.get(jsonlSubId),
        undefined,
        "no JSONL-keyed row when a live subagent absorbed the events"
      );

      const eventsUnderLive = db
        .prepare(
          "SELECT 1 FROM events WHERE agent_id = ? AND event_type IN ('PreToolUse', 'PostToolUse')"
        )
        .all(liveSubId);
      assert.ok(eventsUnderLive.length >= 4, "events should attach to the live subagent's id");
    } finally {
      fs.unlinkSync(tmpFile);
      try {
        fs.unlinkSync(tmpFile.replace(/\.jsonl$/, ".meta.json"));
      } catch {
        /* ignore */
      }
    }
  });
});

// ── Per-subagent model token attribution (issue #185) ──────────────────
describe("scanAndImportSubagents — per-subagent model token attribution", () => {
  it("buckets each subagent's tokens under its OWN model, skipping the parent model", async () => {
    const sessionId = "sess-185-tiered";
    // Orchestrator on Opus; subagents tiered to Sonnet + Haiku, plus one on the
    // SAME model as the parent (Opus) to verify that bucket is intentionally skipped.
    stmts.insertSession.run(sessionId, "Tiered", "active", "/tmp", "claude-opus-4-8", null);
    stmts.insertAgent.run(
      `${sessionId}-main`,
      sessionId,
      "Main",
      "main",
      null,
      "working",
      null,
      null,
      null
    );

    const { transcriptPath, base } = buildSubagentDir(sessionId, [
      {
        id: "hq",
        lines: buildModelSubLines("qa", "claude-haiku-4-5-20251001", { input: 1000, output: 500 }),
      },
      {
        id: "se",
        lines: buildModelSubLines("engineer", "claude-sonnet-4-6", { input: 2000, output: 800 }),
      },
      {
        id: "op",
        lines: buildModelSubLines("planner", "claude-opus-4-8", { input: 9999, output: 9999 }),
      },
    ]);

    try {
      await importHistory.scanAndImportSubagents(dbModule, sessionId, transcriptPath);

      const rows = stmts.getTokensBySession.all(sessionId);
      const byModel = Object.fromEntries(rows.map((r) => [r.model, r]));
      const models = Object.keys(byModel).sort();

      // Haiku + Sonnet buckets are written under their own models.
      assert.deepEqual(models, ["claude-haiku-4-5-20251001", "claude-sonnet-4-6"]);
      assert.equal(byModel["claude-haiku-4-5-20251001"].input_tokens, 1000);
      assert.equal(byModel["claude-haiku-4-5-20251001"].output_tokens, 500);
      assert.equal(byModel["claude-sonnet-4-6"].input_tokens, 2000);
      assert.equal(byModel["claude-sonnet-4-6"].output_tokens, 800);

      // The Opus subagent's tokens are NOT written here — that bucket belongs to
      // the main-transcript writer; double-writing it would inflate via baseline.
      assert.equal(byModel["claude-opus-4-8"], undefined, "parent-model bucket must be skipped");

      // Cost is priced at the real (cheaper) per-subagent models, never Opus.
      const cost = calculateCost(rows, stmts.listPricing.all());
      const costModels = cost.breakdown.map((b) => b.model).sort();
      assert.deepEqual(costModels, ["claude-haiku-4-5-20251001", "claude-sonnet-4-6"]);
      assert.ok(cost.total_cost > 0);

      // Each subagent row records its real model in metadata.
      const hq = stmts.getAgent.get(`${sessionId}-jsonl-hq`);
      assert.equal(JSON.parse(hq.metadata).model, "claude-haiku-4-5-20251001");
      const se = stmts.getAgent.get(`${sessionId}-jsonl-se`);
      assert.equal(JSON.parse(se.metadata).model, "claude-sonnet-4-6");
    } finally {
      fs.rmSync(base, { recursive: true, force: true });
    }
  });

  it("re-running does not inflate token buckets (idempotent per-model write)", async () => {
    const sessionId = "sess-185-idem";
    stmts.insertSession.run(sessionId, "Idem", "active", "/tmp", "claude-opus-4-8", null);
    stmts.insertAgent.run(
      `${sessionId}-main`,
      sessionId,
      "Main",
      "main",
      null,
      "working",
      null,
      null,
      null
    );
    const { transcriptPath, base } = buildSubagentDir(sessionId, [
      {
        id: "hq",
        lines: buildModelSubLines("qa", "claude-haiku-4-5-20251001", { input: 1000, output: 500 }),
      },
    ]);
    try {
      await importHistory.scanAndImportSubagents(dbModule, sessionId, transcriptPath);
      await importHistory.scanAndImportSubagents(dbModule, sessionId, transcriptPath);
      const rows = stmts.getTokensBySession.all(sessionId);
      const haiku = rows.find((r) => r.model === "claude-haiku-4-5-20251001");
      // getTokensBySession already returns effective totals (current + baseline).
      // Re-running must not double them — append-only subagent JSONLs never drop.
      assert.equal(haiku.input_tokens, 1000);
      assert.equal(haiku.output_tokens, 500);
    } finally {
      fs.rmSync(base, { recursive: true, force: true });
    }
  });

  it("skips every model the MAIN transcript used, not just the latest (mid-session /model switch)", async () => {
    const sessionId = "sess-185-switch";
    // Orchestrator switched Opus → Sonnet mid-session; session.model holds the
    // latest (Sonnet), but the main transcript wrote BOTH. A subagent on the
    // earlier Opus must still be skipped to avoid colliding with the main writer.
    stmts.insertSession.run(sessionId, "Switch", "active", "/tmp", "claude-sonnet-4-6", null);
    stmts.insertAgent.run(
      `${sessionId}-main`,
      sessionId,
      "Main",
      "main",
      null,
      "working",
      null,
      null,
      null
    );
    const { transcriptPath, base } = buildSubagentDir(sessionId, [
      {
        id: "op",
        lines: buildModelSubLines("planner", "claude-opus-4-8", { input: 5000, output: 5000 }),
      },
      {
        id: "hq",
        lines: buildModelSubLines("qa", "claude-haiku-4-5-20251001", { input: 100, output: 50 }),
      },
    ]);
    try {
      // parentModels carries BOTH orchestrator models (as hooks.js would pass).
      await importHistory.scanAndImportSubagents(dbModule, sessionId, transcriptPath, {
        parentModels: ["claude-sonnet-4-6", "claude-opus-4-8"],
      });
      const models = stmts.getTokensBySession
        .all(sessionId)
        .map((r) => r.model)
        .sort();
      // Only Haiku is written; both orchestrator models (Sonnet + the earlier
      // Opus) are skipped even though Opus != session.model.
      assert.deepEqual(models, ["claude-haiku-4-5-20251001"]);
    } finally {
      fs.rmSync(base, { recursive: true, force: true });
    }
  });

  it("backfills a live subagent row's model from its own transcript", async () => {
    const sessionId = "sess-185-live";
    const startedAt = "2026-05-01T10:00:00.000Z";
    stmts.insertSession.run(sessionId, "Live", "active", "/tmp", "claude-opus-4-8", null);
    stmts.insertAgent.run(
      `${sessionId}-main`,
      sessionId,
      "Main",
      "main",
      null,
      "working",
      null,
      null,
      null
    );
    // Live subagent created by the PreToolUse "Agent" hook — no model recorded.
    const liveId = "live-185-qa";
    stmts.insertAgent.run(
      liveId,
      sessionId,
      "QA",
      "subagent",
      "qa",
      "working",
      "task",
      `${sessionId}-main`,
      null
    );
    db.prepare("UPDATE agents SET started_at = ?, ended_at = ?, updated_at = ? WHERE id = ?").run(
      startedAt,
      startedAt,
      startedAt,
      liveId
    );

    const { transcriptPath, base } = buildSubagentDir(sessionId, [
      {
        id: "qa1",
        agentType: "qa",
        lines: buildModelSubLines(
          "qa",
          "claude-haiku-4-5-20251001",
          { input: 10, output: 5 },
          startedAt
        ),
      },
    ]);
    try {
      assert.equal(stmts.getAgent.get(liveId).metadata, null, "live row starts with no model");
      await importHistory.scanAndImportSubagents(dbModule, sessionId, transcriptPath);
      const meta = JSON.parse(stmts.getAgent.get(liveId).metadata || "{}");
      assert.equal(meta.model, "claude-haiku-4-5-20251001", "live row backfilled with real model");
      // No JSONL-keyed duplicate row was created (events merged into the live row).
      assert.equal(stmts.getAgent.get(`${sessionId}-jsonl-qa1`), undefined);
    } finally {
      fs.rmSync(base, { recursive: true, force: true });
    }
  });
});

describe("scanAndImportSubagents — nested subagent hierarchy", () => {
  // A subagent's own transcript records each child it spawned via the Task tool
  // as `toolUseResult.agentId`. Build a spawner file that claims `childIds`.
  function buildSpawnerLines(childIds, startedAt = "2026-06-01T10:00:00.000Z") {
    const lines = [
      { type: "user", timestamp: startedAt, message: { content: [{ type: "text", text: "go" }] } },
    ];
    childIds.forEach((cid, i) => {
      const ts = `2026-06-01T10:00:0${i + 1}.000Z`;
      lines.push({
        type: "assistant",
        timestamp: ts,
        message: {
          model: "claude-opus-4-8",
          content: [{ type: "tool_use", id: `toolu_task_${cid}`, name: "Task", input: {} }],
        },
      });
      lines.push({
        type: "user",
        timestamp: ts,
        toolUseResult: { agentId: cid, status: "completed" },
        message: {
          content: [{ type: "tool_result", tool_use_id: `toolu_task_${cid}`, content: "spawned" }],
        },
      });
    });
    return lines;
  }

  function buildLeafLines(startedAt = "2026-06-01T10:01:00.000Z") {
    return [
      {
        type: "user",
        timestamp: startedAt,
        message: { content: [{ type: "text", text: "work" }] },
      },
      {
        type: "assistant",
        timestamp: startedAt,
        message: {
          model: "claude-haiku-4-5-20251001",
          content: [{ type: "tool_use", id: "toolu_leaf", name: "Read", input: {} }],
        },
      },
    ];
  }

  it("nests subagents under their true spawner instead of flattening to main", async () => {
    const sessionId = "sess-nested-tree";
    const mainAgentId = `${sessionId}-main`;
    stmts.insertSession.run(sessionId, "Nested", "active", "/tmp", "claude-opus-4-8", null);
    stmts.insertAgent.run(
      mainAgentId,
      sessionId,
      "Main",
      "main",
      null,
      "working",
      null,
      null,
      null
    );

    // main → orch; orch → leafA, leafB; main → solo
    const { transcriptPath, base } = buildSubagentDir(sessionId, [
      { id: "orch", lines: buildSpawnerLines(["leafA", "leafB"]) },
      { id: "leafA", lines: buildLeafLines() },
      { id: "leafB", lines: buildLeafLines() },
      { id: "solo", lines: buildLeafLines() },
    ]);

    try {
      const res = await importHistory.scanAndImportSubagents(dbModule, sessionId, transcriptPath);
      assert.equal(res.reparented, 2, "leafA + leafB repointed under orch");

      const parentOf = (id) => stmts.getAgent.get(`${sessionId}-jsonl-${id}`).parent_agent_id;
      assert.equal(parentOf("orch"), mainAgentId, "orch is a direct child of main");
      assert.equal(parentOf("solo"), mainAgentId, "solo stays under main");
      assert.equal(parentOf("leafA"), `${sessionId}-jsonl-orch`, "leafA nests under orch");
      assert.equal(parentOf("leafB"), `${sessionId}-jsonl-orch`, "leafB nests under orch");
    } finally {
      fs.rmSync(base, { recursive: true, force: true });
    }
  });

  it("is idempotent — a second scan repoints nothing", async () => {
    const sessionId = "sess-nested-idem";
    const mainAgentId = `${sessionId}-main`;
    stmts.insertSession.run(sessionId, "NestedIdem", "active", "/tmp", "claude-opus-4-8", null);
    stmts.insertAgent.run(
      mainAgentId,
      sessionId,
      "Main",
      "main",
      null,
      "working",
      null,
      null,
      null
    );

    const { transcriptPath, base } = buildSubagentDir(sessionId, [
      { id: "orch2", lines: buildSpawnerLines(["leafC"]) },
      { id: "leafC", lines: buildLeafLines() },
    ]);

    try {
      const first = await importHistory.scanAndImportSubagents(dbModule, sessionId, transcriptPath);
      assert.equal(first.reparented, 1);
      const second = await importHistory.scanAndImportSubagents(
        dbModule,
        sessionId,
        transcriptPath
      );
      assert.equal(second.reparented, 0, "no re-parenting on the second pass");
      assert.equal(
        stmts.getAgent.get(`${sessionId}-jsonl-leafC`).parent_agent_id,
        `${sessionId}-jsonl-orch2`
      );
    } finally {
      fs.rmSync(base, { recursive: true, force: true });
    }
  });
});

describe("events dedup index", () => {
  it("has a (agent_id, event_type) index so per-tool-event dedup is not a full scan", () => {
    // importSubagentFromJsonl dedups every tool event with
    // "WHERE agent_id = ? AND event_type = ? AND data LIKE '%tool_use_id%'".
    // Without this index each dedup full-scans the events table; on a large DB a
    // single re-import (e.g. the startup sweep touching a subagent-heavy session)
    // takes tens of seconds and blocks the event loop. Guard it from regressing.
    const row = db
      .prepare(
        "SELECT name FROM sqlite_master WHERE type = 'index' AND name = 'idx_events_agent_type'"
      )
      .get();
    assert.ok(row, "idx_events_agent_type must exist to keep subagent dedup indexed");
  });
});
