import { afterEach, beforeEach, describe, expect, it } from "vitest";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import {
  appendGoalDecision,
  appendGoalEvidence,
  createGoalTask,
  formatGoalBlockingPrerequisites,
  formatGoalPrerequisiteInstruction,
  getActiveGoalRun,
  getGoalRun,
  hashPath,
  loadGoalRuns,
  projectDir,
  reconcileActiveGoalRuns,
  saveGoalRuns,
  summarizeGoalCounts,
  summarizeGoalCountsFromRuns,
  updateGoalTask,
  upsertGoalRun,
  type GoalRun,
} from "./goal-store.js";

let tmpBase: string;
let tmpProject: string;

async function readGoalsFile(cwd: string): Promise<GoalRun[]> {
  const raw = await fs.readFile(path.join(projectDir(cwd), "goals.json"), "utf-8");
  return JSON.parse(raw) as GoalRun[];
}

beforeEach(async () => {
  tmpBase = await fs.mkdtemp(path.join(os.tmpdir(), "goal-store-test-base-"));
  tmpProject = await fs.mkdtemp(path.join(os.tmpdir(), "goal-store-test-project-"));
  process.env.GG_GOALS_BASE = tmpBase;
});

afterEach(async () => {
  delete process.env.GG_GOALS_BASE;
  await fs.rm(tmpBase, { recursive: true, force: true });
  await fs.rm(tmpProject, { recursive: true, force: true });
});

