import { describe, expect, it } from "vitest";
import type {
  Item,
  PlanProjection,
  ThreadEvidenceRecord,
  ThreadId,
  TurnId,
} from "@best-agent/protocol";
import {
  buildApprovedExecutionNodePacket,
  buildInitialPlanModeNodePacket,
  buildOrdinaryTurnNodePacket,
  compileOrdinaryTurnNodePacketPrompt,
  buildPlanModeNodePacket,
  compilePlanModeNodePacketPrompt,
  compileNodePacketPrompt,
  createNodePacketPromptCacheKey,
  selectNodePacketEvidence,
} from "./node-packet.js";

describe("node packet prompt", () => {
  it("builds an approved execution packet from plan, evidence, and recent transcript", () => {
    const threadId = "thread-1" as ThreadId;
    const turn1 = "turn-1" as TurnId;
    const turn2 = "turn-2" as TurnId;
    const turn3 = "turn-3" as TurnId;
    const plan: PlanProjection = {
      acceptanceCriteria: ["App builds successfully"],
      affectedPaths: ["Package.swift", "Sources/VoiceBar/AppDelegate.swift"],
      evidenceRefs: ["evidence-plan"],
      graphId: "graph-1",
      graphRevision: 3,
      openQuestions: [],
      risks: ["Accessibility permissions"],
      status: "approved",
      summary: "Ship the first approved slice without reconstructing the plan from transcript.",
      threadId,
      title: "Voice App Implementation Plan",
      todos: [
        {
          content: "Scaffold the app entry point",
          dependsOn: [],
          id: "todo-1",
          kind: "implementation",
          owner: "parent",
          status: "done",
        },
        {
          content: "Implement Fn key capture",
          dependsOn: [],
          id: "todo-2",
          kind: "implementation",
          owner: "parent",
          status: "in_progress",
        },
      ],
      updatedAt: "2026-04-10T00:00:00.000Z",
    };

    const items: Item[] = [
      {
        createdAt: "2026-04-10T00:00:00.000Z",
        id: "item-1" as never,
        kind: "message",
        role: "user",
        text: "Plan the implementation.",
        threadId,
        turnId: turn1,
      },
      {
        createdAt: "2026-04-10T00:00:01.000Z",
        id: "item-2" as never,
        kind: "message",
        role: "agent",
        text: "Plan approved.",
        threadId,
        turnId: turn2,
      },
      {
        createdAt: "2026-04-10T00:00:02.000Z",
        id: "item-3" as never,
        kind: "message",
        role: "user",
        text: "Start implementing now.",
        threadId,
        turnId: turn3,
      },
    ];

    const evidence: ThreadEvidenceRecord[] = [
      {
        confidence: 0.9,
        createdAt: "2026-04-10T00:00:03.000Z",
        evidenceId: "evidence-plan",
        freshness: "current",
        kind: "workspace-fact",
        payload: {},
        relatedFilePaths: ["Package.swift"],
        summary: "Package manifest already exists and compiles.",
        threadId,
        turnId: turn3,
      },
      {
        confidence: 0.8,
        createdAt: "2026-04-10T00:00:04.000Z",
        evidenceId: "evidence-runtime",
        freshness: "current",
        kind: "tool-outcome",
        payload: {},
        relatedFilePaths: ["Sources/VoiceBar/AppDelegate.swift"],
        summary: "The app entry point now launches without crashing.",
        threadId,
        turnId: turn3,
      },
    ];

    const packet = buildApprovedExecutionNodePacket({
      items,
      plan,
      recentEvidence: evidence,
    });
    const prompt = compileNodePacketPrompt(packet, evidence);

    expect(packet.nodeId).toBe("todo-2");
    expect(packet.nodeObjective).toBe("Implement Fn key capture");
    expect(packet.continuitySnapshot.frontierNodeIds).toEqual(["todo-2"]);
    expect(packet.continuitySnapshot.touchedPaths).toEqual([
      "Package.swift",
      "Sources/VoiceBar/AppDelegate.swift",
    ]);
    expect(packet.boundedTranscriptSuffix).toEqual([
      { role: "user", text: "Plan the implementation.", turnId: turn1 },
      { role: "assistant", text: "Plan approved.", turnId: turn2 },
      { role: "user", text: "Start implementing now.", turnId: turn3 },
    ]);
    expect(prompt).toContain("Approved execution node packet:");
    expect(prompt).toContain("Objective: Implement Fn key capture");
    expect(prompt).toContain("Recent evidence ids: evidence-plan, evidence-runtime");
    expect(prompt).toContain("workspace-fact: Package manifest already exists and compiles.");
    expect(prompt).toContain("assistant: Plan approved.");
  });

  it("derives a stable cache key from packet and evidence contents", () => {
    const packet = {
      artifactRefs: [],
      boundedTranscriptSuffix: [{ role: "user" as const, text: "Implement now." }],
      capabilitySummary: {
        description: "Execution scoped to runtime-selected tools with workspace-write access.",
        id: "node-todo-1",
        kind: "skill" as const,
        source: "runtime",
      },
      continuitySnapshot: {
        frontierNodeIds: ["todo-1"],
        pendingApprovalNodeIds: [],
        pendingJoinNodeIds: [],
        pendingWaitNodeIds: [],
        recentEvidenceIds: ["evidence-1"],
        touchedPaths: ["README.md"],
        unresolvedNodeIds: ["todo-1"],
      },
      graphId: "graph-1",
      graphRevision: 1,
      nodeContract: {
        allowedTools: [],
        budget: {},
        capabilityRequirements: [],
        closurePolicy: {
          deterministicSufficient: false,
          humanVerdictRequired: false,
          modelVerdictAllowed: true,
          onInconclusive: "reopen" as const,
          requiredAgreement: "any-one" as const,
        },
        joinPolicy: {
          allowPartialSuccess: false,
          mode: "all-of" as const,
          onPendingOrUnresolved: "block-parent" as const,
          requiresDeterministicMerge: false,
          successCondition: "Complete the node.",
        },
        nodeId: "todo-1",
        preferredRoutingRole: "change" as const,
        purpose: "Execute approved plan todo: Read the relevant files",
        requiredEvidenceKinds: [],
        writeScope: {
          mode: "workspace-write" as const,
          paths: ["README.md"],
        },
      },
      nodeId: "todo-1",
      nodeObjective: "Read the relevant files",
      relevantEvidenceIds: ["evidence-1"],
      unresolvedBlockers: [],
    };
    const evidence = [
      {
        confidence: 0.8,
        createdAt: "2026-04-10T00:00:00.000Z",
        evidenceId: "evidence-1",
        freshness: "current",
        kind: "workspace-fact",
        payload: {},
        relatedFilePaths: ["README.md"],
        summary: "README already contains the migration notes.",
        threadId: "thread-1" as ThreadId,
        turnId: "turn-1" as TurnId,
      },
    ] satisfies ThreadEvidenceRecord[];

    const first = createNodePacketPromptCacheKey(packet, evidence);
    const second = createNodePacketPromptCacheKey(packet, evidence);
    const changed = createNodePacketPromptCacheKey({ ...packet, graphRevision: 2 }, evidence);

    expect(first).toBe(second);
    expect(changed).not.toBe(first);
  });

  it("keeps referenced and high-signal evidence instead of only the latest records", () => {
    const threadId = "thread-1" as ThreadId;
    const turnId = "turn-1" as TurnId;
    const evidence: ThreadEvidenceRecord[] = Array.from({ length: 8 }, (_, index) => ({
      confidence: 0.5,
      createdAt: `2026-04-10T00:00:${String(index).padStart(2, "0")}.000Z`,
      evidenceId: `recent-${index}`,
      freshness: "current",
      kind: "tool-outcome",
      payload: {},
      summary: `Recent low-signal tool result ${index}.`,
      threadId,
      turnId,
    }));
    evidence.unshift({
      confidence: 0.9,
      createdAt: "2026-04-09T00:00:00.000Z",
      diagnosticCode: "VERIFY_FAIL",
      evidenceId: "critical-old",
      freshness: "current",
      kind: "verification-verdict",
      payload: {},
      summary: "Old verification failed on the important acceptance test.",
      threadId,
      turnId,
    });

    expect(selectNodePacketEvidence(evidence, ["critical-old"]).map((record) => record.evidenceId))
      .toContain("critical-old");
  });

  it("builds a plan-mode packet from graph-backed planning state", () => {
    const threadId = "thread-1" as ThreadId;
    const turn1 = "turn-1" as TurnId;
    const turn2 = "turn-2" as TurnId;
    const plan: PlanProjection = {
      acceptanceCriteria: ["The revised plan is still coherent and reviewable"],
      affectedPaths: ["README.md", "docs/ARCHITECTURE.md"],
      evidenceRefs: ["evidence-plan"],
      graphId: "graph-1",
      graphRevision: 2,
      openQuestions: ["Does the migration need a follow-up benchmark?"],
      risks: ["A hidden dependency could widen the slice"],
      status: "review_ready",
      summary: "Refine the current plan without treating rendered markdown as the source of truth.",
      threadId,
      title: "Kernel Cutover Planning",
      todos: [
        {
          content: "Audit the remaining packet gaps",
          dependsOn: [],
          id: "todo-1",
          kind: "research",
          owner: "parent",
          status: "done",
        },
        {
          content: "Update the plan after reviewing the remaining gaps",
          dependsOn: [],
          id: "todo-2",
          kind: "implementation",
          owner: "parent",
          status: "in_progress",
        },
      ],
      updatedAt: "2026-04-10T00:00:00.000Z",
    };

    const items: Item[] = [
      {
        createdAt: "2026-04-10T00:00:00.000Z",
        id: "item-1" as never,
        kind: "message",
        role: "user",
        text: "Plan the implementation.",
        threadId,
        turnId: turn1,
      },
      {
        createdAt: "2026-04-10T00:00:01.000Z",
        id: "item-2" as never,
        kind: "message",
        role: "agent",
        text: "Initial plan saved.",
        threadId,
        turnId: turn1,
      },
      {
        createdAt: "2026-04-10T00:00:02.000Z",
        id: "item-3" as never,
        kind: "message",
        role: "user",
        text: "Refine the plan to call out the benchmark risk.",
        threadId,
        turnId: turn2,
      },
    ];

    const evidence: ThreadEvidenceRecord[] = [
      {
        confidence: 0.9,
        createdAt: "2026-04-10T00:00:03.000Z",
        evidenceId: "evidence-plan",
        freshness: "current",
        kind: "workspace-fact",
        payload: {},
        relatedFilePaths: ["docs/ARCHITECTURE.md"],
        summary: "The architecture notes already describe the benchmark gate.",
        threadId,
        turnId: turn2,
      },
    ];

    const packet = buildPlanModeNodePacket({
      items,
      plan,
      recentEvidence: evidence,
    });
    const prompt = compilePlanModeNodePacketPrompt(packet, plan, evidence);

    expect(packet.nodeId).toBe("todo-2");
    expect(packet.nodeContract.preferredRoutingRole).toBe("plan");
    expect(packet.nodeContract.writeScope).toEqual({
      mode: "read-only",
      paths: ["README.md", "docs/ARCHITECTURE.md"],
    });
    expect(packet.continuitySnapshot.pendingApprovalNodeIds).toEqual(["todo-2"]);
    expect(prompt).toContain("Plan mode node packet:");
    expect(prompt).toContain("Current plan status: review_ready");
    expect(prompt).toContain("Title: Kernel Cutover Planning");
    expect(prompt).toContain("- [~] Update the plan after reviewing the remaining gaps");
    expect(prompt).toContain(
      "workspace-fact: The architecture notes already describe the benchmark gate.",
    );
    expect(prompt).toContain("assistant: Initial plan saved.");
    expect(prompt).toContain("Use this node packet as the primary planning context");
  });

  it("builds an initial plan-mode packet before any plan projection exists", () => {
    const threadId = "thread-1" as ThreadId;
    const turn1 = "turn-1" as TurnId;
    const items: Item[] = [
      {
        createdAt: "2026-04-10T00:00:00.000Z",
        id: "item-1" as never,
        kind: "message",
        role: "user",
        text: "Plan the implementation.",
        threadId,
        turnId: turn1,
      },
    ];
    const evidence: ThreadEvidenceRecord[] = [
      {
        confidence: 0.7,
        createdAt: "2026-04-10T00:00:01.000Z",
        evidenceId: "evidence-1",
        freshness: "current",
        kind: "workspace-fact",
        payload: {},
        relatedFilePaths: ["README.md"],
        summary: "README already captures the current migration goals.",
        threadId,
        turnId: turn1,
      },
    ];

    const packet = buildInitialPlanModeNodePacket({
      items,
      recentEvidence: evidence,
      threadId,
    });
    const prompt = compilePlanModeNodePacketPrompt(packet, null, evidence);

    expect(packet.graphId).toBe("thread-1:plan-authoring");
    expect(packet.graphRevision).toBe(0);
    expect(packet.nodeId).toBe("plan-root");
    expect(packet.nodeObjective).toBe("Author the initial thread plan.");
    expect(packet.nodeContract.preferredRoutingRole).toBe("plan");
    expect(packet.nodeContract.writeScope).toEqual({
      mode: "read-only",
      paths: [],
    });
    expect(prompt).toContain("Plan mode node packet:");
    expect(prompt).toContain("Current plan status: (none yet)");
    expect(prompt).toContain("Title: (none yet)");
    expect(prompt).toContain(
      "workspace-fact: README already captures the current migration goals.",
    );
    expect(prompt).toContain(
      "This node accepts only a structured plan_update action through the __best_agent_plan_update tool with the updated plan state.",
    );
  });

  it("builds an ordinary-turn packet from durable evidence and active paths", () => {
    const threadId = "thread-1" as ThreadId;
    const turn1 = "turn-1" as TurnId;
    const turn2 = "turn-2" as TurnId;
    const items: Item[] = [
      {
        createdAt: "2026-04-10T00:00:00.000Z",
        id: "item-1" as never,
        kind: "message",
        role: "user",
        text: "tool:write-scratch notes.txt|durable evidence",
        threadId,
        turnId: turn1,
      },
      {
        createdAt: "2026-04-10T00:00:01.000Z",
        id: "item-2" as never,
        kind: "tool-call",
        toolName: "write-scratch",
        input: "notes.txt|durable evidence",
        callId: "call-1" as never,
        threadId,
        turnId: turn1,
      },
      {
        createdAt: "2026-04-10T00:00:02.000Z",
        id: "item-3" as never,
        kind: "tool-result",
        outcome: "success",
        toolName: "write-scratch",
        output: "stored durable evidence",
        callId: "call-1" as never,
        threadId,
        turnId: turn1,
      },
      {
        createdAt: "2026-04-10T00:00:03.000Z",
        id: "item-4" as never,
        kind: "message",
        role: "user",
        text: "What already happened here?",
        threadId,
        turnId: turn2,
      },
    ];
    const evidence: ThreadEvidenceRecord[] = [
      {
        confidence: 0.8,
        createdAt: "2026-04-10T00:00:04.000Z",
        evidenceId: "evidence-1",
        freshness: "current",
        kind: "tool-outcome",
        payload: {},
        relatedFilePaths: ["notes.txt"],
        summary: "Scratch notes already contain durable evidence.",
        threadId,
        turnId: turn2,
      },
    ];

    const packet = buildOrdinaryTurnNodePacket({
      activePaths: ["notes.txt"],
      currentObjective: "Summarize the prior scratch write.",
      items,
      recentEvidence: evidence,
      threadId,
    });
    const prompt = compileOrdinaryTurnNodePacketPrompt(packet, evidence);

    expect(packet.graphId).toBe("thread-1:ordinary-turn");
    expect(packet.nodeObjective).toBe("Summarize the prior scratch write.");
    expect(packet.nodeContract.preferredRoutingRole).toBe("change");
    expect(packet.continuitySnapshot.touchedPaths).toEqual(["notes.txt"]);
    expect(prompt).toContain("Ordinary turn node packet:");
    expect(prompt).toContain("Objective: Summarize the prior scratch write.");
    expect(prompt).toContain("tool-outcome: Scratch notes already contain durable evidence.");
    expect(prompt).toContain("user: What already happened here?");
    expect(prompt).toContain("Convergence policy:");
    expect(prompt).toContain("verify it directly before doing more exploration");
    expect(prompt).toContain("Use this node packet as the primary ordinary-turn context");
  });
});
