import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { afterEach, describe, expect, it } from "vitest";
import type { NodeContract, PlanProjection } from "@best-agent/protocol";
import { LocalPlanStore } from "./plan-store.js";
import { LocalThreadStore } from "./thread-store.js";

const storageRoots: string[] = [];

afterEach(() => {
  while (storageRoots.length > 0) {
    rmSync(storageRoots.pop() as string, { force: true, recursive: true });
  }
});

describe("local plan store", () => {
  it("persists graph-backed plan projections through thread-backed plan state", async () => {
    const storageRoot = mkdtempSync(`${tmpdir()}/best-agent-plan-store-`);
    storageRoots.push(storageRoot);

    const threads = new LocalThreadStore({ storageRoot });
    const plans = new LocalPlanStore(threads);
    const thread = await threads.createThread({
      cwd: storageRoot,
      mode: "plan",
      title: "plan store",
    });
    const plan: PlanProjection = {
      acceptanceCriteria: ["All graph fields persisted"],
      affectedPaths: ["packages/protocol/src/index.ts"],
      approvedRevision: undefined,
      evidenceRefs: ["evidence-1"],
      graphId: "graph-1",
      graphRevision: 1,
      openQuestions: ["Can approval bind to current graph revision?"],
      rejectedRevision: undefined,
      rootNodeId: "todo-1",
      risks: ["Migration drift"],
      status: "review_ready",
      summary: "Initial plan projection",
      threadId: thread.id,
      title: "Draft to Projection",
      todos: [
        {
          content: "Add protocol types",
          dependsOn: [],
          id: "todo-1",
          kind: "implementation",
          owner: "parent",
          status: "pending",
        },
      ],
      updatedAt: new Date("2026-04-10T00:00:00.000Z").toISOString(),
    };

    const saved = await plans.saveProjection(plan);
    expect(saved).toMatchObject({
      graphId: "graph-1",
      graphRevision: 1,
      status: "review_ready",
      title: "Draft to Projection",
    });

    const storedState = await threads.getPlanState(thread.id);
    expect(storedState?.plan).toMatchObject({
      graphRevision: 1,
      title: "Draft to Projection",
    });
    expect(storedState?.nodes).toEqual([
      expect.objectContaining({
        nodeId: "todo-1",
        position: 0,
      }),
    ]);

    const approved = await plans.updateProjectionStatus(thread.id, "approved");
    expect(approved).toMatchObject({
      approvedRevision: 1,
      status: "approved",
    });
    expect((await threads.getPlanState(thread.id))?.plan.approvedRevision).toBe(1);

    const updatedTodos = await plans.updateProjectionTodos(thread.id, [
      {
        ...plan.todos[0],
        status: "in_progress",
      },
    ]);
    expect(updatedTodos).toMatchObject({
      approvedRevision: undefined,
      status: "approved",
      graphRevision: 2,
    });

    const updatedState = await threads.getPlanState(thread.id);
    expect(updatedState?.plan).toMatchObject({
      approvedRevision: undefined,
      graphRevision: 2,
    });
    expect(updatedState?.nodes).toEqual([
      expect.objectContaining({
        nodeId: "todo-1",
        todo: expect.objectContaining({
          status: "in_progress",
        }),
      }),
    ]);

    expect(await plans.getProjection(thread.id)).toMatchObject({
      status: "approved",
      graphRevision: 2,
    });
  });

  it("clones plan projections with fresh graph bindings", async () => {
    const storageRoot = mkdtempSync(`${tmpdir()}/best-agent-plan-clone-`);
    storageRoots.push(storageRoot);

    const threads = new LocalThreadStore({ storageRoot });
    const plans = new LocalPlanStore(threads);
    const source = await threads.createThread({
      cwd: storageRoot,
      mode: "plan",
      title: "source plan",
    });
    const target = await threads.createThread({
      cwd: storageRoot,
      mode: "plan",
      title: "target plan",
    });

    await plans.saveProjection({
      acceptanceCriteria: ["Carry plan state into the fork"],
      affectedPaths: ["packages/engine/src/runtime/agent-runtime.ts"],
      approvedRevision: 3,
      evidenceRefs: ["evidence-1"],
      graphId: "graph-source",
      graphRevision: 3,
      openQuestions: [],
      rejectedRevision: undefined,
      rootNodeId: "todo-1",
      risks: [],
      status: "approved",
      summary: "Forkable plan",
      threadId: source.id,
      title: "Forkable plan",
      todos: [
        {
          content: "Keep the plan in the fork",
          dependsOn: [],
          id: "todo-1",
          kind: "implementation",
          owner: "parent",
          status: "pending",
        },
      ],
      updatedAt: new Date("2026-04-10T00:00:00.000Z").toISOString(),
    });
    const sourceState = await threads.getPlanState(source.id);
    const sourceContract: NodeContract = {
      allowedTools: ["shell"],
      budget: {},
      capabilityRequirements: [],
      closurePolicy: {
        deterministicSufficient: true,
        humanVerdictRequired: false,
        modelVerdictAllowed: true,
        onInconclusive: "reopen",
        requiredAgreement: "any-one",
      },
      joinPolicy: {
        allowPartialSuccess: false,
        mode: "aggregate-evidence",
        onPendingOrUnresolved: "block-parent",
        requiresDeterministicMerge: false,
        successCondition: "source contract only",
      },
      nodeId: "todo-1",
      purpose: "source-only contract",
      requiredEvidenceKinds: [],
      writeScope: {
        mode: "read-only",
        paths: [],
      },
    };
    await threads.savePlanState({
      nodeContracts: [sourceContract],
      plan: sourceState!.plan,
    });

    const cloned = await plans.cloneProjection(source.id, target.id);
    expect(cloned).toMatchObject({
      approvedRevision: undefined,
      graphRevision: 1,
      rejectedRevision: undefined,
      threadId: target.id,
      title: "Forkable plan",
    });
    expect(cloned?.graphId).not.toBe("graph-source");
    expect(cloned?.todos).toEqual([
      expect.objectContaining({
        content: "Keep the plan in the fork",
        id: "todo-1",
      }),
    ]);
    expect((await plans.getProjection(target.id))?.graphRevision).toBe(1);
    expect((await plans.getProjection(target.id))?.threadId).toBe(target.id);
    expect((await plans.getProjection(source.id))?.graphRevision).toBe(3);
    expect((await plans.getProjection(source.id))?.threadId).toBe(source.id);
    expect((await threads.getPlanState(source.id))?.nodes[0]?.contract).toMatchObject({
      purpose: "source-only contract",
    });
    expect((await threads.getPlanState(target.id))?.nodes[0]?.contract).toBeUndefined();
  });
});