describe("goal store persistence", () => {
  it("returns empty runs for a project with no goal file", async () => {
    await expect(loadGoalRuns(tmpProject)).resolves.toEqual([]);
  });

  it("uses stable normalized per-project hashed directories", async () => {
    const relativeProject = path.relative(process.cwd(), tmpProject);
    expect(hashPath(tmpProject)).toHaveLength(16);
    expect(hashPath(relativeProject)).toBe(hashPath(tmpProject));
    expect(projectDir(relativeProject)).toBe(path.join(tmpBase, hashPath(tmpProject)));

    const run = await upsertGoalRun(relativeProject, {
      id: "normalized-run",
      title: "Normalized",
      goal: "Persist under stable absolute path",
    });
    expect(run.projectPath).toBe(path.resolve(relativeProject));
    await expect(getGoalRun(tmpProject, "normalized-run")).resolves.toMatchObject({
      id: "normalized-run",
    });
  });

  it("creates and updates a goal run", async () => {
    const created = await upsertGoalRun(tmpProject, {
      title: "Refine prompt",
      goal: "Build a repeatable prompt harness",
      successCriteria: ["Harness runs", "Verifier passes"],
      status: "ready",
      evidencePlan: [
        {
          id: "proof-a",
          label: "Browser smoke proof",
          mechanism: "browser",
          description: "Run a local browser smoke test.",
          status: "planned",
        },
      ],
    });

    expect(created.id).toBeTruthy();
    expect(created.status).toBe("ready");
    expect(created.successCriteria).toEqual(["Harness runs", "Verifier passes"]);
    expect(created.evidencePlan[0]).toMatchObject({
      id: "proof-a",
      mechanism: "browser",
      status: "planned",
    });

    const updated = await upsertGoalRun(tmpProject, {
      id: created.id,
      title: "Refine prompt automatically",
      goal: created.goal,
      blockers: ["Need test persona list"],
      status: "blocked",
    });

    expect(updated.id).toBe(created.id);
    expect(updated.title).toBe("Refine prompt automatically");
    expect(updated.createdAt).toBe(created.createdAt);
    expect(updated.updatedAt >= created.updatedAt).toBe(true);
    expect(updated.blockers).toEqual(["Need test persona list"]);
    expect(updated.evidencePlan[0]?.id).toBe("proof-a");

    const loaded = await getGoalRun(tmpProject, created.id.slice(0, 8));
    expect(loaded?.title).toBe("Refine prompt automatically");
  });

  it("formats user-facing prerequisite instructions", async () => {
    const run = await upsertGoalRun(tmpProject, {
      title: "Supabase setup",
      goal: "Connect to Supabase",
      status: "ready",
      prerequisites: [
        {
          id: "token",
          label: "Supabase token",
          status: "missing",
          instructions: "Provide SUPABASE_ACCESS_TOKEN in chat or the local environment.",
        },
      ],
    });

    expect(formatGoalPrerequisiteInstruction(run.prerequisites[0])).toBe(
      "Provide SUPABASE_ACCESS_TOKEN in chat or the local environment.",
    );
    expect(formatGoalBlockingPrerequisites(run)).toBe(
      "Supabase token: Provide SUPABASE_ACCESS_TOKEN in chat or the local environment.",
    );
  });

  it("marks a run blocked when prerequisites are unknown or missing", async () => {
    const run = await upsertGoalRun(tmpProject, {
      title: "Expo audio check",
      goal: "Verify app audio playback programmatically",
      status: "ready",
      prerequisites: [
        { id: "ffmpeg", label: "ffmpeg available", status: "met", evidence: "ffmpeg exists" },
        { id: "sim", label: "iOS simulator available", status: "unknown" },
      ],
    });

    expect(run.status).toBe("blocked");

    const counts = summarizeGoalCountsFromRuns([run]);
    expect(counts.blocked).toBe(1);
    expect(counts.active).toBe(1);
  });

  it("updates task state and appends a new task if given a task input", async () => {
    const task = createGoalTask({
      id: "task-a",
      title: "Create harness",
      prompt: "Create scripts/goal-harness.ts",
    });
    const run = await upsertGoalRun(tmpProject, {
      title: "Harness",
      goal: "Build harness",
      tasks: [task],
    });

    const updatedRun = await updateGoalTask(tmpProject, run.id, "task-a", {
      status: "done",
      lastSummary: "Harness created",
    });
    expect(updatedRun?.tasks[0]?.status).toBe("done");
    expect(updatedRun?.tasks[0]?.lastSummary).toBe("Harness created");
    expect(updatedRun?.tasks[0]?.title).toBe("Create harness");
    expect(updatedRun?.tasks[0]?.prompt).toBe("Create scripts/goal-harness.ts");

    const appendedRun = await updateGoalTask(tmpProject, run.id, "new-task", {
      id: "task-b",
      title: "Run verifier",
      prompt: "Run pnpm test",
      status: "pending",
    });
    expect(appendedRun?.tasks.map((item) => item.id)).toEqual(["task-a", "task-b"]);
  });

  it("discovers a run by id even when the caller cwd does not match the project cwd", async () => {
    const otherProject = await fs.mkdtemp(path.join(os.tmpdir(), "goal-store-test-other-project-"));
    try {
      await upsertGoalRun(tmpProject, {
        id: "099c9f7f-bce7-475c-93b8-d9b3f88a0569",
        title: "Vanished run",
        goal: "Recover discoverability by UUID",
        status: "ready",
      });

      await expect(
        getGoalRun(otherProject, "099c9f7f-bce7-475c-93b8-d9b3f88a0569"),
      ).resolves.toMatchObject({
        id: "099c9f7f-bce7-475c-93b8-d9b3f88a0569",
        projectPath: tmpProject,
      });
      await expect(getGoalRun(otherProject, "099c9f7f")).resolves.toMatchObject({
        id: "099c9f7f-bce7-475c-93b8-d9b3f88a0569",
      });
    } finally {
      await fs.rm(otherProject, { recursive: true, force: true });
    }
  });

  it("appends evidence to an existing run", async () => {
    const run = await upsertGoalRun(tmpProject, {
      title: "Evidence",
      goal: "Capture command output",
    });

    const updated = await appendGoalEvidence(tmpProject, run.id, {
      kind: "command",
      label: "Verifier output",
      content: "Exit code: 0",
    });

    expect(updated?.evidence).toHaveLength(1);
    expect(updated?.evidence[0]?.kind).toBe("command");
    expect(updated?.evidence[0]?.content).toBe("Exit code: 0");
  });

  it("appends durable decision evidence and generates a progress journal", async () => {
    const run = await upsertGoalRun(tmpProject, {
      id: "decision-run",
      title: "Decision",
      goal: "Record controller rationale",
      tasks: [createGoalTask({ id: "task-a", title: "Do work", prompt: "Do work" })],
    });

    await appendGoalDecision(tmpProject, run.id, {
      kind: "start_worker",
      task: run.tasks[0]!,
      attempts: 1,
      reason: "Task is ready.",
    });

    const loaded = await getGoalRun(tmpProject, run.id);
    expect(loaded?.evidence[0]).toMatchObject({
      kind: "summary",
      label: "Goal decision: start_worker",
    });
    expect(loaded?.evidence[0]?.content).toContain("reason=Task is ready.");
    const journal = await fs.readFile(
      path.join(projectDir(tmpProject), "journals", `${run.id}.md`),
      "utf-8",
    );
    expect(journal).toContain("# Decision");
    expect(journal).toContain("Goal decision: start_worker");
  });

  it("loads runs newest-first", async () => {
    const oldRun = await upsertGoalRun(tmpProject, {
      id: "old",
      title: "Old",
      goal: "Old goal",
    });
    const newRun = await upsertGoalRun(tmpProject, {
      id: "new",
      title: "New",
      goal: "New goal",
    });

    const saved = await readGoalsFile(tmpProject);
    saved[0] = { ...oldRun, updatedAt: "2020-01-01T00:00:00.000Z" };
    saved[1] = { ...newRun, updatedAt: "2021-01-01T00:00:00.000Z" };
    await saveGoalRuns(tmpProject, saved);

    const loaded = await loadGoalRuns(tmpProject);
    expect(loaded.map((run) => run.id)).toEqual(["new", "old"]);
  });

  it("falls back to empty runs for corrupt goal JSON", async () => {
    await fs.mkdir(projectDir(tmpProject), { recursive: true });
    await fs.writeFile(path.join(projectDir(tmpProject), "goals.json"), "{not-json", "utf-8");

    await expect(loadGoalRuns(tmpProject)).resolves.toEqual([]);
  });

  it("reconciles stale running workers and verifier state", async () => {
    const run = await upsertGoalRun(tmpProject, {
      id: "stale-run",
      title: "Stale",
      goal: "Recover stale goal state",
      status: "running",
      activeWorkerId: "worker-old",
      tasks: [
        createGoalTask({
          id: "task-running",
          title: "Interrupted task",
          prompt: "Do work",
          status: "running",
          workerId: "worker-old",
        }),
      ],
    });

    const result = await reconcileActiveGoalRuns(tmpProject, {
      isWorkerActive: () => false,
      isVerifierActive: () => false,
    });
    const repaired = result.runs.find((item) => item.id === run.id);

    expect(result.repairedRunIds).toEqual([run.id]);
    expect(repaired?.status).toBe("ready");
    expect(repaired?.activeWorkerId).toBeUndefined();
    expect(repaired?.tasks[0]?.status).toBe("pending");
    expect(repaired?.evidence.map((item) => item.label)).toEqual([
      "Goal worker reconciled",
      "Goal task reconciled",
    ]);
  });

  it("reconciles stale verifying state with blocker evidence", async () => {
    const run = await upsertGoalRun(tmpProject, {
      id: "verifying-run",
      title: "Verifying",
      goal: "Recover verifier",
      status: "verifying",
      verifier: { description: "test", command: "pnpm test" },
    });

    const result = await reconcileActiveGoalRuns(tmpProject, { isVerifierActive: () => false });
    const repaired = result.runs.find((item) => item.id === run.id);

    expect(repaired?.status).toBe("ready");
    expect(repaired?.blockers).toContain(
      "Verifier was interrupted; rerun or continue the Goal to verify again.",
    );
    expect(repaired?.evidence.at(-1)?.label).toBe("Goal verifier reconciled");
  });

  it("preserves observed active Goal workers", async () => {
    const run = await upsertGoalRun(tmpProject, {
      id: "active-run",
      title: "Active",
      goal: "Keep active worker",
      status: "running",
      activeWorkerId: "worker-live",
      tasks: [
        createGoalTask({
          id: "task-live",
          title: "Live task",
          prompt: "Do work",
          status: "running",
          workerId: "worker-live",
        }),
      ],
    });

    const result = await reconcileActiveGoalRuns(tmpProject, {
      isWorkerActive: (workerId) => workerId === "worker-live",
    });

    expect(result.repairedRunIds).toEqual([]);
    expect(result.runs.find((item) => item.id === run.id)?.status).toBe("running");
  });

  it("rejects empty saves that would erase active Goal work", async () => {
    await upsertGoalRun(tmpProject, {
      id: "active-run",
      title: "Active",
      goal: "Do not erase active work",
      status: "running",
      activeWorkerId: "worker-live",
      tasks: [
        createGoalTask({
          id: "task-live",
          title: "Live task",
          prompt: "Do work",
          status: "running",
          workerId: "worker-live",
        }),
      ],
    });

    await saveGoalRuns(tmpProject, []);
    const runs = await loadGoalRuns(tmpProject);

    expect(runs).toHaveLength(1);
    expect(runs[0]).toMatchObject({ id: "active-run", status: "running" });
    expect(runs[0]?.evidence).toEqual(
      expect.arrayContaining([expect.objectContaining({ label: "Goal store write rejected" })]),
    );
  });

  it("selects the active run and summarizes counts", async () => {
    await upsertGoalRun(tmpProject, {
      id: "passed",
      title: "Done",
      goal: "Done goal",
      status: "passed",
    });
    const running = await upsertGoalRun(tmpProject, {
      id: "running",
      title: "Running",
      goal: "Running goal",
      status: "running",
    });
    await upsertGoalRun(tmpProject, {
      id: "failed",
      title: "Failed",
      goal: "Failed goal",
      status: "failed",
    });

    const active = await getActiveGoalRun(tmpProject);
    expect(active?.id).toBe(running.id);

    const counts = await summarizeGoalCounts(tmpProject);
    expect(counts.total).toBe(3);
    expect(counts.running).toBe(1);
    expect(counts.passed).toBe(1);
    expect(counts.failed).toBe(1);
    expect(counts.active).toBe(1);
  });
});
